commit 65fca606221054c8734512ae7882b01d8688cd03
Author: fish
Date: Mon Oct 26 16:28:14 2026 +0800
Initial commit: web_hghl_server
Co-Authored-By: Claude Opus 4.7
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd3fbf7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+tool
+.DS_Store
+.project
+.idea
+.vscode
+/temp
+/run
+*.tar.gz
+*.tar.xz
+# Google 服务账号私钥(Indexing API),只能随部署单独投放,禁止入库。
+composed-anvil-*.json
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..589d3fe
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+include tool/build/Makefile.help
+
+## 更新APP文档
+update_app_doc:
+ cd app && swag init --parseDependency --parseDepth 4
+
+## 更新Web文档
+update_h5_doc:
+ cd h5app && swag init --parseDependency --parseDepth 4
+
+## 更新Web文档
+update_web_doc:
+ cd web && swag init --parseDependency --parseDepth 4
+
+## 更新所有项目文档
+update_all_docs: update_app_doc update_h5_doc update_web_doc
+
+## 更新测试服Web
+deploy_test_web:
+ cd web && ./deploy_test.sh
+
+## 更新测试服H5
+deploy_test_h5:
+ cd h5app && ./deploy_test.sh
+
+## 更新测试服APP
+deploy_test_app:
+ cd app && ./deploy_test.sh
+
+## 更新测试服APP + H5
+deploy_test_h5_and_app: deploy_test_app deploy_test_h5
+
+test:
+ ./tool/build/check_syntax.sh
+
+.DEFAULT_GOAL := help
\ No newline at end of file
diff --git a/app/Makefile b/app/Makefile
new file mode 100755
index 0000000..8008b1a
--- /dev/null
+++ b/app/Makefile
@@ -0,0 +1,28 @@
+GOPATH:=$(shell go env GOPATH)
+
+GIT_BRANCH:=$(shell git symbolic-ref --short -q HEAD)
+GIT_VERSION:=$(shell git log --pretty=oneline -p -1 | head -n 1 | awk '{print $$1}')
+BUILD_DATE:=$(shell date '+%Y-%m-%d %H:%M:%S %z')
+BUILD_IP:=$(shell ifconfig | grep inet | grep -v grep | grep -v "inet6" | grep -v "127.0.0.1" | awk '{print $$2}')
+VERSON_PATH:=pfshop/pub/srvversion
+
+APP_BUILD_FLAG:=-ldflags "-X '$(VERSON_PATH).GIT_BRANCH=$(GIT_BRANCH)' -X '$(VERSON_PATH).GIT_VERSION=$(GIT_VERSION)' -X '$(VERSON_PATH).BUILD_DATE=$(BUILD_DATE)' -X '$(VERSON_PATH).BUILD_IP=$(BUILD_IP)'"
+
+
+.PHONY: test deploy
+deploy:
+ @echo "deploy, GOPATH=$(GOPATH)"
+ go clean
+ rm -f web_mdcom_app
+ env GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_mdcom_app -v
+ tar czf web_mdcom_app.tar.gz web_mdcom_app
+ rm -f web_mdcom_app
+
+test:
+ @echo "test, GOPATH=$(GOPATH)"
+ GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_mdcom_app
+ tar czf web_mdcom_app.tar.gz web_mdcom_app docs/
+ rm -f web_mdcom_app
+localtest:
+ @echo "test, GOPATH=$(GOPATH)"
+ go build $(APP_BUILD_FLAG) -o app
\ No newline at end of file
diff --git a/app/api/actressapi/actress.go b/app/api/actressapi/actress.go
new file mode 100644
index 0000000..2fb4ad3
--- /dev/null
+++ b/app/api/actressapi/actress.go
@@ -0,0 +1,57 @@
+package actressapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/actressser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// QueryActressWorkList 指定女优详情和作品混排列表
+// @Tags 女优相关接口
+// @Summary 指定女优详情和作品列表 -- by 王鸣
+// @Description 查询指定女优详情和作品列表,支持上架时间/观看量/收藏量升降序,每间隔8条作品数据插入一个广告
+// @Accept json
+// @Produce json
+// @Param params query proto.QueryActressWorkListReq true "指定女优作品列表请求"
+// @Success 200 {object} proto.QueryActressWorkListResp{list=[]proto.QueryVideoListItem{VideoInfo=proto.VideoInfo}} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/app/actress/work/list [get]
+func QueryActressWorkList(c *gin.Context) {
+ var req proto.QueryActressWorkListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := actressser.QueryActressWorkList(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// ActressList 女优榜单列表
+// @Tags 女优相关接口
+// @Summary 女优榜单列表 -- by 王鸣
+// @Description 返回导航、广告和按热度值降序排列的女优列表
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.QueryActressListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/actress/list [get]
+func ActressList(c *gin.Context) {
+ data, err := actressser.ActressListData(c, c.Request.URL.Path)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ if data == nil {
+ common.ServerJSON(c, srvdef.ErrDbQueryError, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
diff --git a/app/api/appinfoapi/appinfo.go b/app/api/appinfoapi/appinfo.go
new file mode 100644
index 0000000..2a77b54
--- /dev/null
+++ b/app/api/appinfoapi/appinfo.go
@@ -0,0 +1,54 @@
+package appinfoapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/appser"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// QueryAppList 查询应用列表
+// @Tags 应用列表
+// @Summary 查询应用列表
+// @Description 返回应用分类列表,每个分类下含应用信息列表;categoryId=0 返回第一个分类
+// @Accept json
+// @Produce json
+// @Param categoryId query int false "当前分类ID,0表示默认第一个分类"
+// @Success 200 {object} proto.AppListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/applist/list [get]
+func QueryAppList(c *gin.Context) {
+ var req struct {
+ CategoryId int64 `form:"categoryId"`
+ }
+ if err := c.ShouldBindQuery(&req); err != nil || req.CategoryId < 0 {
+ req.CategoryId = 0
+ }
+
+ categoryList, category, serr := appser.AppCategoryData(c, req.CategoryId)
+ if serr != nil {
+ common.ServerJSON(c, serr, "")
+ return
+ }
+
+ if categoryList == nil {
+ categoryList = make([]proto.AppCategoryInfo, 0)
+ }
+
+ resp := proto.AppListResp{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ Config: configser.GetRequestConfig(c),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ CategoryList: categoryList,
+ Category: category,
+ }
+ configser.RewriteImageCdn(c, &resp)
+ common.ServerJSON(c, srvdef.Success, resp)
+}
diff --git a/app/api/authapi/auth.go b/app/api/authapi/auth.go
new file mode 100644
index 0000000..96701cc
--- /dev/null
+++ b/app/api/authapi/auth.go
@@ -0,0 +1,186 @@
+package authapi
+
+import (
+ "videoweb/app/service/authser"
+ "videoweb/app/service/wordsfilterser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// extractUA 从 gin context 中提取 UA 中间件解析到的设备/系统信息
+func extractUA(ctx *gin.Context) authser.UAInfo {
+ return authser.UAInfo{
+ Device: ctx.GetString(consts.CtxDevType),
+ SysType: pubdef.SysType(ctx.GetString(consts.CtxSysType)),
+ AppVer: ctx.GetString(consts.CtxVersion),
+ IP: ctx.ClientIP(),
+ }
+}
+
+type guestLoginReq struct {
+ DevID string `json:"devID"` // 设备 ID(首选)
+ Mac string `json:"mac"` // 设备 MAC 地址(备用)
+ DeviceId string `json:"deviceId"` // 设备 ID 第二版本(备用)
+}
+
+type registerReq struct {
+ AccountNo string `json:"accountNo" binding:"required"` // 账号
+ Password string `json:"password" binding:"required"` // 明文密码
+ AgentInviteCode string `json:"agentInviteCode"` // 邀请人的邀请码(可选)
+}
+
+type loginReq struct {
+ Account string `json:"account" binding:"required"` // 账号
+ Password string `json:"password" binding:"required"` // 明文密码
+}
+
+type updateProfileReq struct {
+ AccountNo string `json:"accountNo" binding:"required"` // 账号(必填,用于定位用户)
+ Avatar *string `json:"avatar"` // 新头像 URL(可选)
+ NikeName *string `json:"nikeName"` // 昵称
+ OldPassword *string `json:"oldPassword"` // 旧密码明文(修改密码时必填)
+ NewPassword *string `json:"newPassword"` // 新密码明文(修改密码时必填)
+}
+
+// GuestLogin 游客登录/自动注册
+// @Tags 用户认证
+// @Summary 游客登录
+// @Description 按设备 ID 查找已有游客账号,找不到则自动注册。若请求携带有效 token 则优先复用该用户。
+// @Accept json
+// @Produce json
+// @Param param body guestLoginReq true "设备信息"
+// @Success 200 {object} authser.LoginResp "登录成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/login/guest [POST]
+func GuestLogin(ctx *gin.Context) {
+ var req guestLoginReq
+ _ = ctx.ShouldBind(&req)
+
+ // 若设备信息为空,尝试从 UA 中间件补充 DevID
+ if req.DevID == "" {
+ req.DevID = ctx.GetString(consts.CtxDevId)
+ }
+
+ currentUserId := common.GetUserId(ctx)
+ resp, serr := authser.GuestLogin(ctx, &authser.GuestLoginReq{
+ DevID: req.DevID,
+ Mac: req.Mac,
+ DeviceId: req.DeviceId,
+ UA: extractUA(ctx),
+ }, currentUserId)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Register 账号注册(游客升级 or 直接注册)
+// @Tags 用户认证
+// @Summary 注册
+// @Description 已登录游客传入账号密码完成升级;未登录用户直接注册新账号。
+// @Accept json
+// @Produce json
+// @Param param body registerReq true "注册参数"
+// @Success 200 {object} authser.LoginResp "注册成功,返回 token 及用户基本信息"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/register [POST]
+func Register(ctx *gin.Context) {
+ var req registerReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ currentUserId := common.GetUserId(ctx)
+ resp, serr := authser.Register(ctx, req.AccountNo, req.Password, req.AgentInviteCode, currentUserId, extractUA(ctx))
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Login 用户登录
+// @Tags 用户认证
+// @Summary 登录
+// @Description 账号均可作为登录凭证
+// @Accept json
+// @Produce json
+// @Param param body loginReq true "登录参数"
+// @Success 200 {object} authser.LoginResp "登录成功,返回 token 及用户基本信息"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/login [POST]
+func Login(ctx *gin.Context) {
+ var req loginReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := authser.Login(ctx, req.Account, req.Password, extractUA(ctx))
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// GetInfo 获取当前登录用户信息
+// @Tags 用户认证
+// @Summary 获取当前登录用户信息
+// @Description 根据当前登录 token 查询用户基础信息
+// @Accept json
+// @Produce json
+// @Success 200 {object} authser.UserInfoResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/getInfo [get]
+func GetInfo(ctx *gin.Context) {
+ userId := common.GetUserId(ctx)
+ if userId <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, nil)
+ return
+ }
+ resp, serr := authser.GetInfo(ctx, userId)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// UpdateProfile 修改用户资料
+// @Tags 用户认证
+// @Summary 修改用户资料(头像 / 密码,可任意组合)
+// @Description 账号必填;avatar、oldPassword+newPassword 均为可选,至少提供一项。修改密码时旧密码和新密码必须同时提供。
+// @Accept json
+// @Produce json
+// @Param param body updateProfileReq true "修改资料参数"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/profile [POST]
+func UpdateProfile(ctx *gin.Context) {
+ var req updateProfileReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if req.NikeName != nil && !wordsfilterser.Validate(*req.NikeName) {
+ common.ServerJSON(ctx, srvdef.ErrContainIllegalWord, "包含非法字符")
+ return
+ }
+ serr := authser.UpdateProfile(ctx, &authser.UpdateProfileReq{
+ AccountNo: req.AccountNo,
+ Avatar: req.Avatar,
+ NikeName: req.NikeName,
+ OldPassword: req.OldPassword,
+ NewPassword: req.NewPassword,
+ })
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/app/api/commentapi/comment.go b/app/api/commentapi/comment.go
new file mode 100644
index 0000000..0cbac23
--- /dev/null
+++ b/app/api/commentapi/comment.go
@@ -0,0 +1,102 @@
+package commentapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/commentser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 评论列表
+// @Tags 评论APP
+// @Summary 评论列表 -- by 王鸣
+// @Description 按对象查询树形评论列表
+// @Accept json
+// @Produce json
+// @Param params query proto.CommentListReq true "评论列表请求"
+// @Success 200 {object} proto.CommentListResp{list=[]proto.CommentInfo} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/comment/list [get]
+func List(ctx *gin.Context) {
+ var req proto.CommentListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := commentser.ListCommentTree(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SendComment 发布评论
+// @Tags 评论APP
+// @Summary 发布评论 -- by 王鸣
+// @Description 发布评论
+// @Accept json
+// @Produce json
+// @Param param body proto.CommentSendReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/app/comment/send [post]
+func SendComment(ctx *gin.Context) {
+ var in proto.CommentSendReq
+ if err := ctx.ShouldBindJSON(&in); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ userId, exists := ctx.Get(consts.CtxUserID)
+ if !exists {
+ common.ServerJSON(ctx, srvdef.InvalidToken, nil)
+ return
+ }
+ userIdInt64, ok := userId.(int64)
+ if !ok || userIdInt64 <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, nil)
+ return
+ }
+
+ ip := ctx.ClientIP()
+ ua, _ := common.GetUA(ctx)
+ err := commentser.SendComment(ctx, ip, userIdInt64, &in, ua)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, "")
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "success")
+}
+
+// SetLike 设置评论点赞状态
+// @Tags 评论APP
+// @Summary 评论点赞/取消点赞 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body proto.CommentLikeSetReq true "点赞参数"
+// @Success 200 {object} proto.CommentLikeSetResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/comment/like/set [post]
+func SetLike(ctx *gin.Context) {
+ var req proto.CommentLikeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ userId := common.GetUserId(ctx)
+ if userId <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, nil)
+ return
+ }
+ resp, err := commentser.SetCommentLike(ctx, userId, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/config/config.go b/app/api/config/config.go
new file mode 100644
index 0000000..1dc8b3f
--- /dev/null
+++ b/app/api/config/config.go
@@ -0,0 +1,66 @@
+package config
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// FooterInfo 查询APP底部信息
+// @Tags app配置相关接口
+// @Summary 查询APP底部信息 -- by 王鸣
+// @Description 返回APP底部品牌、底部导航和联系方式
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.FooterInfoResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/config/footer/info [get]
+func FooterInfo(ctx *gin.Context) {
+ resp := &proto.FooterInfoResp{
+ FooterInfo: configser.GetFooterInfo(),
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// RichTextList 查询APP富文本内容列表
+// @Tags app配置相关接口
+// @Summary 查询APP富文本内容列表 -- by 王鸣
+// @Description 返回启用状态的富文本内容,支持按ids或titles过滤
+// @Accept json
+// @Produce json
+// @Param ids query string false "富文本ID,多个用英文逗号分隔"
+// @Param titles query string false "富文本标题,多个用英文逗号分隔"
+// @Success 200 {object} proto.AppRichTextListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/config/richtext/list [get]
+func RichTextList(ctx *gin.Context) {
+ resp := configser.GetAppRichTextList(ctx.Query("ids"), ctx.Query("titles"))
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ContentTagList 查询APP统一标签列表
+// @Tags app配置相关接口
+// @Summary 查询APP统一标签列表 -- by 王鸣
+// @Description 返回启用且有绑定内容数量的统一标签,contentTagType非必填,按绑定内容数量倒序排序
+// @Accept json
+// @Produce json
+// @Param params query proto.AppContentTagListReq false "统一标签列表请求"
+// @Success 200 {object} proto.AppContentTagListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/content/tag/list [get]
+func ContentTagList(ctx *gin.Context) {
+ req := &proto.AppContentTagListReq{}
+ if err := ctx.ShouldBindQuery(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, err := configser.AppContentTagList(ctx, req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/dhapi/dh.go b/app/api/dhapi/dh.go
new file mode 100644
index 0000000..26d6bd0
--- /dev/null
+++ b/app/api/dhapi/dh.go
@@ -0,0 +1,142 @@
+package dhapi
+
+import (
+ "fmt"
+ "strconv"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/searchser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// DHApi 导航
+// @Tags 导航
+// @Summary 导航
+// @Description 导航
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.DHHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /dh [get]
+func DHApi(c *gin.Context) {
+ data := proto.DHHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ HotNavs: configser.GetHotNavs(),
+ }
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// BusinessCooperationApi 导航
+// @Tags 导航
+// @Summary 导航
+// @Description 导航
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.HomeHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /businessCooperation [get]
+func BusinessCooperationApi(c *gin.Context) {
+ data := proto.DHHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ Advitise: configser.GetAdvitise(),
+ }
+ data.CompletePath = "/businessCooperation"
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// ServicesProjectApi 导航
+// @Tags 导航
+// @Summary 导航
+// @Description 导航
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.HomeHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /servicesProject [get]
+func ServicesProjectApi(c *gin.Context) {
+ data := proto.DHHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ Advitise: configser.GetAdvitise(),
+ }
+ data.CompletePath = "/servicesProject"
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// ConsultingApi 招募
+// @Tags 招募
+// @Summary 招募
+// @Description 招募
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.HomeHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /consulting [get]
+func ConsultingApi(c *gin.Context) {
+ data := proto.DHConsultingHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ }
+ data.CompletePath = "/consulting"
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// ConsultingInfoApi 招募详情
+// @Tags 招募详情
+// @Summary 招募详情
+// @Description 招募详情
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.HomeHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /consultinginfo/:id [get]
+func ConsultingInfoApi(c *gin.Context) {
+ data := proto.DHConsultingInfoHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ Advitise: configser.GetAdvitise(),
+ }
+ idStr := c.Param("id")
+ id, err := strconv.ParseInt(idStr, 10, 64)
+ if err == nil && id > 0 {
+ data.CompletePath = fmt.Sprintf("/consultingInfo/%d", id)
+ }
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
diff --git a/app/api/healthapi/health.go b/app/api/healthapi/health.go
new file mode 100644
index 0000000..f9f9d56
--- /dev/null
+++ b/app/api/healthapi/health.go
@@ -0,0 +1,42 @@
+package healthapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/healthser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HealthCheck 健康检查接口
+func HealthCheck(ctx *gin.Context) {
+ req := &proto.HealthCheckRequest{}
+ if err := ctx.ShouldBindJSON(req); err != nil {
+ log.Warn("health check bind error", log.E(err))
+ respond(ctx, srvdef.ErrParamError, err.Error(), nil)
+ return
+ }
+ log.Info("health check request", log.Any("req", req))
+
+ data, serr := healthser.HealthCheck(ctx.Request.Context(), req)
+ if serr != nil {
+ log.Warn("health check failed", log.E(serr), log.Any("req", req))
+ respond(ctx, serr.Code, serr.GetMsg(), nil)
+ return
+ }
+
+ log.Info("health check response", log.Any("resp", data))
+ respond(ctx, srvdef.Success, "success", data)
+}
+
+func respond(ctx *gin.Context, code srvdef.CodeType, msg string, data interface{}) {
+ if msg == "" {
+ msg = code.Msg()
+ }
+ ctx.JSON(200, gin.H{
+ "code": code,
+ "msg": msg,
+ "data": data,
+ })
+}
diff --git a/app/api/home/home.go b/app/api/home/home.go
new file mode 100644
index 0000000..1bb7d8e
--- /dev/null
+++ b/app/api/home/home.go
@@ -0,0 +1,53 @@
+package home
+
+import (
+ "videoweb/app/service/homeser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Home 首页
+// @Tags 首页
+// @Summary 首页 --by 旺鸣
+// @Description 首页分类数据
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.DataResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/home [get]
+func Home(c *gin.Context) {
+ data, err := homeser.HomeData(c)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// HomeBase 首页公共数据
+// @Tags 首页
+// @Summary 首页公共数据 --by 旺鸣
+// @Description 只返回翻译、配置、底部翻译、广告和公告
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.HomeBaseResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/home/base [get]
+func HomeBase(c *gin.Context) {
+ common.JSONWriterCache(c, srvdef.Success, homeser.HomeBaseData(c))
+}
+
+// HomeTags 首页标签数据
+// @Tags 首页
+// @Summary 首页标签数据 --by 旺鸣
+// @Description 返回视频、短剧的标签信息,以及基础导航、配置、广告和公告
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.HomeTagsResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/home/tags [get]
+func HomeTags(c *gin.Context) {
+ common.JSONWriterCache(c, srvdef.Success, homeser.HomeTagsData(c))
+}
diff --git a/app/api/lawandtrust/lawandtrust.go b/app/api/lawandtrust/lawandtrust.go
new file mode 100644
index 0000000..5f95ce5
--- /dev/null
+++ b/app/api/lawandtrust/lawandtrust.go
@@ -0,0 +1,122 @@
+package lawandtrust
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/searchser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// UserAgreementApi 用户协议
+// @Tags 用户协议
+// @Summary 用户协议
+// @Description 用户协议
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /useragreement [get]
+func UserAgreementApi(c *gin.Context) {
+ data := proto.LawAndTrustHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ Advitise: configser.GetAdvitise(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ }
+
+ data.CompletePath = "/useragreement"
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+
+}
+
+// PrivacyPolicyApi 隐私策略
+// @Tags 隐私策略
+// @Summary 隐私策略
+// @Description 隐私策略
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /privacyPolicy [get]
+func PrivacyPolicyApi(c *gin.Context) {
+ data := proto.LawAndTrustHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ Advitise: configser.GetAdvitise(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ }
+ data.CompletePath = "/useragreement"
+
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+
+}
+
+// DmcaApi dmca
+// @Tags dmca
+// @Summary dmca
+// @Description dmca
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /dmca [get]
+func DmcaApi(c *gin.Context) {
+ data := proto.LawAndTrustHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ Advitise: configser.GetAdvitise(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ }
+
+ data.CompletePath = "/useragreement"
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// StatementApi 合规声明
+// @Tags 合规声明
+// @Summary 合规声明
+// @Description 合规声明
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /statement [get]
+func StatementApi(c *gin.Context) {
+ data := proto.LawAndTrustHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetRequestConfig(c),
+ Announcement: configser.GetAnnouncement(),
+ Advitise: configser.GetAdvitise(),
+ HotSearch: searchser.GetHotSearchItem(c),
+ }
+
+ data.CompletePath = "/useragreement"
+ configser.RewriteImageCdn(c, &data)
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
diff --git a/app/api/navigationapi/header.go b/app/api/navigationapi/header.go
new file mode 100644
index 0000000..74c884b
--- /dev/null
+++ b/app/api/navigationapi/header.go
@@ -0,0 +1,27 @@
+package navigationapi
+
+import (
+ "videoweb/app/service/navigationser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HeaderNavigation 查询头部导航配置
+// @Tags app导航相关接口
+// @Summary 查询头部导航配置 -- by 王鸣
+// @Description 查询导航类型为URL模式和二级导航的头部导航配置
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.AppTranslationInfo "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/navigation/header [get]
+func HeaderNavigation(ctx *gin.Context) {
+ resp, err := navigationser.HeaderNavigation(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.JSONWriterCache(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/playletapi/playlet.go b/app/api/playletapi/playlet.go
new file mode 100644
index 0000000..ea4d6ce
--- /dev/null
+++ b/app/api/playletapi/playlet.go
@@ -0,0 +1,484 @@
+package playletapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/playletser"
+ "videoweb/app/service/slugser"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HomePlaylet app端短剧首页
+// @Tags app短剧相关接口
+// @Summary app端短剧首页 -- by 王鸣
+// @Description 返回短剧Banner、首页配置模块、公告、广告和SEO信息
+// @Produce json
+// @Success 200 {object} proto.HomePlayletResponse "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/home [get]
+func HomePlaylet(ctx *gin.Context) {
+ resp, err := playletser.Home(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// HomeConfigList 按首页配置分页查询内容
+// @Tags app短剧相关接口
+// @Summary app端首页配置内容列表 -- by 王鸣
+// @Description 按首页配置ID或slug分页查询内容,同时传入时优先使用slug
+// @Produce json
+// @Param params query proto.PlayletHomeConfigListReq true "首页配置内容分页请求"
+// @Success 200 {object} proto.PlayletHomeConfigListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/home-config/list [get]
+func HomeConfigList(ctx *gin.Context) {
+ var req proto.PlayletHomeConfigListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.HomeConfigList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// TopicDetail 查询短剧题材或视频分类专题详情
+// @Tags app短剧相关接口
+// @Summary app端专题详情 -- by 王鸣
+// @Produce json
+// @Param params query proto.PlayletTopicDetailReq true "专题详情请求"
+// @Success 200 {object} proto.PlayletTopicDetailResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/topic/detail [get]
+func TopicDetail(ctx *gin.Context) {
+ var req proto.PlayletTopicDetailReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.TopicDetail(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ThemeList 查询全部已启用短剧题材
+// @Tags app短剧相关接口
+// @Summary app端短剧题材列表
+// @Produce json
+// @Success 200 {object} proto.PlayletThemeListResp "成功"
+// @Router /api/app/playlet/theme/list [get]
+func ThemeList(ctx *gin.Context) {
+ resp, err := playletser.PlayletThemeList(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// List 分页查询已上架短剧并穿插广告
+// @Tags app短剧相关接口
+// @Summary app端短剧列表 -- by 王鸣
+// @Description 按题材和排序筛选已上架短剧,最近更新按更新时间降序,每4部短剧穿插1条广告
+// @Produce json
+// @Param params query proto.PlayletListReq true "短剧列表请求"
+// @Success 200 {object} proto.PlayletListResp{list=[]proto.PlayletRecommendListItem} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/list [get]
+func List(ctx *gin.Context) {
+ var req proto.PlayletListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.PlayletList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ContentList 分页查询上架短剧或视频并穿插广告
+// @Tags app短剧相关接口
+// @Summary app端短剧或视频全站排行榜列表 -- by 王鸣
+// @Description 按观看量查询上架内容,dataType 为 1 时每 4 条短剧穿插 1 条广告,为 2 时每 5 条视频穿插 1 条广告,并返回广告配置
+// @Produce json
+// @Param params query proto.PlayletContentListReq true "短剧或视频列表请求"
+// @Success 200 {object} proto.PlayletContentListResp{list=[]proto.PlayletContentListItem} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/content/list [get]
+func ContentList(ctx *gin.Context) {
+ var req proto.PlayletContentListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.ContentList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// RecommendList app端推荐短剧列表
+// @Tags app短剧相关接口
+// @Summary app端推荐短剧列表 -- by 王鸣
+// @Description 分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletRecommendListReq true "推荐短剧列表请求"
+// @Success 200 {object} proto.PlayletRecommendListResp{list=[]proto.PlayletRecommendListItem} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/recommend/list [get]
+func RecommendList(ctx *gin.Context) {
+ var req proto.PlayletRecommendListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.PlayletRecommendList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// GuessList 分页查询猜你喜欢短剧
+// @Tags app短剧相关接口
+// @Summary 猜你喜欢短剧列表 -- by 王鸣
+// @Description id可选,传入时排除当前短剧,按观看量降序查询上架短剧
+// @Produce json
+// @Param params query proto.PlayletGuessListReq true "猜你喜欢短剧请求"
+// @Success 200 {object} proto.PlayletGuessListResp{list=[]proto.PlayletRecommendListItem} "成功"
+// @Router /api/app/playlet/guess/list [get]
+func GuessList(ctx *gin.Context) {
+ var req proto.PlayletGuessListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.PlayletGuessList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// TagList app端根据标签ID查询短剧列表
+// @Tags app短剧相关接口
+// @Summary app端根据标签ID查询短剧列表 -- by 王鸣
+// @Description 根据统一标签ID分页查询已上架短剧
+// @Accept json
+// @Produce json
+// @Param id path int true "标签ID"
+// @Param pageNum query int false "页码,默认1"
+// @Param pageSize query int false "每页数量,默认20,最大100"
+// @Success 200 {object} proto.PlayletRecommendListResp{list=[]proto.PlayletRecommendListItem} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/tag/{id} [get]
+func TagList(ctx *gin.Context) {
+ var req proto.ContentTagPageReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ pathID, pathSlug := slugser.ParseIDOrSlug(ctx.Param("id"))
+ if req.Id <= 0 {
+ req.Id = pathID
+ }
+ if req.Slug == "" {
+ req.Slug = pathSlug
+ }
+ resp, err := playletser.PlayletTagList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// QueryPlayletInfo 短剧详情
+// @Tags app短剧相关接口
+// @Summary 短剧详情 -- by 王鸣
+// @Description 查询短剧详情、剧集列表、点赞状态和收藏状态
+// @Accept json
+// @Produce json
+// @Param params query proto.QueryPlayletInfoReq true "短剧详情查询请求"
+// @Success 200 {object} proto.QueryPlayletInfoResp{playletInfo=proto.PlayletDetailInfo,chapterList=[]proto.PlayletChapterInfo} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/info [get]
+func QueryPlayletInfo(ctx *gin.Context) {
+ var req proto.QueryPlayletInfoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.QueryPlayletInfo(ctx, common.GetUserId(ctx), videoser.H5M3U8RequestIP(ctx), ctx.Request.UserAgent(), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// QueryPlayletChapterInfo 短剧剧集详情
+// @Tags app短剧相关接口
+// @Summary app端短剧剧集详情 -- by 王鸣
+// @Description 根据剧集ID查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态
+// @Accept json
+// @Produce json
+// @Param params query proto.QueryPlayletChapterInfoReq true "短剧剧集详情查询请求"
+// @Success 200 {object} proto.QueryPlayletChapterInfoResp{chapterInfo=proto.PlayletChapterInfo,playletInfo=proto.PlayletData,chapterList=[]proto.PlayletChapterInfo,hotPlayletList=[]proto.PlayletData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/chapter/info [get]
+func QueryPlayletChapterInfo(ctx *gin.Context) {
+ var req proto.QueryPlayletChapterInfoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.QueryPlayletChapterInfo(ctx, common.GetUserId(ctx), videoser.H5M3U8RequestIP(ctx), ctx.Request.UserAgent(), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// LikePlaylet 点赞或取消点赞短剧
+// @Tags app短剧相关接口
+// @Summary 点赞或取消点赞短剧 -- by 王鸣
+// @Description 当前登录用户点赞或取消点赞指定短剧,isLike=true表示点赞,isLike=false表示取消点赞
+// @Accept json
+// @Produce json
+// @Param param body proto.LikePlayletReq true "点赞或取消点赞请求"
+// @Success 200 {object} proto.LikePlayletResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/like [post]
+func LikePlaylet(ctx *gin.Context) {
+ var req proto.LikePlayletReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ userId := common.GetUserId(ctx)
+ if userId <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ ua, _ := common.GetUA(ctx)
+ if ua.UserAgent == "" {
+ ua.UserAgent = ctx.Request.UserAgent()
+ }
+ resp, err := playletser.LikePlaylet(ctx, userId, ctx.ClientIP(), ua, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// QueryLikePlayletList 当前用户点赞短剧分页列表
+// @Tags app短剧相关接口
+// @Summary 当前用户点赞短剧分页列表
+// @Description 按点赞时间倒序返回当前用户点赞的上架短剧
+// @Produce json
+// @Param params query proto.QueryLikePlayletListReq true "点赞短剧分页请求"
+// @Success 200 {object} proto.QueryLikePlayletListResp{list=[]proto.PlayletData} "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/like/list [get]
+func QueryLikePlayletList(ctx *gin.Context) {
+ var req proto.QueryLikePlayletListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ userID := common.GetUserId(ctx)
+ if userID <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ resp, err := playletser.QueryLikePlayletList(ctx, userID, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// FavoritePlaylet 收藏或取消收藏短剧
+// @Tags app短剧相关接口
+// @Summary 收藏或取消收藏短剧
+// @Description isFavorite=true 收藏 false 取消收藏
+// @Accept json
+// @Produce json
+// @Param param body proto.FavoritePlayletReq true "短剧收藏请求"
+// @Success 200 {object} proto.FavoritePlayletResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/favorite [post]
+func FavoritePlaylet(ctx *gin.Context) {
+ var req proto.FavoritePlayletReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ userID := common.GetUserId(ctx)
+ if userID <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ ua, _ := common.GetUA(ctx)
+ if ua.UserAgent == "" {
+ ua.UserAgent = ctx.Request.UserAgent()
+ }
+ resp, err := playletser.FavoritePlaylet(ctx, userID, ctx.ClientIP(), ua, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// QueryFavoritePlayletList 当前用户收藏短剧分页列表
+// @Tags app短剧相关接口
+// @Summary 当前用户收藏短剧分页列表
+// @Produce json
+// @Param params query proto.QueryFavoritePlayletListReq true "收藏短剧分页请求"
+// @Success 200 {object} proto.QueryFavoritePlayletListResp{list=[]proto.PlayletData} "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/favorite/list [get]
+func QueryFavoritePlayletList(ctx *gin.Context) {
+ var req proto.QueryFavoritePlayletListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ userID := common.GetUserId(ctx)
+ if userID <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ resp, err := playletser.QueryFavoritePlayletList(ctx, userID, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ReportPlayletHistory 播放成功后记录当前用户的短剧剧集
+// @Tags app短剧相关接口
+// @Summary 上报短剧观看历史
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletHistoryReportReq true "当前播放短剧和剧集"
+// @Success 200 {object} proto.PlayletHistoryReportResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/history [post]
+func ReportPlayletHistory(ctx *gin.Context) {
+ userID := common.GetUserId(ctx)
+ if userID <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ var req proto.PlayletHistoryReportReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.ReportPlayletHistory(ctx, userID, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// QueryPlayletHistoryList 当前用户短剧观看历史分页列表
+// @Tags app短剧相关接口
+// @Summary 短剧观看历史列表
+// @Produce json
+// @Param params query proto.PlayletHistoryListReq true "分页请求"
+// @Success 200 {object} proto.PlayletHistoryListResp{list=[]proto.PlayletHistoryItem} "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/history/list [get]
+func QueryPlayletHistoryList(ctx *gin.Context) {
+ userID := common.GetUserId(ctx)
+ if userID <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ var req proto.PlayletHistoryListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.QueryPlayletHistoryList(ctx, userID, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ClearPlayletHistory 清空当前用户短剧观看历史
+// @Tags app短剧相关接口
+// @Summary 清空短剧观看历史
+// @Produce json
+// @Success 200 {object} proto.PlayletHistoryClearResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/history [delete]
+func ClearPlayletHistory(ctx *gin.Context) {
+ userID := common.GetUserId(ctx)
+ if userID <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, "")
+ return
+ }
+ resp, err := playletser.ClearPlayletHistory(ctx, userID)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// QueryPlayletRankList 短剧排行榜分页列表
+// @Tags app短剧相关接口
+// @Summary 短剧排行榜分页列表 -- by 王鸣
+// @Description 分页查询短剧排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletRankReq true "短剧排行榜分页请求"
+// @Success 200 {object} proto.PlayletRankResp{list=[]proto.PlayletRankData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/rank/list [get]
+func QueryPlayletRankList(ctx *gin.Context) {
+ var req proto.PlayletRankReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := playletser.QueryPlayletRank(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/proto/actress.go b/app/api/proto/actress.go
new file mode 100644
index 0000000..2c993e7
--- /dev/null
+++ b/app/api/proto/actress.go
@@ -0,0 +1,96 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/response"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/pubdef"
+)
+
+// QueryActressWorkListReq 指定女优作品列表请求
+type QueryActressWorkListReq struct {
+ pubdef.Page
+ // 女优ID
+ Id int64 `json:"id" form:"id"`
+ // 女优slug,非空时优先按slug解析女优ID
+ Slug string `json:"slug" form:"slug"`
+ // 上架时间升序/降序(1-升序 2降序)
+ AddedTimeSort int64 `json:"addedTimeSort" form:"addedTimeSort"`
+ // 按观看量升序/降序(1-升序 2降序)
+ WatchTimesSort int64 `json:"watchTimesSort" form:"watchTimesSort"`
+ // 按收藏(点赞)量升序/降序(1-升序 2降序)
+ LikesSort int64 `json:"likesSort" form:"likesSort"`
+}
+
+// QueryActressWorkListResp 指定女优详情和作品混排列表响应
+type QueryActressWorkListResp struct {
+ // 分页信息;List 类型为 []*QueryVideoListItem,包含视频项(type=0)和广告项(type=2)
+ response.BasePageResp
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 女优信息
+ ActressInfo ActressInfoData `json:"actressInfo"`
+}
+
+// QueryActressListResp 女优榜单列表响应
+type QueryActressListResp struct {
+ // 当前路径
+ Path string `json:"path"`
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 底部导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 女优列表
+ ActressInfoList []ActressInfoData `json:"actressInfoList"`
+}
+
+type ActressInfoData struct {
+ Id int64 `json:"id" bson:"_id"` // Id
+ Alias string `bson:"alias" json:"alias"` // 别名
+ Slug string `json:"slug"` // slug
+ Avatar string `bson:"avatar" json:"avatar"` // 头像
+ Cover string `bson:"cover" json:"cover"` // 封面大图
+ Desc string `bson:"desc" json:"desc"` // 详细描述
+ Meas actress.Meas `bson:"meas" json:"meas"` // 三围
+ MeasValue string `bson:"measValue" json:"measValue"` // 三围值
+ Office string `json:"office" bson:"office"` // 事务所
+ DebutDate time.Time `bson:"debutDate" json:"debutDate" ` // 出道日期
+ BornDate time.Time `bson:"bornDate" json:"bornDate"` // 出生日期
+ Region string `json:"region" bson:"region"` // 地区/国家
+ Score string `json:"score" bson:"score"` // 评分
+ Works int64 `json:"works" bson:"works"` // 作品数
+ HotValue int64 `json:"hotValue" bson:"hotValue"` // 热度值
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Comments int64 `json:"comments" bson:"comments"` // 评论条数
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+}
+
+func ActressInfoToData(actress2 actress.Actress, imageCdn string) ActressInfoData {
+ return ActressInfoData{
+ Id: actress2.Id,
+ Alias: actress2.Alias,
+ Slug: actress2.Slug,
+ Avatar: netutil.MergeURL(imageCdn, actress2.Avatar),
+ Cover: netutil.MergeURL(imageCdn, actress2.Cover),
+ Desc: actress2.Desc,
+ Meas: actress2.Meas,
+ Office: actress2.Office,
+ DebutDate: actress2.DebutDate,
+ BornDate: actress2.BornDate,
+ Region: actress2.Region,
+ Score: actress2.Score,
+ MeasValue: actress2.Meas.Measurements(),
+ Works: actress2.Works,
+ HotValue: actress2.HotValue,
+ WatchTimes: actress2.Real.WatchTimes*100 + 2273,
+ ShowTimes: actress2.Real.WatchTimes*100 + 3273,
+ Comments: actress2.Real.Comments,
+ SeoInfo: actress2.SeoInfo,
+ }
+}
diff --git a/app/api/proto/ads.go b/app/api/proto/ads.go
new file mode 100644
index 0000000..ae27f33
--- /dev/null
+++ b/app/api/proto/ads.go
@@ -0,0 +1,38 @@
+package proto
+
+import "videoweb/pub/storage/proxy"
+
+type AdsClickReq struct {
+ AdsId string `json:"adsId,omitempty" binding:"required"`
+ Location int64 `json:"location,omitempty" binding:"required"` // 位置ID
+}
+
+type AdsClickInfo struct {
+ AppId int64 `json:"appId,omitempty" binding:"required"` // appId
+ AdsId int64 `json:"adsId,omitempty" binding:"required"`
+ Location int64 `json:"location,omitempty" binding:"required"` // 位置ID
+}
+
+type AdvitiseData struct {
+ HomeList []proxy.AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表
+ DetailTopList []proxy.AdvitiseInfo `json:"detailTopList" bson:"detailTopList"` // 详情页顶部广告
+ NineGridList []proxy.AdvitiseInfo `json:"nineGridList" bson:"nineGridList"` // 9宫格
+ DetailbannerList []proxy.AdvitiseInfo `json:"detailbannerList" bson:"detailbannerList"` // 详情banner
+ LaunchScreenList []proxy.AdvitiseInfo `json:"launchScreenList"` // 启动页广告
+ FloatingList []proxy.AdvitiseInfo `json:"floatingList" bson:"floatingList"` // 悬浮小方块
+ AList []proxy.AdvitiseInfo `json:"AList"` // 固定12广告位
+ BList []proxy.AdvitiseInfo `json:"BList"` // 固定13广告位
+ DialogList []proxy.AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告
+ PostList []proxy.AdvitiseInfo `json:"postList" bson:"postList"` // 列表穿插广告(视频、短剧、女优列表使用)
+ MustAppList []proxy.AdvitiseInfo `json:"mustAppList" bson:"mustAppList"` // 必备精品
+ TitleAppList []proxy.AdvitiseInfo `json:"titleAppList" bson:"titleAppList"` // 标题广告(APP名称)
+ FriendList []proxy.AdvitiseInfo `json:"friendList" bson:"friendList"` // 友链
+ VideoList []proxy.AdvitiseInfo `json:"videoList" bson:"videoList"` // 视频列表
+
+ //VideoInfoList []proxy.AdvitiseInfo `json:"videoInfoList" bson:"videoInfoList"` // 视频详情
+ //DownList []proxy.AdvitiseInfo `json:"downList" bson:"downList"` // 底部广告
+ //HomeBanner []proxy.AdvitiseInfo `json:"homeBanner" bson:"homeBanner"` // 首页banner小广告
+ //PlayList []proxy.AdvitiseInfo `json:"playList" bson:"playList"` // 播放强制跳转
+ //PauseyList []proxy.AdvitiseInfo `json:"pauseyList" bson:"pauseyList"` // 暂停广告
+
+}
diff --git a/app/api/proto/appinfo.go b/app/api/proto/appinfo.go
new file mode 100644
index 0000000..bb954ab
--- /dev/null
+++ b/app/api/proto/appinfo.go
@@ -0,0 +1,61 @@
+package proto
+
+// AppListResp 应用列表接口响应
+type AppListResp struct {
+ // 当前路径
+ Path string `json:"path"`
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 底部导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 公告
+ Announcement AnnouncementInfo `json:"announcement"`
+ // 应用分类列表(含各分类下的应用列表)
+ CategoryList []AppCategoryInfo `json:"categoryList"`
+ // 当前分类(含应用列表)
+ Category AppCategoryInfo `json:"category"`
+}
+
+// AppCategoryInfo 应用分类信息
+type AppCategoryInfo struct {
+ // 分类ID
+ Id int64 `json:"id"`
+ // 分类标题
+ Title string `json:"title"`
+ // 分类简介
+ Intro string `json:"intro"`
+ // 排序
+ Sort int64 `json:"sort"`
+ // 分类编码(特殊值:12=广告位A 13=广告位B)
+ Code int64 `json:"code"`
+ // 应用列表
+ AppList []AppInfoData `json:"appList"`
+}
+
+// AppInfoData 应用信息
+type AppInfoData struct {
+ // 应用ID
+ Id int64 `json:"id"`
+ // 所属分类ID
+ CategoryId int64 `json:"categoryId"`
+ // 应用名称
+ Name string `json:"name"`
+ // 简介
+ Intro string `json:"intro"`
+ // 详细描述
+ Description string `json:"description"`
+ // Logo 图片地址
+ Logo string `json:"logo"`
+ // 跳转链接(已含渠道码)
+ Url string `json:"url"`
+ // 排序
+ Sort int64 `json:"sort"`
+ // 广告ID(广告条目时返回)
+ AdvitiseId string `json:"advitiseId"`
+ // 广告位置ID
+ Location int64 `json:"location"`
+}
diff --git a/app/api/proto/comment.go b/app/api/proto/comment.go
new file mode 100644
index 0000000..c2c9fa2
--- /dev/null
+++ b/app/api/proto/comment.go
@@ -0,0 +1,64 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+type CommentListReq struct {
+ ObjectId int64 `form:"objectId" json:"objectId"` // 对象ID
+ ObjectType pubdef.CommentObjectType `form:"objectType" json:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
+ SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1=热门 2=最新 默认最新
+ pubdef.Page
+}
+
+type CommentListResp struct {
+ response.BasePageResp
+}
+
+type GetCommentListReq struct {
+ ObjectId int64 `form:"objectId" json:"objectId"` // 对象ID
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ pubdef.Page
+}
+
+type GetCommentListResp struct {
+ ObjectId int64 `json:"objectId" bson:"objectId"`
+ Total int64 `json:"total" bson:"total"` // 总数量
+ List []CommentInfo `json:"list" bson:"list"` // 数据
+}
+
+type CommentSendReq struct {
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectType pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ Text string `json:"text" bson:"text"` // 评论内容
+}
+
+type CommentInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
+ UserName string `json:"userName" bson:"userName"` // 用户名称
+ Avatar string `json:"avatar" bson:"avatar"` // 用户头像
+ Level int64 `json:"level" bson:"level"` // 评论级别
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
+ Text string `json:"text" bson:"text"` // 评论内容
+ CommentCount int64 `json:"commentCount" bson:"commentCount"` // 该评论回复数量
+ CountLike int64 `json:"countLike" bson:"countLike"` // 点赞数
+ IsLike bool `json:"isLike" bson:"-"` // 当前用户是否点赞
+ SubComment []CommentInfo `json:"subComment" bson:"subComment"` // 子评论
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
+}
+
+type CommentLikeSetReq struct {
+ CommentId int64 `json:"commentId" binding:"required"` // 评论ID
+ IsLike *bool `json:"isLike" binding:"required"` // 是否点赞
+}
+
+type CommentLikeSetResp struct {
+ CommentId int64 `json:"commentId"` // 评论ID
+ IsLike bool `json:"isLike"` // 当前点赞状态
+ CountLike int64 `json:"countLike"` // 点赞数
+}
diff --git a/app/api/proto/config.go b/app/api/proto/config.go
new file mode 100644
index 0000000..29b4d06
--- /dev/null
+++ b/app/api/proto/config.go
@@ -0,0 +1,132 @@
+package proto
+
+import (
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/pubdef"
+)
+
+type ConfigInfo struct {
+ PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
+ NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
+ NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
+ NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
+ ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
+ GmailAddr string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
+ AppAddr string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
+ WeiChart string `json:"weiChart" bson:"weiChart"` //微信群
+ QQ string `json:"qq" bson:"qq"` //qq群
+ TgGroup string `json:"tgGroup" bson:"tgGroup"` //tg群
+ PotaoGroup string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
+ GitAddr string `json:"gitAddr" bson:"gitAddr"` //git地址
+ TwitterAddr string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
+ Telegram string `json:"telegram" bson:"telegram"` //商务tg
+ TelegramAdd string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
+ TranslationAddr string `json:"translationAddr" bson:"translationAddr"` //应用导航
+ ImgCdn string `json:"imgCdn" bson:"imgCdn"` // 图片CDN
+ TsCdn string `json:"tsCdn" bson:"tsCdn"` // 视频CDN
+ ImgAppUsCdn string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片CDN(海外app使用)
+ MediaAppUsCdn string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn"` // 视频CDN(海外app使用)
+ WebTitle string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
+ WebKeyworld []string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
+ WebDescription string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
+ WebSiteName string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
+ WebAddr string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
+ DHNewUrlAddr1 string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路1
+ DHNewUrlAddr2 string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
+ DHNewUrlAddr3 string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
+ FreeVPN string `json:"freeVPN" bson:"freeVPN"` // 免费vpn链接
+ UseIngAddress []string `json:"useIngAddress" bson:"useIngAddress"` // 所有使用中的域名
+ DHTitle string `json:"dhTitle" bson:"dhTitle"` // 导航SEO标题
+ DHKeyworld []string `json:"dhKeyworld" bson:"dhKeyworld"` // 导航SEO关键字
+ DHDescription string `json:"dhDescription" bson:"dhDescription"` // 导航SEO描述
+ DHSiteName string `json:"dhSiteName" bson:"dhSiteName"` // 导航SEO网站名称
+ ReportSDKConfig string `json:"reportSdkConfig"` // 数据上报SDK域名配置
+ ChannelCode string `json:"channelCode"` // 渠道码
+ EnableH265 bool `json:"enableH265" bson:"enableH265"` // 是否允许客户端使用 H265 地址播放
+ M3U8DayRateLimitMax int64 `json:"m3u8DayRateLimitMax" bson:"m3u8DayRateLimitMax"` // 每 IP 每天最多不同正片数
+
+}
+
+// AnnouncementInfo 公告数据
+type AnnouncementInfo struct {
+ VideoInfo []VideoInfoData `json:"videoInfo" bson:"videoInfo"` // 热门信息
+ PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
+ NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` // 新国内地址
+ NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` // 备用地址1
+ NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` // 备用地址1
+ ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` // 永久海外地址(翻墙访问 永不迷路)
+ AppAddr string `json:"appAddr" bson:"appAddr"` // 吃瓜app (免费吃瓜 每日更新)
+ TranslationAddr string `json:"translationAddr" bson:"translationAddr"` // 应用导航
+}
+
+type AdvitiseInfo struct {
+ Id int64 `json:"id" bson:"_id"` // 广告id
+ Title string `json:"title" bson:"title"` // 广告标题
+ Images string `json:"images" bson:"images"` // 视频banner
+ Link string `json:"link" bson:"link"` // 链接
+}
+
+// FooterInfoResp APP底部信息响应
+type FooterInfoResp struct {
+ FooterInfo FooterInfo `json:"footerInfo"` // 底部信息
+}
+
+// FooterInfo APP底部信息
+type FooterInfo struct {
+ Brand FooterBrand `json:"brand"` // 品牌信息
+ NavList []TranslationCategory `json:"navList"` // 底部导航
+ ContactList []FooterContact `json:"contactList"` // 联系方式
+}
+
+// FooterBrand APP底部品牌信息
+type FooterBrand struct {
+ Title string `json:"title"` // 品牌标题
+ Content string `json:"content"` // 品牌标语
+}
+
+// FooterContact APP底部联系方式
+type FooterContact struct {
+ Name string `json:"name"` // 联系方式名称
+ Value string `json:"value"` // 联系方式值
+}
+
+// AppContentTagListReq app端统一标签列表请求
+type AppContentTagListReq struct {
+ ContentTagType *contenttagmod.ContentTagType `form:"contentTagType" json:"contentTagType"` // 内容类型 1-视频 5-短剧,不传返回全部有绑定数量的标签
+}
+
+// ContentTagPageReq 根据统一标签分页查询内容请求
+type ContentTagPageReq struct {
+ pubdef.Page
+ Id int64 `json:"id" form:"id"` // 标签ID
+ Slug string `json:"slug" form:"slug"` // 标签slug,非空时优先按slug解析标签ID
+}
+
+// AppContentTagListResp app端统一标签列表响应
+type AppContentTagListResp struct {
+ List []*AppContentTagItem `json:"list"` // 统一标签列表
+}
+
+// AppContentTagItem app端统一标签列表项
+type AppContentTagItem struct {
+ Id int64 `json:"id" bson:"_id"` // 标签ID
+ Name string `json:"name" bson:"name"` // 标签名称
+ Slug string `json:"slug" bson:"slug"` // slug
+ Types []contenttagmod.ContentTagType `json:"types" bson:"types"` // 支持类型 1-视频 5-短剧
+ BindCount int64 `json:"bindCount" bson:"bindCount"` // 当前查询类型绑定数量,不传type时为各类型绑定数量合计
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 绑定视频数量
+ PlayletCount int64 `json:"playletCount" bson:"playletCount"` // 绑定短剧数量
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Rank int64 `json:"rank,omitempty" bson:"rank,omitempty"` // 排序
+}
+
+// type AdvitiseData struct {
+// DialogList []AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告
+// HomeList []AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表
+// VideoTopList []AdvitiseInfo `json:"videoTopList" bson:"videoTopList"` // 帖子详情(上)
+// VideoDownList []AdvitiseInfo `json:"videoDownList" bson:"videoDownList"` // 帖子详情(下)
+// HotAppList []AdvitiseInfo `json:"hotAppList" bson:"hotAppList"` // 热门应用
+// NewAppList []AdvitiseInfo `json:"newAppList" bson:"newAppList"` // 最新上架
+// MustAppList []AdvitiseInfo `json:"mustAppList" bson:"mustAppList"` // 必备精品
+// TitleAppList []AdvitiseInfo `json:"titleAppList" bson:"titleAppList"` // 标题广告(APP名称)
+// }
diff --git a/app/api/proto/contribute.go b/app/api/proto/contribute.go
new file mode 100644
index 0000000..96613bb
--- /dev/null
+++ b/app/api/proto/contribute.go
@@ -0,0 +1,6 @@
+package proto
+
+type ContributeData struct {
+ Path string `json:"path"`
+ RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
+}
diff --git a/app/api/proto/dh.go b/app/api/proto/dh.go
new file mode 100644
index 0000000..b314aed
--- /dev/null
+++ b/app/api/proto/dh.go
@@ -0,0 +1,50 @@
+package proto
+
+// HotNavItem 热门导航
+type HotNavItem struct {
+ Id int64 `json:"id"` // ID
+ Title string `json:"title"` // 标题
+ Intro string `json:"intro"` // 介绍
+ Url string `json:"url"` // 链接
+ Icon string `json:"icon"` // 图标
+}
+
+// DHHtmlData 导航数据
+type DHHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ HotNavs []HotNavItem `json:"hotNavs"` // 热门导航
+}
+
+// DHConsultingHtmlData 招募
+type DHConsultingHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+}
+
+// DHConsultingInfoHtmlData 招募
+type DHConsultingInfoHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+}
diff --git a/app/api/proto/findVideo.go b/app/api/proto/findVideo.go
new file mode 100644
index 0000000..b2f9fdc
--- /dev/null
+++ b/app/api/proto/findVideo.go
@@ -0,0 +1,5 @@
+package proto
+
+type FindVideoData struct {
+ RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
+}
\ No newline at end of file
diff --git a/app/api/proto/health.go b/app/api/proto/health.go
new file mode 100644
index 0000000..b5b5a3a
--- /dev/null
+++ b/app/api/proto/health.go
@@ -0,0 +1,37 @@
+package proto
+
+type HealthCheckRequest struct {
+ GroupID string `json:"groupId" binding:"required"`
+ Timestamp int64 `json:"timestamp" binding:"required"`
+ Sign string `json:"sign" binding:"required"`
+}
+
+type ComponentStatus struct {
+ Status string `json:"status"`
+ Msg string `json:"msg,omitempty"`
+ Master string `json:"master,omitempty"`
+}
+
+type HealthCPUMetrics struct {
+ Cores int `json:"cores"`
+ Used float64 `json:"used"`
+}
+
+type HealthMemoryMetrics struct {
+ Total int64 `json:"total"`
+ Used int64 `json:"used"`
+}
+
+type HealthCheckData struct {
+ ServerName string `json:"serverName,omitempty"`
+ ServerIP string `json:"serverIp,omitempty"`
+ CPU HealthCPUMetrics `json:"cpu"`
+ Memory HealthMemoryMetrics `json:"memory"`
+ Disk HealthMemoryMetrics `json:"disk"`
+ Mysql *ComponentStatus `json:"mysql,omitempty"`
+ Mongodb *ComponentStatus `json:"mongodb,omitempty"`
+ Redis *ComponentStatus `json:"redis,omitempty"`
+ Elasticsearch *ComponentStatus `json:"elasticsearch,omitempty"`
+ Kafka *ComponentStatus `json:"kafka,omitempty"`
+ Timestamp int64 `json:"timestamp"`
+}
diff --git a/app/api/proto/home.go b/app/api/proto/home.go
new file mode 100644
index 0000000..64095a7
--- /dev/null
+++ b/app/api/proto/home.go
@@ -0,0 +1,171 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// DataResp 首页数据响应
+type DataResp struct {
+ // 当前路径
+ Path string `json:"path"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 公告
+ Announcement AnnouncementInfo `json:"announcement"`
+ // 首页banner信息列表
+ HomeBannerList []HomeBannerInfo `json:"homeBannerList"`
+ // 首页短剧数据
+ PlayletData HomePlayletDataResp `json:"playletData"`
+ // 首页视频数据
+ VideoData HomeVideoDataResp `json:"videoData"`
+ // 女优信息
+ ActressInfoList []ActressInfo `json:"actressInfoList"`
+}
+
+// HomeBannerInfo 首页banner信息
+type HomeBannerInfo struct {
+ Id int64 `json:"id"` // ID
+ Name string `json:"name"` // 名称
+ Image string `json:"image"` // 图片,已拼接图片域名
+ VerticalImage string `json:"verticalImage"` // 竖版图片,已拼接图片域名
+ Url string `json:"url"` // URL
+ Rank int64 `json:"rank"` // 排序
+ Status bool `json:"status"` // 状态 true-启用 false-停用
+}
+
+// HomePlayletDataResp 首页短剧数据响应
+type HomePlayletDataResp struct {
+ RecommendList []PlayletData `json:"recommendList"` // 推荐短剧列表
+ NewestList []PlayletData `json:"newestList"` // 最新短剧列表
+ HotList []PlayletData `json:"hotList"` // 最热短剧列表
+}
+
+// HomeVideoDataResp 首页视频数据响应
+type HomeVideoDataResp struct {
+ RecommendList []VideoInfo `json:"recommendList"` // 推荐视频列表
+ NewestList []VideoInfo `json:"newestList"` // 最新视频列表
+ HotList []VideoInfo `json:"hotList"` // 最热视频列表
+}
+
+// HomeBaseResp 首页公共数据响应
+type HomeBaseResp struct {
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 公告
+ Announcement AnnouncementInfo `json:"announcement"`
+}
+
+// HomeTagsResp 首页标签数据响应
+type HomeTagsResp struct {
+ HomeBaseResp
+ // 视频标签
+ VideoTagsList []TagsInfoData `json:"videoTagsList"`
+ // 短剧标签
+ PlayletTagsList []TagsInfoData `json:"playletTagsList"`
+}
+
+// VideoCategoryDataInfo 视频分类数据
+type VideoCategoryDataInfo struct {
+ CommonData
+ // Seo相关信息
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+ // 描述
+ Desc string `json:"desc"`
+ // 分类关联的视频标签
+ Tags []int64 `json:"tags"`
+ // 视频信息集合
+ VideoInfoList []VideoInfo `json:"videoInfoList"`
+}
+
+// CommonData 分类通用数据
+type CommonData struct {
+ // Id
+ Id int64 `json:"id"`
+ // 分类名称
+ Name string `json:"name"`
+ // slug
+ Slug string `json:"slug"`
+ // APP显示
+ Title string `json:"title"`
+ // 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ DisplayPosition pubdef.CategoryDisplayPosition `json:"displayPosition"`
+ // 排序方式 0:上线时间降序 1:观看次数降序 2:点赞(收藏)次数降序 3:上线时间升序 4:观看次数升序 5:点赞(收藏)次数升序
+ SortType pubdef.CategorySortType `json:"sortType"`
+ // 展示方式
+ DisplayMode pubdef.DisplayMode `json:"displayMode"`
+ // URL地址
+ UrlValue string `json:"urlValue"`
+}
+
+// VideoInfo 视频信息
+type VideoInfo struct {
+ ContentData
+}
+
+// ContentData 内容数据信息
+type ContentData struct {
+ // id
+ Id int64 `json:"id"`
+ // 标题
+ Title string `json:"title"`
+ // slug
+ Slug string `json:"slug"`
+ // 上架时间
+ AddedTime time.Time `json:"addedTime"`
+ // 封面
+ CoverImg string `json:"coverImg"`
+ // 标签id
+ Tags []int64 `json:"tags"`
+ // 标签名称
+ TagsName []string `json:"tagsName"`
+ // 标签slug 与tagsName顺序一致
+ TagSlugs []string `json:"tagSlugs"`
+ // 观看展示数
+ WatchNum int64 `json:"watchNum"`
+ // 点赞展示数
+ LikeNum int64 `json:"likeNum"`
+ // 收藏展示数
+ CountCollect int64 `json:"countCollect,omitempty"`
+ // Seo相关信息
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+}
+
+// RankResp 排行榜通用响应
+type RankResp struct {
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ RankData HomeRankData `json:"rankData"`
+}
+
+// HomeRankData 首页排行榜数据(总榜/周榜/月榜/年榜)
+type HomeRankData struct {
+ Total []ContentData `json:"total"` // 总榜
+ Week []ContentData `json:"week"` // 周榜
+ Month []ContentData `json:"month"` // 月榜
+ Year []ContentData `json:"year"` // 年榜
+}
+
+// ActressInfo 女优信息
+type ActressInfo struct {
+ // Id
+ Id int64 `json:"id"`
+ // 别名
+ Alias string `json:"alias"`
+ // slug
+ Slug string `json:"slug"`
+ // 头像
+ Avatar string `json:"avatar"`
+ // 作品数
+ Works int64 `json:"works"`
+ // 热度值
+ HotValue int64 `json:"hotValue"`
+}
diff --git a/app/api/proto/lawandtrust.go b/app/api/proto/lawandtrust.go
new file mode 100644
index 0000000..af707b2
--- /dev/null
+++ b/app/api/proto/lawandtrust.go
@@ -0,0 +1,13 @@
+package proto
+
+// LawAndTrustHtmlData
+type LawAndTrustHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+}
diff --git a/app/api/proto/playlet.go b/app/api/proto/playlet.go
new file mode 100644
index 0000000..5a3ec97
--- /dev/null
+++ b/app/api/proto/playlet.go
@@ -0,0 +1,380 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+)
+
+// PlayletRecommendListReq 推荐短剧分页请求
+type PlayletRecommendListReq struct {
+ pubdef.Page
+ TagId int64 `form:"tagId" json:"tagId"` // 标签ID,不传查询全部标签
+ TagSlug string `form:"tagSlug" json:"tagSlug"` // 标签slug,非空时优先按slug解析标签ID
+ CategoryId int64 `form:"categoryId" json:"categoryId"` // 短剧分类ID,不传查询全部分类
+ CategorySlug string `form:"categorySlug" json:"categorySlug"` // 分类slug,非空时优先按slug解析分类ID
+ SortList int64 `form:"sortList" json:"sortList"` // 排序方式 1-推荐 2-最新 3-最热
+}
+
+// PlayletRecommendSort 推荐短剧排序方式
+type PlayletRecommendSort int64
+
+const (
+ PlayletRecommendSortRecommend PlayletRecommendSort = 1 // 推荐 后台配置 isRecommend=true
+ PlayletRecommendSortNewest PlayletRecommendSort = 2 // 最新 按短剧最后更新时间从近到远排序
+ PlayletRecommendSortHot PlayletRecommendSort = 3 // 最热 按短剧累计点击量从高到低排序
+)
+
+// PlayletRecommendItemType 推荐短剧列表项类型
+type PlayletRecommendItemType int64
+
+const (
+ PlayletRecommendItemTypePlaylet PlayletRecommendItemType = 0 // 短剧数据
+ PlayletRecommendItemTypeAdvertise PlayletRecommendItemType = 2 // 广告数据
+)
+
+// PlayletRecommendListResp 推荐短剧响应
+type PlayletRecommendListResp struct {
+ response.BasePageResp
+}
+
+// PlayletThemeListResp 已启用短剧题材列表响应
+type PlayletThemeListResp struct {
+ List []PlayletThemeItem `json:"list"` // 全部已启用题材
+}
+
+// PlayletThemeItem 短剧题材列表项
+type PlayletThemeItem struct {
+ Id int64 `json:"id"` // 题材ID
+ Name string `json:"name"` // 题材名称
+ Slug string `json:"slug"` // 题材slug
+ Icon string `json:"icon"` // 题材图标完整地址
+ Rank int64 `json:"rank"` // 排序值,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // SEO信息
+}
+
+// PlayletGuessListReq 猜你喜欢短剧分页请求
+type PlayletGuessListReq struct {
+ pubdef.Page
+ Id int64 `form:"id" json:"id"` // 当前短剧ID,可选,传入时排除该短剧
+}
+
+// PlayletGuessListResp 猜你喜欢短剧分页响应
+type PlayletGuessListResp struct {
+ response.BasePageResp
+}
+
+// PlayletListReq 短剧列表分页请求
+type PlayletListReq struct {
+ pubdef.Page
+ ThemeId int64 `form:"themeId" json:"themeId"` // 题材 ID,不传查询全部题材
+ ThemeSlug string `form:"themeSlug" json:"themeSlug"` // 题材 slug,非空时优先于 themeId
+ SortType int64 `form:"sortType" json:"sortType"` // 排序方式,1 热度优先,2 最近更新,默认 1
+}
+
+// PlayletListResp 短剧与广告混排分页响应
+type PlayletListResp struct {
+ response.BasePageResp
+ Advitise AdvitiseData `json:"advitise"` // 完整广告配置
+}
+
+// PlayletContentListReq 短剧或视频分页请求
+type PlayletContentListReq struct {
+ pubdef.Page
+ DataType int64 `form:"dataType" json:"dataType"` // 数据类型,1 短剧,2 视频,必填
+}
+
+// PlayletContentListResp 短剧或视频与广告混排分页响应
+type PlayletContentListResp struct {
+ response.BasePageResp
+ Advitise AdvitiseData `json:"advitise"` // 完整广告配置,postList 为列表广告
+}
+
+// PlayletContentListItem 短剧、视频或广告列表项
+type PlayletContentListItem struct {
+ Type int64 `json:"type"` // 数据类型,0 内容,2 广告
+ Playlet *PlayletData `json:"playlet,omitempty"` // 短剧数据,dataType 为 1 且 type 为 0 时返回
+ Video *VideoInfo `json:"video,omitempty"` // 视频数据,dataType 为 2 且 type 为 0 时返回
+ Advertise *proxy.AdvitiseInfo `json:"advertise,omitempty"` // 广告数据,type 为 2 时返回
+}
+
+// PlayletRecommendListItem 推荐短剧列表项,type=0时返回短剧信息,type=2时返回广告信息
+type PlayletRecommendListItem struct {
+ Type PlayletRecommendItemType `json:"type"` // 数据类型 0-短剧数据 2-广告
+ AdvertiseId string `json:"advertiseId"` // 广告ID,type=2时返回
+ Link string `json:"link"` // 广告跳转链接,type=2时返回
+ Location int64 `json:"location"` // 广告位置ID,type=2时返回
+ PlayletData
+}
+
+// LikePlayletReq 点赞或取消点赞短剧请求
+type LikePlayletReq struct {
+ // ID
+ Id int64 `json:"id" binding:"required"`
+ // 是否点赞,true-点赞、false-取消点赞
+ IsLike *bool `json:"isLike" binding:"required"`
+}
+
+// LikePlayletResp 点赞或取消点赞短剧响应
+type LikePlayletResp struct {
+ // ID
+ Id int64 `json:"id"`
+ // 是否点赞,true-点赞、false-取消点赞
+ IsLike bool `json:"isLike"`
+ // 点赞展示数
+ LikeNum int64 `json:"likeNum"`
+}
+
+// QueryLikePlayletListReq 当前用户点赞短剧分页请求
+type QueryLikePlayletListReq struct {
+ pubdef.Page
+}
+
+// QueryLikePlayletListResp 当前用户点赞短剧分页响应
+type QueryLikePlayletListResp struct {
+ response.BasePageResp
+ Translations AppTranslationInfo `json:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
+}
+
+// PlayletHistoryListReq 当前用户短剧观看历史分页请求
+type PlayletHistoryListReq struct {
+ pubdef.Page
+}
+
+// PlayletHistoryReportReq 播放成功后上报短剧观看历史
+type PlayletHistoryReportReq struct {
+ PlayletID int64 `json:"playletId" binding:"required,gt=0"` // 短剧 ID
+ ChapterID int64 `json:"chapterId" binding:"required,gt=0"` // 当前播放剧集 ID
+}
+
+// PlayletHistoryReportResp 短剧观看历史上报响应
+type PlayletHistoryReportResp struct {
+ WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
+}
+
+// PlayletHistoryListResp 当前用户短剧观看历史分页响应
+type PlayletHistoryListResp struct {
+ response.BasePageResp
+ Translations AppTranslationInfo `json:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
+}
+
+// PlayletHistoryItem 短剧观看历史卡片
+type PlayletHistoryItem struct {
+ PlayletData
+ ChapterID int64 `json:"chapterId"` // 最近观看剧集 ID
+ Episode int64 `json:"episode"` // 最近观看集数
+ WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
+}
+
+// PlayletHistoryClearResp 清空短剧观看历史响应
+type PlayletHistoryClearResp struct {
+ DeletedCount int64 `json:"deletedCount"` // 删除记录数
+}
+
+// FavoritePlayletReq 收藏或取消收藏短剧请求
+type FavoritePlayletReq struct {
+ Id int64 `json:"id" binding:"required"` // 短剧ID
+ IsFavorite *bool `json:"isFavorite" binding:"required"` // true 收藏 false 取消收藏
+}
+
+// FavoritePlayletResp 收藏或取消收藏短剧响应
+type FavoritePlayletResp struct {
+ Id int64 `json:"id"` // 短剧ID
+ IsFavorite bool `json:"isFavorite"` // 当前收藏状态
+ CountCollect int64 `json:"countCollect"` // 收藏展示数
+}
+
+// QueryFavoritePlayletListReq 当前用户收藏短剧分页请求
+type QueryFavoritePlayletListReq struct {
+ pubdef.Page
+}
+
+// QueryFavoritePlayletListResp 当前用户收藏短剧分页响应
+type QueryFavoritePlayletListResp struct {
+ response.BasePageResp
+ Translations AppTranslationInfo `json:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
+}
+
+// PlayletRankReq 短剧排行榜分页请求
+type PlayletRankReq struct {
+ pubdef.Page
+ // 榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜
+ RankPeriod int64 `json:"rankPeriod" form:"rankPeriod"`
+}
+
+// PlayletRankPeriod 短剧排行榜类型
+type PlayletRankPeriod int64
+
+const (
+ PlayletRankPeriodLike PlayletRankPeriod = 0 // 点赞榜,按累计点赞量降序,最多50条,不足随机补位
+ PlayletRankPeriodWeek PlayletRankPeriod = 1 // 周榜,按本周观看量降序,最多50条,不足随机补位
+ PlayletRankPeriodMonth PlayletRankPeriod = 2 // 月榜,按本月观看量降序,最多50条,不足随机补位
+ PlayletRankPeriodYear PlayletRankPeriod = 3 // 年榜,按本年观看量降序,最多50条,不足随机补位
+)
+
+// PlayletRankResp 短剧排行榜分页响应
+type PlayletRankResp struct {
+ response.BasePageResp
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+}
+
+// PlayletRankData 短剧排行榜数据
+type PlayletRankData struct {
+ PlayletData
+ // 榜单分数,点赞榜为累计展示点赞数,周/月/年榜为对应周期观看数
+ RankScore int64 `json:"rankScore"`
+}
+
+// QueryPlayletInfoReq 短剧详情请求
+type QueryPlayletInfoReq struct {
+ // 短剧ID
+ Id int64 `json:"id" form:"id"`
+ // 短剧slug,非空时优先按slug解析短剧ID
+ Slug string `json:"slug" form:"slug"`
+}
+
+// QueryPlayletInfoResp 短剧详情响应
+type QueryPlayletInfoResp struct {
+ FreeEpisodeCount int64 `json:"freeEpisodeCount"` // 官网开放集数 1至1000表示开放第1集到第N集 -1表示开放全集 未配置按2集返回
+ // 当前路径
+ Path string `json:"path"`
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 公告
+ Announcement AnnouncementInfo `json:"announcement"`
+ // 短剧详情数据
+ PlayletInfo PlayletDetailInfo `json:"playletInfo"`
+ // 剧集列表
+ ChapterList []PlayletChapterInfo `json:"chapterList"`
+ // 当前登录用户是否已点赞该短剧
+ IsLike bool `json:"isLike"`
+ // 当前登录用户是否已收藏该短剧
+ IsFavorite bool `json:"isFavorite"`
+}
+
+// QueryPlayletChapterInfoReq 短剧剧集详情请求
+type QueryPlayletChapterInfoReq struct {
+ // 剧集ID
+ Id int64 `json:"id" form:"id" binding:"required"`
+}
+
+// QueryPlayletChapterInfoResp 短剧剧集详情响应
+type QueryPlayletChapterInfoResp struct {
+ FreeEpisodeCount int64 `json:"freeEpisodeCount"` // 官网开放集数 1至1000表示开放第1集到第N集 -1表示开放全集 未配置按2集返回
+ // 当前路径
+ Path string `json:"path"`
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 公告
+ Announcement AnnouncementInfo `json:"announcement"`
+ // 当前剧集详情
+ ChapterInfo PlayletChapterInfo `json:"chapterInfo"`
+ // 所属短剧详情
+ PlayletInfo PlayletData `json:"playletInfo"`
+ // 剧集列表
+ ChapterList []PlayletChapterInfo `json:"chapterList"`
+ // 热门短剧列表
+ HotPlayletList []PlayletData `json:"hotPlayletList"`
+ // 当前登录用户是否已点赞该短剧
+ IsLike bool `json:"isLike"`
+ // 当前登录用户是否已收藏该短剧
+ IsFavorite bool `json:"isFavorite"`
+}
+
+// PlayletDetailInfo 短剧详情数据
+type PlayletDetailInfo struct {
+ PlayletData
+ // 评论数量
+ CountComment int64 `json:"countComment"`
+ // 当前登录用户是否已点赞该短剧
+ IsLike bool `json:"isLike"`
+ // 当前登录用户是否已收藏该短剧
+ IsFavorite bool `json:"isFavorite"`
+}
+
+// PlayletChapterInfo 短剧剧集数据
+type PlayletChapterInfo struct {
+ IsOfficialOpen *bool `json:"isOfficialOpen,omitempty"` // 本集是否在官网开放
+ Id int64 `json:"id"` // 剧集ID
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ MediaContentId uint `json:"mediaContentId"` // 媒资中心章节ID
+ CurrentEpisode int64 `json:"currentEpisode"` // 第几集
+ Title string `json:"title"` // 章节名
+ Md5 string `json:"md5"` // 媒体md5
+ Hash string `json:"hash"` // 媒体hash
+ Height int64 `json:"height"` // 高
+ Width int64 `json:"width"` // 宽
+ Ratio float64 `json:"ratio"` // 宽高比
+ VideoUrl string `json:"videoUrl"` // 视频播放地址,优先返回水印地址
+ MediaSize int64 `json:"mediaSize"` // 资源大小
+ MediaTime int64 `json:"mediaTime"` // 影片长度(秒)
+ CountBrowse int64 `json:"countBrowse"` // 浏览数
+ CountCollect int64 `json:"countCollect"` // 收藏数
+ CountLike int64 `json:"countLike"` // 喜欢数
+ CountComment int64 `json:"countComment"` // 评论数
+ PayType playletchaptermod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币
+ WatermarkStatus playletchaptermod.WatermarkStatus `json:"watermarkStatus"` // 水印状态 0-未处理 1-已登记 2-处理中 3-已完成
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 修改时间
+}
+
+// PlayletData 短剧数据
+type PlayletData struct {
+ Id int64 `json:"id"` // 短剧id
+ Status playletmod.Status `json:"status"` // 状态 0-下架 1-上架
+ IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
+ IsNew bool `json:"isNew"` // 是否上新 本周内上架为true
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧 后台设置
+ IsOriginal bool `json:"isOriginal"` // 是否原创 后台新增或无媒资中心ID
+ ShowOriginal bool `json:"showOriginal"` // 页面是否展示原创标识
+ OnlineAt time.Time `json:"onlineAt"` // 上架时间
+ MediaCreatedAt time.Time `json:"mediaCreatedAt"` // 媒资中心创建时间
+ MediaUpdatedAt time.Time `json:"mediaUpdatedAt"` // 媒资中心更新时间
+ Title string `json:"title"` // 标题
+ Slug string `json:"slug"` // slug
+ Desc string `json:"desc"` // 简介
+ Anchor string `json:"anchor"` // 作者或演员
+ Cover string `json:"cover"` // 封面,已拼接图片域名
+ Preview string `json:"preview"` // 预告,已拼接图片域名
+ TagIds []int64 `json:"tagIds"` // 标签ID集合
+ TagNames []string `json:"tagNames"` // 标签名称集合
+ TagSlugs []string `json:"tagSlugs"` // 标签slug集合 与tagNames顺序一致
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合
+ ThemeNames []string `json:"themeNames"` // 题材名称集合
+ ContentType playletmod.ContentType `json:"contentType"` // 内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画
+ PayType playletmod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币,支付方式为金币时生效
+ TotalEpisode int64 `json:"totalEpisode"` // 总集数
+ UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
+ CountBrowse int64 `json:"countBrowse"` // 观看展示数
+ HeatValue int64 `json:"heatValue"` // 热度值
+ CountCollect int64 `json:"countCollect"` // 收藏展示数
+ CountLike int64 `json:"countLike"` // 点赞展示数
+ VirtualCountLike int64 `json:"virtualCountLike"` // 后台虚拟点赞数
+ DisplayCountLike int64 `json:"displayCountLike"` // 点赞展示数
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 修改时间
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
diff --git a/app/api/proto/playlet_home.go b/app/api/proto/playlet_home.go
new file mode 100644
index 0000000..d115c50
--- /dev/null
+++ b/app/api/proto/playlet_home.go
@@ -0,0 +1,116 @@
+package proto
+
+import "videoweb/pub/pubdef"
+
+// HomePlayletResponse 短剧首页数据。
+type HomePlayletResponse struct {
+ HomeBannerData []*HomeBannerData `json:"homeBannerData"`
+ Modules []*HomeModuleData `json:"modules"`
+ Announcements []*AnnouncementItem `json:"announcements"`
+ Advitise AdvitiseData `json:"advitise"`
+ SeoInfo *pubdef.SeoValue `json:"seoInfo,omitempty"`
+}
+
+type HomeBannerData struct {
+ PlayletData
+ ShowOnBanner bool `json:"showOnBanner"`
+ Banner string `json:"banner"`
+}
+
+// HomeModuleData.List 根据 dataType 返回短剧或视频列表。
+type HomeModuleData struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"` // 页面路径标识
+ Title string `json:"title"`
+ Desc string `json:"desc"`
+ Label string `json:"label"`
+ Style string `json:"style"`
+ CardBackgroundImage string `json:"cardBackgroundImage"`
+ ModuleType int64 `json:"moduleType"`
+ DataType int64 `json:"dataType"`
+ RecommendType int64 `json:"recommendType"`
+ CategoryId int64 `json:"categoryId"`
+ Rank int64 `json:"rank"`
+ List any `json:"list"`
+ TopicList []*HomeTopicData `json:"topicList"`
+}
+
+type HomeTopicData struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
+ Icon string `json:"icon"`
+ Rank int64 `json:"rank"`
+ PlayletCount int64 `json:"playletCount"`
+ VideoCount int64 `json:"videoCount,omitempty"`
+ List any `json:"list"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+}
+
+type HomeVideoData struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Slug string `json:"slug"`
+ Cover string `json:"cover"`
+ Desc string `json:"desc"`
+ Tags []int64 `json:"tags"`
+ WatchNum int64 `json:"watchNum"`
+ LikeNum int64 `json:"likeNum"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+}
+
+type AnnouncementItem struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+}
+
+// PlayletHomeConfigListReq 首页配置内容分页请求
+type PlayletHomeConfigListReq struct {
+ // pageNum 默认 1,pageSize 默认 20、最大 100
+ pubdef.Page
+ Id int64 `form:"id" json:"id"` // 首页配置 ID,与 slug 二选一
+ Slug string `form:"slug" json:"slug"` // 页面路径标识,和 ID 同时传时优先使用
+}
+
+// PlayletHomeConfigListResp 首页配置内容分页响应
+type PlayletHomeConfigListResp struct {
+ Config *HomeModuleData `json:"config"` // 当前首页配置,不包含首页预览内容
+ TotalCount int64 `json:"totalCount"` // 符合配置条件的内容或专题总数
+ PageSize int `json:"pageSize"` // 每页条数
+ PageNum int `json:"pageNum"` // 当前页码
+ List any `json:"list"` // 普通模块内容,dataType 为 1 时是短剧,为 2 时是视频
+ TopicList []*HomeTopicData `json:"topicList"` // 专题模块内容,短剧按题材分组,视频按分类分组
+ Advitise AdvitiseData `json:"advitise"` // 广告配置
+}
+
+// PlayletTopicDetailReq 短剧题材或视频分类专题详情请求
+type PlayletTopicDetailReq struct {
+ // pageNum 默认 1,pageSize 默认 12、最大 100
+ pubdef.Page
+ Id int64 `form:"id" json:"id"` // 专题 ID,dataType 为 1 时是题材 ID,为 2 时是视频分类 ID
+ TopicSlug string `form:"topicSlug" json:"topicSlug"` // 专题 slug,非空时优先于 id
+ DataType int64 `form:"dataType" json:"dataType"` // 数据类型,1 为短剧题材,2 为视频分类,默认 1
+ SortType int64 `form:"sortType" json:"sortType"` // 排序方式,1 编辑排序,2 最新,3 最热,默认 1
+}
+
+// PlayletTopicDetailResp 短剧题材或视频分类专题详情响应
+type PlayletTopicDetailResp struct {
+ Id int64 `json:"id"` // 专题 ID
+ DataType int64 `json:"dataType"` // 数据类型,1 为短剧题材,2 为视频分类
+ Name string `json:"name"` // 专题名称
+ Slug string `json:"slug"` // 专题 slug
+ Desc string `json:"desc"` // 专题描述
+ Icon string `json:"icon"` // 题材图标地址,视频分类专题为空
+ Rank int64 `json:"rank"` // 专题排序值
+ PlayletCount int64 `json:"playletCount"` // 已上架短剧数量,视频专题为 0
+ VideoCount int64 `json:"videoCount,omitempty"` // 审核通过的视频数量,为 0 时省略
+ Covers []string `json:"covers"` // 顶部封面,最多 3 张
+ TotalCount int64 `json:"totalCount"` // 专题内符合条件的内容总数
+ PageSize int `json:"pageSize"` // 每页条数
+ PageNum int `json:"pageNum"` // 当前页码
+ List any `json:"list"` // 当前页内容,dataType 为 1 时是短剧,为 2 时是视频
+ MoreTopics []*HomeTopicData `json:"moreTopics"` // 同数据类型的其他专题,最多 3 个
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // 专题 SEO 信息
+ Advitise AdvitiseData `json:"advitise"` // 广告配置
+}
diff --git a/app/api/proto/questionnaire.go b/app/api/proto/questionnaire.go
new file mode 100644
index 0000000..87c88bc
--- /dev/null
+++ b/app/api/proto/questionnaire.go
@@ -0,0 +1,19 @@
+package proto
+
+import "videoweb/pub/models/questionnairemod"
+
+// QuestionnaireSubmitReq App端提交问卷请求
+type QuestionnaireSubmitReq struct {
+ UserId int64 `json:"userId"` // 登录用户ID,0表示匿名(选填)
+ QuestionType string `json:"questionType" binding:"required"` // 问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题
+ Frequency string `json:"frequency" binding:"required"` // 出现频率:每次都会、经常出现、偶尔出现、仅现一次
+ Province string `json:"province" binding:"required"` // 所在省份(必填)
+ City string `json:"city"` // 所在城市(选填)
+ Operator string `json:"operator" binding:"required"` // 运营商:电信、联通、移动、广电
+ NetworkType string `json:"networkType" binding:"required"` // 网络类型:WI-FI、5G、4G、其他
+ DeviceType string `json:"deviceType" binding:"required"` // 设备类型:iPhone、Android、iPad、其他
+ DeviceModel string `json:"deviceModel"` // 设备型号(选填)
+ Description string `json:"description" binding:"required"` // 问题描述(必填)
+ Contact string `json:"contact"` // 联系方式(选填)
+ Attachments []questionnairemod.Attachment `json:"attachments"` // 附件列表,最多3个(选填)
+}
diff --git a/app/api/proto/richtext.go b/app/api/proto/richtext.go
new file mode 100644
index 0000000..bdb8050
--- /dev/null
+++ b/app/api/proto/richtext.go
@@ -0,0 +1,24 @@
+package proto
+
+import "videoweb/pub/pubdef"
+
+type RichTextInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 标题
+ Content string `json:"content" bson:"content"` // 内容
+ Desc string `json:"desc" bson:"desc"` // 标题(app)
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+}
+
+// AppRichTextItemResp APP富文本内容列表项
+type AppRichTextItemResp struct {
+ Id int64 `json:"id"` // 富文本ID
+ Title string `json:"title"` // 标题
+ Content string `json:"content"` // 内容
+ Desc string `json:"desc"` // 标题(app)
+}
+
+// AppRichTextListResp APP富文本内容列表响应
+type AppRichTextListResp struct {
+ List []AppRichTextItemResp `json:"list"` // 富文本内容列表
+}
diff --git a/app/api/proto/search.go b/app/api/proto/search.go
new file mode 100644
index 0000000..18cb50f
--- /dev/null
+++ b/app/api/proto/search.go
@@ -0,0 +1,82 @@
+package proto
+
+type SearchSortType string
+
+const (
+ SearchSortTypeDefault SearchSortType = "default" // 默认 相似度
+ SearchSortTypeHot SearchSortType = "hot" // 热度排序
+ SearchSortTypeNew SearchSortType = "new" // 最新排序
+)
+
+// RankType 排行榜类型
+type RankType int64
+
+const (
+ RankType_Video = 1 // 视频
+ RankType_Actress = 2 // 女优
+ RankType_Filmreview = 3 // 影评
+)
+
+// RankPeriod 排行榜周期
+type RankPeriod int64
+
+const (
+ RankPeriod_Week RankPeriod = 1
+ RankPeriod_Month RankPeriod = 2
+ RankPeriod_Year RankPeriod = 3
+)
+
+type SearchRankHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ VideoWeek SearchRankData `json:"videoWeek"`
+ ActressWeek SearchRankData `json:"actressWeek"`
+ FilmreviewWeek SearchRankData `json:"filmreviewWeek"`
+ VideoMonth SearchRankData `json:"videoMonth"`
+ ActressMonth SearchRankData `json:"actressMonth"`
+ FilmreviewMonth SearchRankData `json:"filmreviewMonth"`
+ VideoYear SearchRankData `json:"videoYear"`
+ ActressYear SearchRankData `json:"actressYear"`
+ FilmreviewYear SearchRankData `json:"filmreviewYear"`
+}
+type SearchRankData struct {
+ RankName string `json:"rankName" form:"rankName"` // 排行榜名字
+ RankType RankType `json:"rankType" form:"rankType"` // 排行榜类型 1视频, 2女优 ,3影评
+ RankPeriod RankPeriod `json:"rankPeriod" form:"rankPeriod"` // 排行榜周期 1=周 2=月 3=年
+ List []HotSearchItem `json:"list" form:"list"`
+}
+
+type HotSearchItem struct {
+ Id int64 `json:"id" form:"id"` // ID
+ Title string `json:"title" form:"title"` //标题
+ HotValue int64 `json:"hotValue" form:"hotValue"` // 热度值
+}
+
+type SearchResp struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ InTitle string `json:"inTitle"` //输入标题
+ InType int64 `json:"inType"` // 输入类型 -1=全部 0=视频 6=短剧
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Sort []SearchSortItem `json:"sorts"` // 排序选项
+ SearchRecommend []string `json:"searchRecommend"` // 搜索推荐
+ Media []VideoInfoData `json:"media" form:"media"` // 视频资源
+ Playlets []PlayletData `json:"playlets" form:"playlets"` // 短剧资源
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+}
+
+type SearchSortItem struct {
+ Key string `json:"key"` // 值
+ Title string `json:"title"` // 标题
+}
diff --git a/app/api/proto/sitemap.go b/app/api/proto/sitemap.go
new file mode 100644
index 0000000..d9b13c3
--- /dev/null
+++ b/app/api/proto/sitemap.go
@@ -0,0 +1,18 @@
+package proto
+
+import (
+ "encoding/xml"
+ "time"
+)
+
+type URL struct {
+ Loc string `xml:"loc"`
+ LastMod time.Time `xml:"lastmod"`
+ ChangeFreq string `xml:"changefreq"` // always/hourly/daily/weekly/monthly/yearly/never
+ Priority float32 `xml:"priority"` // 0.0 - 1.0
+}
+type Sitemap struct {
+ XMLName xml.Name `xml:"urlset"`
+ Xmlns string `xml:"xmlns,attr"`
+ URLs []URL `xml:"url"`
+}
diff --git a/app/api/proto/translation.go b/app/api/proto/translation.go
new file mode 100644
index 0000000..5f49de0
--- /dev/null
+++ b/app/api/proto/translation.go
@@ -0,0 +1,25 @@
+package proto
+
+import "videoweb/pub/pubdef"
+
+type AppTranslationInfo struct {
+ CategoryList []TranslationCategory `json:"categoryList" form:"categoryList"`
+}
+
+type TranslationCategory struct {
+ ID int64 `json:"id"`
+ Name string `json:"name" bson:"name"` // 名称
+ Icon string `json:"icon" bson:"icon"` // Icon
+ TranslationList []TranslationInfo `json:"translationList" form:"translationList"`
+ UrlValue string `json:"urlValue" bson:"urlValue"` // URl路径
+ CategoryType pubdef.TranslationType `json:"categoryType"` // 导航类型
+}
+
+type TranslationInfo struct {
+ ID int64 `json:"id"` // ID
+ Name string `json:"name" bson:"name"` // 名称
+ IsFriendAds bool `json:"isFriendAds"` // 是否是友链广告位
+ UrlValue string `json:"urlValue" bson:"urlValue"` // URl路径
+ TransCategoryId int64 `json:"-"` // 导航分类
+ DisplayMode pubdef.DisplayMode `json:"displayMode"` // 展示方式
+}
diff --git a/app/api/proto/video.go b/app/api/proto/video.go
new file mode 100644
index 0000000..ac71f51
--- /dev/null
+++ b/app/api/proto/video.go
@@ -0,0 +1,522 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/response"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+// latestTime 返回两个时间中较大的那个
+func latestTime(a, b time.Time) time.Time {
+ if b.After(a) {
+ return b
+ }
+ return a
+}
+
+type TagsInfoData struct {
+ Id int64 `json:"id" `
+ Name string `json:"name"` // 帖子标题
+ Slug string `json:"slug"` // slug
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Works int64 `json:"works" bson:"works,omitempty"` // 作品数量
+ Images string `json:"images" bson:"images,omitempty"` // 背景
+}
+type CollectionInfoData struct {
+ Id int64 `json:"id" `
+ Name string `json:"name"` // 帖子标题
+}
+type VideoType int64
+
+const (
+ VideoTypeVideo VideoType = 0 // 视频
+ VideoTypeCollection VideoType = 1 // 合集
+ VideoTypeAdvitise VideoType = 2 // 广告
+ VideoType_AV VideoType = 3 // AV视频
+)
+
+type VideoInfoData struct {
+ Id int64 `json:"id"`
+ Type VideoType `json:"type"` // 类型
+ Title string `json:"title"` // 帖子标题
+ Slug string `json:"slug"` // slug
+ Keyworld []string `json:"keyworld"` // 关键字
+ AddedTime time.Time `json:"addedTime"` // 帖子上架时间
+ CreateInfo PostCreateInfo `json:"createInfo"` // 作者信息
+ CoverImg string `json:"coverImg"` // 封面
+ // Nodes []videoinformationmod.Node `json:"nodes"` // 节点
+ Video videoinformationmod.VideoInfo `json:"video"` // 视频
+ Tags []TagsInfoData `json:"tags"` // 标签
+ IsHot bool `json:"isHot"` // 是否热门
+ AdvitiseId string `json:"advitiseId"` // 广告Id
+ CollectionInfo []VideoInfoData `json:"collectionInfo"` // 合集数据
+ Link string `json:"link"` // 跳转链接
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ LikeNum int64 `json:"likeNum,omitempty"` // 点赞展示数
+ CountCollect int64 `json:"countCollect,omitempty"` // 收藏展示数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Location int64 `json:"location" bson:"location"` // 广告位置id
+ ActressInfo []ActressInfoData `json:"actressInfo" bson:"actressInfo"` // 女优信息
+ IssuedAt time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
+ IssuedBrand string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+ UpdatedAt time.Time `json:"updatedAt"` // 页面更新时间
+ PreviewVtt string `json:"previewVtt,omitempty"` // 雪碧图 vtt
+}
+
+// 面包屑
+type BreadcrumbItem struct {
+ Name string `json:"name"` // 名称
+ Url string `json:"url"` // 跳转链接
+}
+
+// 详情页导航
+type DetailNavItem struct {
+ Name string `json:"name"` // 导航名称
+ Url string `json:"url"` // 导航链接
+}
+
+type VideoHistoryInfo struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"` // 帖子标题
+ AddedTime time.Time `json:"addedTime"` // 上架时间
+}
+
+type VideoCategoryInfo struct {
+ Id int64 `json:"id" `
+ Name string `json:"name"`
+ Slug string `json:"slug"` // slug
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Title string `json:"title" bson:"title"` // APP显示
+ Desc string `json:"desc" bson:"desc"` // 描述
+}
+
+// VideoCategoryListResp 已启用视频分类列表响应
+type VideoCategoryListResp struct {
+ List []VideoCategoryListItem `json:"list"` // 全部已启用视频分类
+}
+
+// VideoCategoryListItem 视频分类列表项
+type VideoCategoryListItem struct {
+ Id int64 `json:"id"` // 分类ID
+ Name string `json:"name"` // 分类名称
+ Slug string `json:"slug"` // 分类slug
+ Title string `json:"title"` // APP展示标题
+ Desc string `json:"desc"` // 分类描述
+ Rank int64 `json:"rank"` // 排序值,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // SEO信息
+}
+
+type VideoPlayReq struct {
+ VideoId int64 `json:"videoId" form:"videoId"` // 视频ID
+}
+
+type VideoPlayResp struct {
+ AuthKey string `json:"authKey"`
+}
+
+type VideoCreaterInfo struct {
+ Id int64 `json:"id" `
+ Name string `json:"name"`
+ Slug string `json:"slug"` // slug
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+}
+
+// QueryVideoReq 根据分类id查询视频列表请求
+type QueryVideoReq struct {
+ pubdef.Page
+ // 视频分类id
+ CategoryId int64 `json:"categoryId" form:"categoryId"`
+ // 分类slug,非空时优先按slug解析分类ID
+ CategorySlug string `json:"categorySlug" form:"categorySlug"`
+}
+
+// QueryVideoItemType 视频更多列表项类型
+type QueryVideoItemType int64
+
+const (
+ // QueryVideoItemTypeVideo 表示视频数据
+ QueryVideoItemTypeVideo QueryVideoItemType = 0
+ // QueryVideoItemTypeAdvertise 表示广告数据
+ QueryVideoItemTypeAdvertise QueryVideoItemType = 2
+)
+
+// QueryVideoResp 视频更多分页响应
+type QueryVideoResp struct {
+ response.BasePageResp
+ // 当前分类信息(含 SEO)
+ CategoryInfo VideoCategoryInfo `json:"categoryInfo"`
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+}
+
+// QueryVideoListItem 视频更多列表项,type=0时返回视频信息,type=2时返回广告信息。
+type QueryVideoListItem struct {
+ // 数据类型:0-视频数据、2-广告
+ Type QueryVideoItemType `json:"type"`
+ // 广告ID,type=2时返回
+ AdvertiseId string `json:"advertiseId"`
+ // 广告跳转链接,type=2时返回
+ Link string `json:"link"`
+ // 广告位置ID,type=2时返回
+ Location int64 `json:"location"`
+ VideoInfo
+}
+
+// VideoRecommendListReq 推荐视频分页请求
+type VideoRecommendListReq struct {
+ pubdef.Page
+ // 分类id,不传查询全部分类id
+ CategoryId int64 `form:"categoryId" json:"categoryId"`
+ // 分类slug,非空时优先按slug解析分类ID
+ CategorySlug string `form:"categorySlug" json:"categorySlug"`
+ // 标签ID,不传查询全部标签
+ TagId int64 `form:"tagId" json:"tagId"`
+ // 标签slug,非空时优先按slug解析标签ID
+ TagSlug string `form:"tagSlug" json:"tagSlug"`
+ // 排序方式 1-推荐 2-最新 3-最热
+ SortList int64 `form:"sortList" json:"sortList"`
+}
+
+// VideoListReq 上架视频分页请求
+type VideoListReq struct {
+ pubdef.Page
+ CategoryId int64 `form:"categoryId" json:"categoryId"` // 视频分类 ID,不传查询全部分类
+ SortType int64 `form:"sortType" json:"sortType"` // 排序方式,1 热度优先,2 最近更新,默认 1
+}
+
+// VideoListResp 视频与广告混排分页响应
+type VideoListResp struct {
+ response.BasePageResp
+ Advitise AdvitiseData `json:"advitise"` // 完整广告配置
+}
+
+// VideoRecommendSort 推荐视频排序方式
+type VideoRecommendSort int64
+
+const (
+ VideoRecommendSortRecommend VideoRecommendSort = 1 // 推荐 后台配置 isRecommend=true
+ VideoRecommendSortNewest VideoRecommendSort = 2 // 最新 按视频最后更新时间从近到远排序
+ VideoRecommendSortHot VideoRecommendSort = 3 // 最热 按视频累计播放量从高到低排序
+)
+
+// VideoRecommendItemType 推荐视频列表项类型
+type VideoRecommendItemType int64
+
+const (
+ VideoRecommendItemTypeVideo VideoRecommendItemType = 0 // 视频数据
+ VideoRecommendItemTypeAdvertise VideoRecommendItemType = 2 // 广告数据
+)
+
+// VideoRecommendListResp 推荐视频分页响应
+type VideoRecommendListResp struct {
+ response.BasePageResp
+}
+
+// VideoGuessListReq 猜你喜欢视频分页请求
+type VideoGuessListReq struct {
+ pubdef.Page
+ Id int64 `form:"id" json:"id"` // 当前视频ID,可选,传入时排除该视频
+}
+
+// VideoGuessListResp 猜你喜欢视频分页响应
+type VideoGuessListResp struct {
+ response.BasePageResp
+}
+
+// VideoRecommendListItem 推荐视频列表项
+type VideoRecommendListItem struct {
+ Type VideoRecommendItemType `json:"type"` // 数据类型 0-视频数据 2-广告
+ AdvertiseId string `json:"advertiseId"` // 广告ID,type=2时返回
+ Link string `json:"link"` // 广告跳转链接,type=2时返回
+ Location int64 `json:"location"` // 广告位置ID,type=2时返回
+ VideoInfo
+}
+
+// FavoriteVideoReq 收藏或取消收藏视频请求
+type FavoriteVideoReq struct {
+ // ID
+ Id int64 `json:"id" binding:"required"`
+ // 是否收藏,true-收藏、false-取消收藏
+ IsFavorite *bool `json:"isFavorite" binding:"required"`
+}
+
+// FavoriteVideoResp 收藏或取消收藏视频响应
+type FavoriteVideoResp struct {
+ // ID
+ Id int64 `json:"id"`
+ // 是否收藏,true-收藏、false-取消收藏
+ IsFavorite bool `json:"isFavorite"`
+}
+
+// LikeVideoReq 点赞或取消点赞视频请求
+type LikeVideoReq struct {
+ // ID
+ Id int64 `json:"id" binding:"required"`
+ // 是否点赞,true-点赞、false-取消点赞
+ IsLike *bool `json:"isLike" binding:"required"`
+}
+
+// LikeVideoResp 点赞或取消点赞视频响应
+type LikeVideoResp struct {
+ // ID
+ Id int64 `json:"id"`
+ // 是否点赞,true-点赞、false-取消点赞
+ IsLike bool `json:"isLike"`
+ // 点赞展示数
+ LikeNum int64 `json:"likeNum"`
+}
+
+// QueryLikeVideoListReq 当前用户点赞视频分页请求
+type QueryLikeVideoListReq struct {
+ pubdef.Page
+}
+
+// QueryLikeVideoListResp 当前用户点赞视频分页响应
+type QueryLikeVideoListResp struct {
+ response.BasePageResp
+ Translations AppTranslationInfo `json:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
+}
+
+// VideoHistoryListReq 当前用户视频观看历史分页请求
+type VideoHistoryListReq struct {
+ pubdef.Page
+}
+
+// VideoHistoryReportReq 播放成功后上报视频观看历史
+type VideoHistoryReportReq struct {
+ Id int64 `json:"id" binding:"required,gt=0"` // 视频 ID
+}
+
+// VideoHistoryReportResp 视频观看历史上报响应
+type VideoHistoryReportResp struct {
+ WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
+}
+
+// VideoHistoryListResp 当前用户视频观看历史分页响应
+type VideoHistoryListResp struct {
+ response.BasePageResp
+ Translations AppTranslationInfo `json:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
+}
+
+// VideoHistoryItem 视频观看历史卡片
+type VideoHistoryItem struct {
+ FavoriteVideoListItem
+ Desc string `json:"desc"` // 视频简介
+ WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
+}
+
+// VideoHistoryClearResp 清空视频观看历史响应
+type VideoHistoryClearResp struct {
+ DeletedCount int64 `json:"deletedCount"` // 删除记录数
+}
+
+// QueryFavoriteVideoListReq 当前登录用户收藏视频分页请求
+type QueryFavoriteVideoListReq struct {
+ pubdef.Page
+}
+
+// QueryFavoriteVideoListResp 当前登录用户收藏视频分页响应
+type QueryFavoriteVideoListResp struct {
+ response.BasePageResp
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+}
+
+// FavoriteVideoListItem 当前登录用户收藏视频列表项
+type FavoriteVideoListItem struct {
+ // id
+ Id int64 `json:"id"`
+ // 标题
+ Title string `json:"title"`
+ // slug
+ Slug string `json:"slug"`
+ // 上架时间
+ AddedTime time.Time `json:"addedTime"`
+ // 封面
+ CoverImg string `json:"coverImg"`
+ // 标签id
+ Tags []int64 `json:"tags"`
+ // 标签名称
+ TagsName []string `json:"tagsName"`
+ // 标签slug 与tagsName顺序一致
+ TagSlugs []string `json:"tagSlugs"`
+ // 观看展示数
+ WatchNum int64 `json:"watchNum"`
+ // 点赞展示数
+ LikeNum int64 `json:"likeNum"`
+ // 收藏展示数
+ CountCollect int64 `json:"countCollect"`
+}
+
+// VideoRankReq 视频排行榜分页请求
+type VideoRankReq struct {
+ pubdef.Page
+ // 榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜
+ RankPeriod int64 `json:"rankPeriod" form:"rankPeriod"`
+}
+
+// VideoRankPeriod 视频排行榜类型
+type VideoRankPeriod int64
+
+const (
+ VideoRankPeriodLike VideoRankPeriod = 0 // 点赞榜,按累计点赞量降序,最多50条,不足随机补位
+ VideoRankPeriodWeek VideoRankPeriod = 1 // 周榜,按本周观看量降序,最多50条,不足随机补位
+ VideoRankPeriodMonth VideoRankPeriod = 2 // 月榜,按本月观看量降序,最多50条,不足随机补位
+ VideoRankPeriodYear VideoRankPeriod = 3 // 年榜,按本年观看量降序,最多50条,不足随机补位
+)
+
+// VideoRankResp 视频排行榜分页响应
+type VideoRankResp struct {
+ response.BasePageResp
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+}
+
+// QueryVideoInfoReq 视频详情请求
+type QueryVideoInfoReq struct {
+ Id int64 `json:"id" form:"id"`
+ // 视频slug,非空时优先按slug解析视频ID
+ Slug string `json:"slug" form:"slug"`
+}
+
+// QueryVideoInfoResp 视频详情响应
+type QueryVideoInfoResp struct {
+ // 当前路径
+ Path string `json:"path"`
+ // 导航
+ Translations AppTranslationInfo `json:"translations"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 底部翻译
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 公告
+ Announcement AnnouncementInfo `json:"announcement"`
+
+ // 视频详情数据
+ VideoInfo VideoDetailInfo `json:"videoInfo"`
+ // 当前登录用户是否已收藏该视频
+ IsFavorite bool `json:"isFavorite"`
+ // 当前登录用户是否已点赞该视频
+ IsLike bool `json:"isLike"`
+}
+
+// VideoDetailInfo 视频详情数据
+type VideoDetailInfo struct {
+ // id
+ Id int64 `json:"id"`
+ // 标题
+ Title string `json:"title"`
+ // slug
+ Slug string `json:"slug"`
+ // 封面
+ CoverImg string `json:"coverImg"`
+ // 视频播放信息
+ Video videoinformationmod.VideoInfo `json:"video"`
+ // 标签
+ Tags []TagsInfoData `json:"tags"`
+ // 观看展示数
+ WatchNum int64 `json:"watchNum"`
+ // 点赞展示数
+ LikeNum int64 `json:"likeNum"`
+ // 收藏展示数
+ CountCollect int64 `json:"countCollect"`
+ // 上架时间
+ AddedTime time.Time `json:"addedTime"`
+ // 作品简介
+ Desc string `json:"desc"`
+ // 当前登录用户是否已收藏该视频
+ IsFavorite bool `json:"isFavorite"`
+ // 当前登录用户是否已点赞该视频
+ IsLike bool `json:"isLike"`
+}
+
+func VideoInfoToData(videoInfo *videoinformationmod.VideoInformation, imgCdn string, creatorInfo PostCreateInfo) VideoInfoData {
+ t := VideoTypeVideo
+ switch videoInfo.MediaType {
+ case videoinformationmod.MediaType_Video:
+ t = VideoTypeVideo
+ case videoinformationmod.MediaType_Collection:
+ t = VideoTypeCollection
+ case videoinformationmod.MediaType_Advitise:
+ t = VideoTypeAdvitise
+ case videoinformationmod.MediaType_AV:
+ t = VideoType_AV
+ }
+ videotemp := VideoInfoData{
+ Id: videoInfo.Id,
+ Type: t,
+ Title: videoInfo.Title,
+ Slug: videoInfo.Slug,
+ Keyworld: videoInfo.Keyworld,
+ AddedTime: videoInfo.AddedTime,
+ CreateInfo: creatorInfo,
+ CoverImg: netutil.MergeURL(imgCdn, videoInfo.CoverImg),
+ // Nodes: make([]videoinformationmod.Node, 0),
+ Video: videoInfo.Video,
+ IsHot: videoInfo.IsHot,
+ Tags: make([]TagsInfoData, 0),
+ ShowTimes: videoInfo.Real.ShowTimes*100 + 6241,
+ WatchTimes: videoInfo.Real.WatchTimes*100 + 3152,
+ ActressInfo: make([]ActressInfoData, 0),
+ Desc: videoInfo.Desc,
+ Bango: videoInfo.Bango,
+ IssuedBrand: videoInfo.IssuedBrand,
+ IssuedAt: videoInfo.IssuedAt,
+ UpdatedAt: latestTime(videoInfo.UpdatedAt, videoInfo.PageUpdatedAt),
+ PreviewVtt: videoInfo.PreviewVtt,
+ }
+ if videoInfo.Video.Cover != "" {
+ videotemp.Video.Cover = netutil.MergeURL(imgCdn, videotemp.Video.Cover)
+ }
+ //for _, nodeInfo := range videoInfo.Nodes {
+ // nodeTmp := videoinformationmod.Node{
+ // Images: make([]string, 0),
+ // Url: nodeInfo.Url,
+ // Text: nodeInfo.Text,
+ // Videos: make([]videoinformationmod.VideoInfo, 0),
+ // Title: nodeInfo.Title,
+ // }
+ // for _, image := range nodeInfo.Images {
+ // nodeTmp.Images = append(nodeTmp.Images, netutil.MergeURL(imgCdn, image))
+ // }
+ // if nodeInfo.Videos != nil && len(nodeInfo.Videos) > 0 {
+ // for _, video := range nodeInfo.Videos {
+ // videoTmp := videoinformationmod.VideoInfo{
+ // TaskId: video.TaskId,
+ // FileId: video.FileId,
+ // Url: netutil.MergeURL(imgCdn, video.Url),
+ // Cover: video.Cover,
+ // Height: video.Height,
+ // Width: video.Width,
+ // VideoTime: video.VideoTime,
+ // PreMp4FileNameFs: video.PreMp4FileNameFs,
+ // }
+ // nodeTmp.Videos = append(nodeTmp.Videos, videoTmp)
+ // }
+ // }
+ // videotemp. = append(videotemp.Nodes, nodeTmp)
+ //}
+ return videotemp
+}
+
+// StripH265Url 清空主视频 H265 地址。
+func StripH265Url(data *VideoInfoData) {
+ if data == nil {
+ return
+ }
+ data.Video.H265Url = ""
+}
diff --git a/app/api/proto/webdata.go b/app/api/proto/webdata.go
new file mode 100644
index 0000000..9fb442b
--- /dev/null
+++ b/app/api/proto/webdata.go
@@ -0,0 +1,212 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+// HomeHtmlData 首页
+type HomeHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"` // 请求路径
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+}
+
+type VideoCategoryData struct {
+ Id int64 `json:"id" form:"id"`
+ Name string `json:"name" form:"name"`
+ Slug string `json:"slug"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"`
+}
+
+type VideoCategoryHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ VideoCategory VideoCategoryData `json:"videoCategory" form:"videoCategory"` // 当前分类数据
+}
+
+type CategoryVideoListHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
+ CategoryList []VideoCategoryData `json:"categoryList" form:"categoryList"`
+}
+
+type VideoCategoryListHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
+ CategoryList []VideoCategoryInfo `json:"categoryList" form:"categoryList"`
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+}
+
+type VideoDetailsHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Video VideoInfoData `json:"video" form:"video"` // 视频详情
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
+ RecommendVideos []VideoInfoData `json:"recommendVideos" form:"recommendVideos"` // 推荐视频
+ ChoiceVideos []VideoInfoData `json:"choiceVideos" form:"choiceVideos"` // 精选视频
+ Breadcrumbs []BreadcrumbItem `json:"breadcrumbs" form:"breadcrumbs"` // 面包屑
+ DetailNavs []DetailNavItem `json:"detailNavs" form:"detailNavs"` // 详情页导航
+}
+
+// CreateVideoListHtmlData 创作者视频列表
+type CreateVideoListHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ CreateInfo PostCreateInfo `json:"createInfo" bson:"createInfo"` // 创作者信息
+ VideoList []VideoInfoData `json:"videoList" form:"videoList"`
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+}
+type PostCreateInfo struct {
+ Id int64 `json:"id" form:"id"`
+ Name string `json:"name" form:"name"`
+ Slug string `json:"slug"` // slug
+ Image string `json:"image" form:"image"` //头像
+ Works int64 `json:"works" form:"works"`
+ CreateType int64 `json:"createType"` // 创作者类型 0=普通 1=官方
+ Desc string `json:"desc" form:"desc"` // 作者描述
+ Background string `json:"background"` // 背景
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+}
+
+// TagHtmlData 首页
+type TagHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Media []VideoInfoData `json:"media" form:"media"` // 视频资源
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Tag TagsInfoData `json:"tag" form:"tag"` // 标签
+ SortItem []videoinformationmod.VideoSortItem `json:"sortItem" form:"sortItem"` // 排序项
+ SortValue videoinformationmod.VideoSortItem `json:"sortValue" form:"sortValue"` // 当前排序项
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+}
+type VideoTagHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Tags []TagsInfoData `json:"tags" form:"tags"`
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+}
+
+type VideoHistoryHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ Tags []TagsInfoData `json:"tags" form:"tags"` // 标签
+ Media []VideoHistoryInfo `json:"media" form:"media"` // 视频
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+}
+
+type RichTextHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+}
+type NotFoundHtmlData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+}
+
+type UrlRedirectData struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
+ BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Category VideoCategoryInfo `json:"category" form:"category"` // 分类
+ UrlRedirect string `json:"urlRedirect" form:"urlRedirect"`
+}
+
+type ContactUsReq struct {
+ Name string `json:"name" form:"name"` // 姓名
+ Email string `json:"email" form:"email"` // 邮件
+ Content string `json:"content" form:"content"` // 内容
+ CategoryName string `json:"categoryName" form:"categoryName"` // 主旨分类
+}
+
+// QuestionnaireHtmlData 问卷调查页
+type QuestionnaireHtmlData struct {
+ Path string `json:"path"`
+ Translation AppTranslationInfo `json:"translation" form:"translation"`
+ Config ConfigInfo `json:"config" form:"config"`
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+ Announcement AnnouncementInfo `json:"announcement" form:"announcement"`
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"`
+}
diff --git a/app/api/questionnaireapi/page.go b/app/api/questionnaireapi/page.go
new file mode 100644
index 0000000..ace5d15
--- /dev/null
+++ b/app/api/questionnaireapi/page.go
@@ -0,0 +1,27 @@
+package questionnaireapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/searchser"
+ "videoweb/app/service/translationser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Page 问卷调查页
+func Page(c *gin.Context) {
+ hotSearch := searchser.GetHotSearchItem(c)
+ data := proto.QuestionnaireHtmlData{
+ Path: c.Request.URL.Path,
+ Translation: translationser.GetTranslationData(),
+ Config: configser.GetRequestConfig(c),
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ HotSearch: hotSearch,
+ }
+ configser.RewriteImageCdn(c, &data)
+ c.HTML(200, "questionnaire", gin.H{
+ "data": data,
+ })
+}
diff --git a/app/api/questionnaireapi/questionnaire.go b/app/api/questionnaireapi/questionnaire.go
new file mode 100644
index 0000000..50fa313
--- /dev/null
+++ b/app/api/questionnaireapi/questionnaire.go
@@ -0,0 +1,34 @@
+package questionnaireapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/questionnaireser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Submit 提交问卷
+// @Tags 问卷调查APP
+// @Summary 提交问卷
+// @Description 用户提交问题反馈问卷
+// @Accept json
+// @Produce json
+// @Param param body proto.QuestionnaireSubmitReq true "提交参数"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/questionnaire/submit [post]
+func Submit(ctx *gin.Context) {
+ var req proto.QuestionnaireSubmitReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := questionnaireser.Submit(ctx, ctx.ClientIP(), &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
diff --git a/app/api/richtextapi/richtext.go b/app/api/richtextapi/richtext.go
new file mode 100644
index 0000000..cc67ce1
--- /dev/null
+++ b/app/api/richtextapi/richtext.go
@@ -0,0 +1,36 @@
+package richtextapi
+
+import (
+ "videoweb/app/service/richtextser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+type getByTitleReq struct {
+ Title string `form:"title" binding:"required"`
+}
+
+// GetByTitle 按 title 查询富文本
+// @Tags 富文本
+// @Summary 按 title 查询富文本
+// @Description 从缓存中查询启用状态的富文本信息
+// @Produce json
+// @Param title query string true "富文本内部标题"
+// @Success 200 {object} proto.RichTextInfo "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/richtext [get]
+func GetByTitle(ctx *gin.Context) {
+ var req getByTitleReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ data, serr := richtextser.GetByTitle(req.Title)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, data)
+}
diff --git a/app/api/searchapi/search.go b/app/api/searchapi/search.go
new file mode 100644
index 0000000..0eb184f
--- /dev/null
+++ b/app/api/searchapi/search.go
@@ -0,0 +1,53 @@
+package searchapi
+
+import (
+ "fmt"
+ "strconv"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/searchser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SearchApi 搜索
+// @Tags 搜索
+// @Summary 搜索
+// @Description 搜索视频和短剧,省略 type 查询全部,type 为 0 查询视频,为 6 查询短剧,sort 支持 default/hot/new
+// @Accept json
+// @Produce json
+// @Param title query string false "搜索关键词"
+// @Param page query int false "页码,默认1"
+// @Param type query int false "媒体类型,不传=全部,0=视频,6=短剧"
+// @Param sort query string false "排序方式 default/hot/new,默认default"
+// @Success 200 {object} proto.SearchResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /search [get]
+func SearchApi(c *gin.Context) {
+ title := c.Query("title")
+ pageStr := c.DefaultQuery("page", "1")
+ sort := c.DefaultQuery("sort", string(proto.SearchSortTypeDefault))
+ mediaType := int64(-1)
+ if typeStr, ok := c.GetQuery("type"); ok {
+ var err error
+ mediaType, err = strconv.ParseInt(typeStr, 10, 64)
+ if err != nil || (mediaType != 0 && mediaType != 6) {
+ common.ServerJSON(c, srvdef.ErrParamError, "type 只支持 0 或 6")
+ return
+ }
+ }
+ page, err := strconv.ParseInt(pageStr, 10, 64)
+ if err != nil || page < 1 {
+ page = 1
+ }
+ data := searchser.Search(c, title, mediaType, page, sort)
+ data.Path = c.Request.URL.Path
+ data.CompletePath = fmt.Sprintf("/search")
+ data.Advitise = configser.GetAdvitise()
+ data.BottonTranslations = translationser.GetTranslationBottomList()
+ data.Translations = translationser.GetTranslationData()
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
diff --git a/app/api/searchapi/search_test.go b/app/api/searchapi/search_test.go
new file mode 100644
index 0000000..58fd5ef
--- /dev/null
+++ b/app/api/searchapi/search_test.go
@@ -0,0 +1,29 @@
+package searchapi
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+func TestSearchApiRejectsUnsupportedTypes(t *testing.T) {
+ for _, query := range []string{"type=1", "type=3", "type=2", "type=-1", "type=abc", "type="} {
+ writer := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(writer)
+ ctx.Request = httptest.NewRequest(http.MethodGet, "/search/?"+query, nil)
+ SearchApi(ctx)
+ var resp struct {
+ Code int64 `json:"code"`
+ }
+ if err := json.Unmarshal(writer.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("参数 %s 的响应无法解析: %v", query, err)
+ }
+ if resp.Code != int64(srvdef.ErrParamError) {
+ t.Errorf("参数 %s 返回 code %d, want %d", query, resp.Code, srvdef.ErrParamError)
+ }
+ }
+}
diff --git a/app/api/uploadapi/upload.go b/app/api/uploadapi/upload.go
new file mode 100644
index 0000000..9457670
--- /dev/null
+++ b/app/api/uploadapi/upload.go
@@ -0,0 +1,128 @@
+package uploadapi
+
+import (
+ "net/http"
+
+ "videoweb/app/service/uploadser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// VideoBody 分片上传视频
+// @Tags 上传
+// @Summary 视频分片上传
+// @Description 将视频分片转发至存储服务 /media/web/autopublish/videoBody
+// @Accept multipart/form-data
+// @Produce json
+// @Param taskId formData string true "视频任务ID"
+// @Param pos formData string true "当前分片序号,从0开始"
+// @Param uploadUserName formData string false "上传用户名,为空默认 questionnaires"
+// @Param file formData file true "分片文件数据"
+// @Success 200 {object} uploadser.VideoBodyResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Router /api/app/upload/videoBody [post]
+func VideoBody(ctx *gin.Context) {
+ form, err := ctx.MultipartForm()
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ taskIds := form.Value["taskId"]
+ posList := form.Value["pos"]
+ uploadUserNames := form.Value["uploadUserName"]
+ fileList := form.File["file"]
+
+ if len(taskIds) == 0 || taskIds[0] == "" {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "taskId 不能为空")
+ return
+ }
+ if len(posList) == 0 || posList[0] == "" {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "pos 不能为空")
+ return
+ }
+ if len(fileList) == 0 {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "file 不能为空")
+ return
+ }
+
+ param := &uploadser.VideoBodyParam{
+ TaskId: taskIds[0],
+ Pos: posList[0],
+ FileHeader: fileList[0],
+ }
+ if len(uploadUserNames) > 0 {
+ param.UploadUserName = uploadUserNames[0]
+ }
+
+ respBytes, err := uploadser.ForwardVideoBody(param)
+ if err != nil {
+ log.Error("VideoBody forward failed", log.E(err))
+ common.ServerJSON(ctx, srvdef.ServerError, nil)
+ return
+ }
+
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
+}
+
+// VideoOk 通知视频分片上传完成
+// @Tags 上传
+// @Summary 视频分片上传完成通知
+// @Description 所有分片上传完毕后调用,转发至存储服务 /media/web/autopublish/videoOk
+// @Accept json
+// @Produce json
+// @Param param body object true "{ \"taskId\": \"...\" }"
+// @Success 200 {object} uploadser.VideoOkResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Router /api/app/upload/videoOk [post]
+func VideoOk(ctx *gin.Context) {
+ var req struct {
+ TaskId string `json:"taskId" binding:"required"`
+ }
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ respBytes, err := uploadser.ForwardVideoOk(req.TaskId)
+ if err != nil {
+ log.Error("VideoOk forward failed", log.E(err))
+ common.ServerJSON(ctx, srvdef.ServerError, nil)
+ return
+ }
+
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
+}
+
+// UploadImage 上传图片
+// @Tags 上传
+// @Summary 图片上传
+// @Description 将图片转发至存储服务 /media/web/autopublish/image
+// @Accept multipart/form-data
+// @Produce json
+// @Param file formData file true "图片文件"
+// @Success 200 {object} uploadser.ImageUploadResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Router /api/app/upload/image [post]
+func UploadImage(ctx *gin.Context) {
+ f, err := ctx.FormFile("file")
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "file 不能为空")
+ return
+ }
+
+ param := &uploadser.ImageParam{
+ FileHeader: f,
+ }
+ respBytes, err := uploadser.ForwardImage(param)
+ if err != nil {
+ log.Error("UploadImage forward failed", log.E(err))
+ common.ServerJSON(ctx, srvdef.ServerError, nil)
+ return
+ }
+
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
+}
diff --git a/app/api/videoapi/video.go b/app/api/videoapi/video.go
new file mode 100644
index 0000000..59b520c
--- /dev/null
+++ b/app/api/videoapi/video.go
@@ -0,0 +1,428 @@
+package videoapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/slugser"
+ "videoweb/app/service/translationser"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List app端上架视频列表
+// @Tags 视频相关接口
+// @Summary app端上架视频列表 -- by 王鸣
+// @Description 按分类和排序分页查询上架视频,最近更新按更新时间降序,每五条视频后穿插一条广告
+// @Accept json
+// @Produce json
+// @Param params query proto.VideoListReq true "视频列表请求"
+// @Success 200 {object} proto.VideoListResp{list=[]proto.VideoRecommendListItem} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/list [get]
+func List(c *gin.Context) {
+ var req proto.VideoListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := videoser.VideoList(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// CategoryList 查询全部已启用视频分类
+// @Tags 视频相关接口
+// @Summary app端视频分类列表
+// @Produce json
+// @Success 200 {object} proto.VideoCategoryListResp "成功"
+// @Router /api/app/video/category/list [get]
+func CategoryList(c *gin.Context) {
+ resp, err := videoser.VideoCategoryList(c)
+ if err != nil {
+ common.ServerJSON(c, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// RecommendList app端推荐视频列表
+// @Tags 视频相关接口
+// @Summary app端推荐视频列表 -- by 王鸣
+// @Description 分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
+// @Accept json
+// @Produce json
+// @Param params query proto.VideoRecommendListReq true "推荐视频列表请求"
+// @Success 200 {object} proto.VideoRecommendListResp{list=[]proto.VideoRecommendListItem} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/recommend/list [get]
+func RecommendList(c *gin.Context) {
+ var req proto.VideoRecommendListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := videoser.VideoRecommendList(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// GuessList 分页查询猜你喜欢视频
+// @Tags 视频相关接口
+// @Summary 猜你喜欢视频列表 -- by 王鸣
+// @Description id可选,传入时排除当前视频,按展示观看量降序查询上架视频
+// @Produce json
+// @Param params query proto.VideoGuessListReq true "猜你喜欢视频请求"
+// @Success 200 {object} proto.VideoGuessListResp{list=[]proto.VideoRecommendListItem} "成功"
+// @Router /api/app/video/guess/list [get]
+func GuessList(c *gin.Context) {
+ var req proto.VideoGuessListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := videoser.VideoGuessList(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// TagList app端根据标签ID查询视频列表
+// @Tags 视频相关接口
+// @Summary app端根据标签ID查询视频列表 -- by 王鸣
+// @Description 根据统一标签ID分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
+// @Accept json
+// @Produce json
+// @Param id path int true "标签ID"
+// @Param pageNum query int false "页码,默认1"
+// @Param pageSize query int false "每页数量,默认20,最大100"
+// @Success 200 {object} proto.VideoRecommendListResp{list=[]proto.VideoRecommendListItem} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/tag/{id} [get]
+func TagList(c *gin.Context) {
+ var req proto.ContentTagPageReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ pathID, pathSlug := slugser.ParseIDOrSlug(c.Param("id"))
+ if req.Id <= 0 {
+ req.Id = pathID
+ }
+ if req.Slug == "" {
+ req.Slug = pathSlug
+ }
+ data, err := videoser.VideoTagList(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// QueryVideoMore
+// @Tags 视频相关接口
+// @Summary 根据分类id查询视频列表 --by 王鸣
+// @Description 根据分类id查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
+// @Accept json
+// @Produce json
+// @Param params query proto.QueryVideoReq true "视频更多查询请求"
+// @Success 200 {object} proto.QueryVideoResp{list=[]proto.QueryVideoListItem{VideoInfo=proto.VideoInfo}} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/more [get]
+func QueryVideoMore(c *gin.Context) {
+ var req proto.QueryVideoReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := videoser.QueryVideoMore(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// QueryVideoInfo 视频详情
+// @Tags 视频相关接口
+// @Summary 视频详情 -- by 王鸣
+// @Description 查询视频详情、收藏状态和点赞状态
+// @Accept json
+// @Produce json
+// @Param params query proto.QueryVideoInfoReq true "视频详情查询请求"
+// @Success 200 {object} proto.QueryVideoInfoResp{videoInfo=proto.VideoDetailInfo} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/info [get]
+func QueryVideoInfo(c *gin.Context) {
+ var req proto.QueryVideoInfoReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := videoser.QueryVideoInfo(c, common.GetUserId(c), videoser.H5M3U8RequestIP(c), c.Request.UserAgent(), &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// FavoriteVideo 收藏或取消收藏视频
+// @Tags 视频相关接口
+// @Summary 收藏或取消收藏视频 --by 旺鸣
+// @Description 当前登录用户收藏或取消收藏指定视频,isFavorite=true表示收藏,isFavorite=false表示取消收藏
+// @Accept json
+// @Produce json
+// @Param param body proto.FavoriteVideoReq true "收藏或取消收藏请求"
+// @Success 200 {object} proto.FavoriteVideoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/app/video/favorite [post]
+func FavoriteVideo(c *gin.Context) {
+ var req proto.FavoriteVideoReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ userId := common.GetUserId(c)
+ if userId <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ ua, _ := common.GetUA(c)
+ if ua.UserAgent == "" {
+ ua.UserAgent = c.Request.UserAgent()
+ }
+ data, err := videoser.FavoriteVideo(c, userId, c.ClientIP(), ua, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// LikeVideo 点赞或取消点赞视频
+// @Tags 视频相关接口
+// @Summary 点赞或取消点赞视频 -- by 王鸣
+// @Description 当前登录用户点赞或取消点赞指定视频,isLike=true表示点赞,isLike=false表示取消点赞
+// @Accept json
+// @Produce json
+// @Param param body proto.LikeVideoReq true "点赞或取消点赞请求"
+// @Success 200 {object} proto.LikeVideoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/app/video/like [post]
+func LikeVideo(c *gin.Context) {
+ var req proto.LikeVideoReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ userId := common.GetUserId(c)
+ if userId <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ ua, _ := common.GetUA(c)
+ if ua.UserAgent == "" {
+ ua.UserAgent = c.Request.UserAgent()
+ }
+ data, err := videoser.LikeVideo(c, userId, c.ClientIP(), ua, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// QueryVideoRank 视频收藏排行榜
+// @Tags 视频相关接口
+// @Summary 视频收藏排行榜
+// @Description 视频收藏排行榜
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.RankResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/rank [get]
+func QueryVideoRank(c *gin.Context) {
+ rank := proto.HomeRankData{
+ Total: configser.GetVideoRankList(configser.RankPeriodTotal, 0),
+ Week: configser.GetVideoRankList(configser.RankPeriodWeek, 0),
+ Month: configser.GetVideoRankList(configser.RankPeriodMonth, 0),
+ Year: configser.GetVideoRankList(configser.RankPeriodYear, 0),
+ }
+ common.ServerJSON(c, srvdef.Success, proto.RankResp{
+ RankData: rank,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ })
+}
+
+// QueryVideoRankList 视频排行榜分页列表
+// @Tags 视频相关接口
+// @Summary 视频排行榜分页列表 -- by 王鸣
+// @Description 分页查询视频排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50
+// @Accept json
+// @Produce json
+// @Param params query proto.VideoRankReq true "视频排行榜分页请求"
+// @Success 200 {object} proto.VideoRankResp{list=[]proto.ContentData} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/video/rank/list [get]
+func QueryVideoRankList(c *gin.Context) {
+ var req proto.VideoRankReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ data, err := videoser.QueryVideoRank(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// QueryFavoriteVideoList 当前登录用户收藏视频分页列表
+// @Tags 视频相关接口
+// @Summary 当前登录用户收藏视频分页列表 --by 旺鸣
+// @Description 当前登录用户收藏视频分页列表,按收藏时间倒序排序
+// @Accept json
+// @Produce json
+// @Param params query proto.QueryFavoriteVideoListReq true "收藏视频分页请求"
+// @Success 200 {object} proto.QueryFavoriteVideoListResp{list=[]proto.FavoriteVideoListItem} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/app/video/favorite/list [get]
+func QueryFavoriteVideoList(c *gin.Context) {
+ var req proto.QueryFavoriteVideoListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ userId := common.GetUserId(c)
+ if userId <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ data, err := videoser.QueryFavoriteVideoList(c, userId, &req)
+ if err != nil {
+ common.ServerJSON(c, err, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+// QueryLikeVideoList 当前用户点赞视频分页列表
+// @Tags 视频相关接口
+// @Summary 当前用户点赞视频分页列表
+// @Description 按点赞时间倒序返回当前用户点赞的上架视频
+// @Produce json
+// @Param params query proto.QueryLikeVideoListReq true "点赞视频分页请求"
+// @Success 200 {object} proto.QueryLikeVideoListResp{list=[]proto.FavoriteVideoListItem} "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/video/like/list [get]
+func QueryLikeVideoList(c *gin.Context) {
+ var req proto.QueryLikeVideoListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ userID := common.GetUserId(c)
+ if userID <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ resp, err := videoser.QueryLikeVideoList(c, userID, &req)
+ if err != nil {
+ common.ServerJSON(c, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// ReportVideoHistory 播放成功后记录当前用户的视频
+// @Tags 视频相关接口
+// @Summary 上报视频观看历史
+// @Accept json
+// @Produce json
+// @Param param body proto.VideoHistoryReportReq true "当前播放视频"
+// @Success 200 {object} proto.VideoHistoryReportResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/video/history [post]
+func ReportVideoHistory(c *gin.Context) {
+ userID := common.GetUserId(c)
+ if userID <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ var req proto.VideoHistoryReportReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := videoser.ReportVideoHistory(c, userID, &req)
+ if err != nil {
+ common.ServerJSON(c, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// QueryVideoHistoryList 当前用户视频观看历史分页列表
+// @Tags 视频相关接口
+// @Summary 视频观看历史列表
+// @Produce json
+// @Param params query proto.VideoHistoryListReq true "分页请求"
+// @Success 200 {object} proto.VideoHistoryListResp{list=[]proto.VideoHistoryItem} "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/video/history/list [get]
+func QueryVideoHistoryList(c *gin.Context) {
+ userID := common.GetUserId(c)
+ if userID <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ var req proto.VideoHistoryListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "")
+ return
+ }
+ resp, err := videoser.QueryVideoHistoryList(c, userID, &req)
+ if err != nil {
+ common.ServerJSON(c, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// ClearVideoHistory 清空当前用户视频观看历史
+// @Tags 视频相关接口
+// @Summary 清空视频观看历史
+// @Produce json
+// @Success 200 {object} proto.VideoHistoryClearResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/video/history [delete]
+func ClearVideoHistory(c *gin.Context) {
+ userID := common.GetUserId(c)
+ if userID <= 0 {
+ common.ServerJSON(c, srvdef.InvalidToken, "")
+ return
+ }
+ resp, err := videoser.ClearVideoHistory(c, userID)
+ if err != nil {
+ common.ServerJSON(c, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
diff --git a/app/appg/appg.go b/app/appg/appg.go
new file mode 100644
index 0000000..102dd2b
--- /dev/null
+++ b/app/appg/appg.go
@@ -0,0 +1,125 @@
+package appg
+
+import (
+ "videoweb/pub/common/ipaddr"
+
+ "github.com/patrickmn/go-cache"
+)
+
+const EsIndex = "sz"
+
+var (
+ Conf GlobalCfgEx
+ Cache *cache.Cache
+ Name NameSource
+ DetectorV6 *ipaddr.XDBDetector
+ DetectorV4 *ipaddr.XDBDetector
+)
+
+type NameSource struct {
+ FamilyNames []string `yaml:"familyNames"`
+ MiddleNames []string `yaml:"middleNames"`
+ AfterNames []string `yaml:"afterNames"`
+}
+type PictureSource struct {
+ Portrait []string `yaml:"portrait"`
+ UserbackGround []string `yaml:"userbackground"`
+ ActressBackground []string `yaml:"actressbackground"`
+}
+type GlobalCfgEx struct {
+ Redis struct {
+ Addr string `yaml:"addr"`
+ Db int `yaml:"db"`
+ Passwd string `yaml:"passwd"`
+ Subscribe bool `yaml:"subscribe"`
+ }
+
+ Mongodb struct {
+ Addr string `yaml:"addr"`
+ LogAddr string `yaml:"logaddr"`
+ RecomAddr string `yaml:"recomaddr"`
+ AIOrderCenter string `yaml:"aiordercenter"`
+ }
+ Es struct {
+ Addr string `yaml:"addr"`
+ User string `yaml:"user"`
+ Pass string `yaml:"pass"`
+ } `yaml:"es"`
+ RecomSys struct {
+ Enable bool `yaml:"enable"`
+ Addr string `yaml:"addr"`
+ ApiKey string `yaml:"apikey"`
+ } `yaml:"recomsys"`
+ HttpOptions struct {
+ DcCtxTimeOut uint64 `yaml:"dcCtxTimeOut"` //代充http请求 超时ctx取消时间
+ } `yaml:"httpOptions"`
+ Kafka struct {
+ Url string `yaml:"url"`
+ Group string `yaml:"group"`
+ } `yaml:"kafka"`
+ JwtKey string `yaml:"jwtKey"`
+ RunMod string `yaml:"runMod"`
+ ResKey string `yaml:"resKey"`
+ CdnKey string `yaml:"cdnKey"`
+ Pprof bool `yaml:"pprof"`
+ Master bool `yaml:"master"` //是否是主节点
+ Host string `yaml:"host"`
+ ListenAddr string `yaml:"listenAddr"`
+ DHListenAddr string `yaml:"dhListenAddr"`
+ KfAppId string `yaml:"kfAppId"`
+ KfSecret string `yaml:"kfSecret"`
+ KfApiUrlPrefix string `yaml:"kfApiUrlPrefix"`
+ VersionUrl string `yaml:"versionUrl"`
+ KafkaUrl string `yaml:"kafkaUrl"`
+ CenterDomain string `yaml:"centerDomain"`
+ TgToken string `yaml:"tgToken"`
+ ReplayAttackConfig struct {
+ Enable bool `yaml:"enable"`
+ AppKey string `yaml:"appkey"`
+ GuestKey string `yaml:"guestKey"`
+ } `yaml:"attack"`
+ ReplayH5AttackConfig struct {
+ Enable bool `yaml:"enable"`
+ AppKey string `yaml:"appkey"`
+ GuestKey string `yaml:"guestKey"`
+ } `yaml:"h5attack"`
+ PayCenter struct {
+ Addr string `yaml:"addr"`
+ AppId int64 `yaml:"appId"`
+ Serect string `yaml:"serect"`
+ } `yaml:"payCenter"`
+ AiCenterAppId int64 `yaml:"aiCenterAppId"`
+ AiUndressingCallbackUrl string `yaml:"aiUndressingCallbackUrl"`
+ NavigateUrl string `yaml:"navigateUrl"`
+ NewProductUrl string `yaml:"newProductUrl"`
+ TransactionUrl string `yaml:"transactionUrl"` // 支付下单地址
+ LocalWebUrl string `yaml:"localWebUrl"`
+ JituanProductUrl string `yaml:"jituanProductUrl"`
+ AiImageToVideoUrl string `yaml:"aiImageToVideoUrl"` // AI图生视频请求地址
+ AiTextToImageUrl string `yaml:"aiTextToImageUrl"` // Ai文生图请求地址
+ AiMateH5 string `yaml:"aiMateH5"`
+ OrigDomain string `yaml:"origDomain"` // M3U8下载地址
+ CdnAuthKey string `yaml:"cdnAuthKey"`
+ TsAuthKey string `yaml:"tsAuthKey"`
+ LsjMediaConf struct {
+ AppId string `yaml:"appId"`
+ ApiKey string `yaml:"apiKey"`
+ ApiUrl string `yaml:"apiUrl"`
+ M3u8Yuan string `yaml:"m3u8Yuan"`
+ TsYuan string `yaml:"tsYuan"`
+ ImageYuan string `yaml:"imageYuan"`
+ DomainsRefreshHr int64 `yaml:"domainsRefreshHr"`
+ } `yaml:"lsjMediaConf"`
+ TrustedProxies string `yaml:"trustedProxies"` // 可信反向代理IP,多个用逗号分隔,空则不信任任何代理
+ BucketVideoAddressV2 string `yaml:"bucketVideoAddressV2"`
+ BucketVideoAddress string `yaml:"bucketVideoAddress"`
+ AdCenter struct {
+ ApiDomain string `yaml:"apiDomain"` // api地址 如:https://adopenapi.adcs01.top
+ MerchantCode string `yaml:"merchantCode"` // 商户 code
+ AppCode string `yaml:"appCode"` // 应用 code
+ AesKey string `yaml:"aesKey"` // 数据解密KEY
+ } `yaml:"adCenter"`
+ DataReport struct {
+ Url string `yaml:"url"`
+ } `yaml:"dataReport"`
+}
diff --git a/app/appg/cache.go b/app/appg/cache.go
new file mode 100644
index 0000000..0d70ba5
--- /dev/null
+++ b/app/appg/cache.go
@@ -0,0 +1,11 @@
+package appg
+
+import (
+ "time"
+
+ "github.com/patrickmn/go-cache"
+)
+
+func InitCache() {
+ Cache = cache.New(5*time.Minute, 10*time.Minute)
+}
diff --git a/app/appg/helper.go b/app/appg/helper.go
new file mode 100644
index 0000000..bfb2f85
--- /dev/null
+++ b/app/appg/helper.go
@@ -0,0 +1 @@
+package appg
diff --git a/app/conf/app.yaml b/app/conf/app.yaml
new file mode 100644
index 0000000..f2c8d06
--- /dev/null
+++ b/app/conf/app.yaml
@@ -0,0 +1,39 @@
+redis:
+ addr: "172.104.181.99:16789"
+ db: 3
+ passwd: "B6vJHlYiTseLj77z"
+ subscribe: true
+mongodb:
+ addr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_mdcom?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
+ logaddr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_mdcom_log?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
+es:
+ addr: "http://172.104.181.99:9200"
+ user: "elastic"
+ pass: "admin123456"
+httpOptions:
+ dcCtxTimeOut: 10
+jwtKey: "2311b15a4da1c80dc541b99abb622233"
+runMod: "debug"
+resKey: "vEukA&w15z4VAD3kAY#fkL#rBnU!WDhN"
+cdnKey: "vEukAw15z4VAD3kAYfkLrBnUWDhN123"
+tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
+tgToken: ""
+attack:
+ enable: false
+ appkey: "kaFtkDJRcchRMTI9"
+ h5key: "kaFttDJRcahRMTI7"
+ windowDurationSeconds: 60
+pprof: false
+host: "172.104.181.99"
+listenAddr: "25221"
+origDomain: "https://yuan.bbaazz.me/"
+# trustedProxies: "172.104.181.99"
+trustedProxies: ""
+bucketVideoAddressV2: "http://139.162.22.207:24996"
+adCenter:
+ apiDomain: "https://adopenapi.adcs01.top"
+ merchantCode: "1991129808640122880"
+ appCode: "JHA-1009"
+ aesKey: "gD4+rmbX5TS5cLbb3wAN5uEAYp/1j4hkQrS9SzuSjc8"
+dataReport:
+ url: "https://api.shuifeng.cc"
\ No newline at end of file
diff --git a/app/conf/ip2region_v4.xdb b/app/conf/ip2region_v4.xdb
new file mode 100644
index 0000000..6f86c7d
Binary files /dev/null and b/app/conf/ip2region_v4.xdb differ
diff --git a/app/conf/ip2region_v6.xdb b/app/conf/ip2region_v6.xdb
new file mode 100644
index 0000000..2e1dc6f
Binary files /dev/null and b/app/conf/ip2region_v6.xdb differ
diff --git a/app/conf/m3u8.key b/app/conf/m3u8.key
new file mode 100644
index 0000000..6cce2a7
--- /dev/null
+++ b/app/conf/m3u8.key
@@ -0,0 +1 @@
+—�ˆȒšÁC�„
\ No newline at end of file
diff --git a/app/conf/names.yaml b/app/conf/names.yaml
new file mode 100644
index 0000000..328bff2
--- /dev/null
+++ b/app/conf/names.yaml
@@ -0,0 +1,2605 @@
+familyNames: [
+ "王",
+ "李",
+ "张",
+ "刘",
+ "陈",
+ "杨",
+ "黄",
+ "吴",
+ "赵",
+ "周",
+ "徐",
+ "孙",
+ "马",
+ "朱",
+ "胡",
+ "林",
+ "郭",
+ "何",
+ "高",
+ "罗",
+ "郑",
+ "梁",
+ "谢",
+ "宋",
+ "唐",
+ "许",
+ "邓",
+ "冯",
+ "韩",
+ "曹",
+ "曾",
+ "彭",
+ "肖",
+ "蔡",
+ "潘",
+ "田",
+ "董",
+ "袁",
+ "于",
+ "余",
+ "蒋",
+ "叶",
+ "杜",
+ "苏",
+ "魏",
+ "程",
+ "吕",
+ "丁",
+ "沈",
+ "任",
+ "姚",
+ "卢",
+ "钟",
+ "姜",
+ "崔",
+ "谭",
+ "廖",
+ "范",
+ "汪",
+ "陆",
+ "金",
+ "石",
+ "戴",
+ "贾",
+ "韦",
+ "夏",
+ "邱",
+ "方",
+ "侯",
+ "邹",
+ "熊",
+ "孟",
+ "秦",
+ "白",
+ "毛",
+ "江",
+ "闫",
+ "薛",
+ "尹",
+ "付",
+ "段",
+ "雷",
+ "黎",
+ "史",
+ "龙",
+ "钱",
+ "贺",
+ "陶",
+ "顾",
+ "龚",
+ "郝",
+ "邵",
+ "万",
+ "严",
+ "洪",
+ "赖",
+ "武",
+ "傅",
+ "莫",
+ "孔",
+ "汤",
+ "向",
+ "常",
+ "温",
+ "康",
+ "施",
+ "文",
+ "牛",
+ "樊",
+ "葛",
+ "邢",
+ "安",
+ "齐",
+ "易",
+ "乔",
+ "伍",
+ "庞",
+ "颜",
+ "倪",
+ "庄",
+ "聂",
+ "章",
+ "鲁",
+ "岳",
+ "翟",
+ "申",
+ "殷",
+ "詹",
+ "欧",
+ "耿",
+ "关",
+ "覃",
+ "兰",
+ "焦",
+ "俞",
+ "左",
+ "柳",
+ "甘",
+ "祝",
+ "包",
+ "代",
+ "宁",
+ "符",
+ "阮",
+ "尚",
+ "舒",
+ "纪",
+ "柯",
+ "梅",
+ "童",
+ "毕",
+ "凌",
+ "单",
+ "季",
+ "成",
+ "霍",
+ "苗",
+ "裴",
+ "涂",
+ "谷",
+ "曲",
+ "盛",
+ "冉",
+ "翁",
+ "蓝",
+ "骆",
+ "路",
+ "游",
+ "靳",
+ "辛",
+ "欧阳",
+ "管",
+ "柴",
+ "蒙",
+ "鲍",
+ "华",
+ "喻",
+ "祁",
+ "房",
+ "蒲",
+ "滕",
+ "萧",
+ "屈",
+ "饶",
+ "解",
+ "牟",
+ "艾",
+ "尤",
+ "时",
+ "阳",
+ "阎",
+ "穆",
+ "应",
+ "农",
+ "司",
+ "古",
+ "吉",
+ "卓",
+ "车",
+ "简",
+ "连",
+ "缪",
+ "项",
+ "麦",
+ "褚",
+ "窦",
+ "娄",
+ "戚",
+ "岑",
+ "党",
+ "宫",
+ "景",
+ "卜",
+ "费",
+ "冷",
+ "晏",
+ "卫",
+ "席",
+ "柏",
+ "米",
+ "隋",
+ "宗",
+ "桂",
+ "瞿",
+ "全",
+ "苟",
+ "楼",
+ "闵",
+ "佟",
+ "臧",
+ "边",
+ "卞",
+ "姬",
+ "邬",
+ "和",
+ "师",
+ "仇",
+ "栾",
+ "丘",
+ "刁",
+ "沙",
+ "商",
+ "寇",
+ "荣",
+ "巫",
+ "郎",
+ "桑",
+ "丛",
+ "甄",
+ "敖",
+ "虞",
+ "仲",
+ "池",
+ "巩",
+ "明",
+ "佘",
+ "查",
+ "麻",
+ "苑",
+ "迟",
+ "邝",
+ "封",
+ "官",
+ "谈",
+ "鞠",
+ "匡",
+ "惠",
+ "荆",
+ "乐",
+ "冀",
+ "胥",
+ "郁",
+ "南",
+ "班",
+ "储",
+ "芦",
+ "原",
+ "栗",
+ "燕",
+ "楚",
+ "鄢",
+ "扬",
+ "劳",
+ "谌",
+ "奚",
+ "皮",
+ "蔺",
+ "粟",
+ "冼",
+ "盘",
+ "满",
+ "闻",
+ "厉",
+ "伊",
+ "候",
+ "仝",
+ "区",
+ "郜",
+ "海",
+ "阚",
+ "花",
+ "权",
+ "强",
+ "帅",
+ "屠",
+ "朴",
+ "盖",
+ "练",
+ "廉",
+ "禹",
+ "井",
+ "巴",
+ "漆",
+ "祖",
+ "丰",
+ "支",
+ "卿",
+ "国",
+ "狄",
+ "平",
+ "计",
+ "索",
+ "宣",
+ "晋",
+ "相",
+ "占",
+ "初",
+ "门",
+ "云",
+ "容",
+ "敬",
+ "来",
+ "扈",
+ "晁",
+ "都",
+ "芮",
+ "普",
+ "阙",
+ "戈",
+ "浦",
+ "伏",
+ "薄",
+ "鹿",
+ "邸",
+ "雍",
+ "辜",
+ "阿",
+ "羊",
+ "母",
+ "亓",
+ "裘",
+ "修",
+ "邰",
+ "杭",
+ "赫",
+ "况",
+ "那",
+ "宿",
+ "鲜",
+ "逯",
+ "印",
+ "隆",
+ "习",
+ "茹",
+ "诸",
+ "战",
+ "慕",
+ "危",
+ "玉",
+ "银",
+ "亢",
+ "公",
+ "嵇",
+ "豆",
+ "哈",
+ "湛",
+ "宾",
+ "勾",
+ "茅",
+ "戎",
+ "利",
+ "於",
+ "呼",
+ "居",
+ "干",
+ "揭",
+ "但",
+ "尉",
+ "斯",
+ "元",
+ "束",
+ "檀",
+ "衣",
+ "信",
+ "阴",
+ "展",
+ "昝",
+ "智",
+ "幸",
+ "奉",
+ "植",
+ "富",
+ "衡",
+ "昌",
+ "巢",
+ "郦",
+ "逄",
+ "寿",
+ "由",
+ "竺",
+ "贝",
+ "步",
+ "东",
+ "鄂",
+ "弓",
+ "过",
+ "钮",
+ "濮",
+ "水",
+ "蔚",
+ "郗",
+ "荀",
+ "贲",
+ "别",
+ "邴",
+ "才",
+ "操",
+ "畅",
+ "从",
+ "达",
+ "凡",
+ "凤",
+ "扶",
+ "贡",
+ "户",
+ "滑",
+ "郏",
+ "矫",
+ "经",
+ "靖",
+ "琚",
+ "巨",
+ "蒯",
+ "旷",
+ "历",
+ "励",
+ "伦",
+ "雒",
+ "茆",
+ "糜",
+ "宓",
+ "木",
+ "綦",
+ "谯",
+ "渠",
+ "山",
+ "生",
+ "侍",
+ "眭",
+ "腾",
+ "铁",
+ "宛",
+ "隗",
+ "沃",
+ "乌",
+ "毋",
+ "咸",
+ "忻",
+ "续",
+ "延",
+ "言",
+ "尧",
+ "英",
+ "鱼",
+ "庾",
+ "招",
+ "种",
+ "訾",
+ "宝",
+ "保",
+ "暴",
+ "闭",
+ "布",
+ "藏",
+ "承",
+ "福",
+ "果",
+ "黑",
+ "怀",
+ "宦",
+ "回",
+ "汲",
+ "籍",
+ "蹇",
+ "将",
+ "可",
+ "里",
+ "粱",
+ "冒",
+ "么",
+ "年",
+ "泮",
+ "郄",
+ "青",
+ "秋",
+ "释",
+ "双",
+ "税",
+ "潭",
+ "天",
+ "望",
+ "位",
+ "仵",
+ "香",
+ "刑",
+ "轩",
+ "玄",
+ "郇",
+ "要",
+ "宇",
+ "员",
+ "哀",
+ "爱",
+ "昂",
+ "熬",
+ "傲",
+ "奥",
+ "把",
+ "百",
+ "百里",
+ "摆",
+ "拜",
+ "邦",
+ "北",
+ "本",
+ "碧",
+ "薜",
+ "伯",
+ "博",
+ "补",
+ "部",
+ "采",
+ "彩",
+ "菜",
+ "仓",
+ "苍",
+ "曽",
+ "茶",
+ "产",
+ "苌",
+ "倡",
+ "唱",
+ "钞",
+ "超",
+ "朝",
+ "潮",
+ "沉",
+ "晨",
+ "呈",
+ "赤",
+ "敕",
+ "崇",
+ "瘳",
+ "丑",
+ "除",
+ "楮",
+ "揣",
+ "传",
+ "春",
+ "淳",
+ "淳于",
+ "啜",
+ "慈",
+ "次",
+ "爨",
+ "催",
+ "翠",
+ "寸",
+ "笪",
+ "答",
+ "丹",
+ "旦",
+ "淡",
+ "澹台",
+ "刀",
+ "道",
+ "德",
+ "迪",
+ "底",
+ "第",
+ "第五",
+ "佃",
+ "定",
+ "东方",
+ "冬",
+ "懂",
+ "钭",
+ "独孤",
+ "堵",
+ "端",
+ "端木",
+ "段干",
+ "敦",
+ "顿",
+ "多",
+ "朵",
+ "恩",
+ "尔",
+ "佴",
+ "法",
+ "番",
+ "藩",
+ "芳",
+ "飞",
+ "斐",
+ "风",
+ "酆",
+ "逢",
+ "俸",
+ "苻",
+ "洑",
+ "浮",
+ "甫",
+ "府",
+ "复",
+ "淦",
+ "刚",
+ "皋",
+ "杲",
+ "格",
+ "庚",
+ "公孙",
+ "公西",
+ "公羊",
+ "公冶",
+ "恭",
+ "拱",
+ "供",
+ "缑",
+ "菇",
+ "贯",
+ "冠",
+ "光",
+ "广",
+ "归",
+ "贵",
+ "呙",
+ "虢",
+ "还",
+ "寒",
+ "汉",
+ "行",
+ "蒿",
+ "好",
+ "昊",
+ "浩",
+ "禾",
+ "合",
+ "河",
+ "荷",
+ "赫连",
+ "恒",
+ "弘",
+ "红",
+ "宏",
+ "鸿",
+ "后",
+ "厚",
+ "呼延",
+ "忽",
+ "湖",
+ "虎",
+ "化",
+ "淮",
+ "槐",
+ "环",
+ "桓",
+ "皇",
+ "皇甫",
+ "辉",
+ "会",
+ "火",
+ "伙",
+ "基",
+ "稽",
+ "及",
+ "戢",
+ "记",
+ "继",
+ "暨",
+ "加",
+ "佳",
+ "家",
+ "嘉",
+ "荚",
+ "甲",
+ "菅",
+ "翦",
+ "见",
+ "建",
+ "剑",
+ "降",
+ "娇",
+ "徼",
+ "缴",
+ "教",
+ "接",
+ "节",
+ "杰",
+ "颉",
+ "介",
+ "京",
+ "静",
+ "酒",
+ "咎",
+ "菊",
+ "句",
+ "具",
+ "俱",
+ "剧",
+ "矍",
+ "军",
+ "君",
+ "俊",
+ "卡",
+ "开",
+ "凯",
+ "勘",
+ "堪",
+ "考",
+ "科",
+ "克",
+ "空",
+ "库",
+ "郐",
+ "奎",
+ "蒉",
+ "阔",
+ "喇",
+ "稂",
+ "朗",
+ "乐正",
+ "勒",
+ "蕾",
+ "类",
+ "冷狐",
+ "梨",
+ "礼",
+ "理",
+ "力",
+ "立",
+ "丽",
+ "莲",
+ "良",
+ "疗",
+ "寥",
+ "列",
+ "吝",
+ "泠",
+ "零",
+ "另",
+ "令",
+ "令狐",
+ "刘付",
+ "刘傅",
+ "留",
+ "庐",
+ "炉",
+ "禄",
+ "洛",
+ "闾",
+ "闾丘",
+ "侣",
+ "律",
+ "绿",
+ "买",
+ "曼",
+ "卯",
+ "枚",
+ "美",
+ "梦",
+ "弥",
+ "弭",
+ "秘",
+ "密",
+ "妙",
+ "闽",
+ "敏",
+ "名",
+ "谬",
+ "磨",
+ "墨",
+ "默",
+ "沐",
+ "牧",
+ "睦",
+ "慕容",
+ "纳",
+ "纳兰",
+ "娜",
+ "乃",
+ "南宫",
+ "南门",
+ "铙",
+ "能",
+ "尼",
+ "念",
+ "乜",
+ "甯",
+ "殴阳",
+ "鸥",
+ "偶",
+ "攀",
+ "朋",
+ "蓬",
+ "澎",
+ "骈",
+ "濮阳",
+ "溥",
+ "柒",
+ "亓官",
+ "其",
+ "奇",
+ "祈",
+ "耆",
+ "千",
+ "乾",
+ "潜",
+ "羌",
+ "钦",
+ "琴",
+ "勤",
+ "清",
+ "庆",
+ "求",
+ "璩",
+ "却",
+ "荛",
+ "绕",
+ "仁",
+ "日",
+ "融",
+ "如",
+ "汝",
+ "瑞",
+ "闰",
+ "润",
+ "若",
+ "撒",
+ "萨",
+ "赛",
+ "伞",
+ "森",
+ "厦",
+ "闪",
+ "陕",
+ "善",
+ "赏",
+ "上",
+ "上官",
+ "韶",
+ "卲",
+ "绍",
+ "厍",
+ "申屠",
+ "莘",
+ "神",
+ "甚",
+ "慎",
+ "升",
+ "绳",
+ "圣",
+ "诗",
+ "拾",
+ "矢",
+ "士",
+ "世",
+ "是",
+ "守",
+ "首",
+ "殳",
+ "书",
+ "疏",
+ "树",
+ "顺",
+ "司空",
+ "司寇",
+ "司马",
+ "司徒",
+ "思",
+ "松",
+ "送",
+ "素",
+ "睢",
+ "随",
+ "所",
+ "锁",
+ "塔",
+ "台",
+ "太",
+ "太史",
+ "太叔",
+ "泰",
+ "郯",
+ "塘",
+ "桃",
+ "淘",
+ "藤",
+ "提",
+ "遆",
+ "帖",
+ "通",
+ "同",
+ "徒",
+ "脱",
+ "陀",
+ "庹",
+ "拓",
+ "拓跋",
+ "完",
+ "完颜",
+ "万俟",
+ "旺",
+ "威",
+ "巍",
+ "伟",
+ "未",
+ "尉迟",
+ "闻人",
+ "问",
+ "瓮",
+ "巫马",
+ "无",
+ "吾",
+ "兀",
+ "西",
+ "西门",
+ "希",
+ "息",
+ "溪",
+ "袭",
+ "洗",
+ "喜",
+ "霞",
+ "夏侯",
+ "夏候",
+ "仙",
+ "先",
+ "鲜于",
+ "贤",
+ "线",
+ "湘",
+ "象",
+ "宵",
+ "箫",
+ "潇",
+ "晓",
+ "孝",
+ "校",
+ "星",
+ "兴",
+ "雄",
+ "吁",
+ "须",
+ "顼",
+ "绪",
+ "轩辕",
+ "禤",
+ "学",
+ "雪",
+ "寻",
+ "牙",
+ "雅",
+ "焉",
+ "岩",
+ "彦",
+ "艳",
+ "宴",
+ "洋",
+ "仰",
+ "养",
+ "样",
+ "幺",
+ "侥",
+ "药",
+ "冶",
+ "野",
+ "业",
+ "依",
+ "仪",
+ "夷",
+ "怡",
+ "宜",
+ "乙",
+ "蚁",
+ "弋",
+ "义",
+ "奕",
+ "羿",
+ "益",
+ "裔",
+ "翼",
+ "盈",
+ "营",
+ "永",
+ "勇",
+ "犹",
+ "油",
+ "友",
+ "有",
+ "迂",
+ "宇文",
+ "羽",
+ "遇",
+ "誉",
+ "毓",
+ "源",
+ "远",
+ "院",
+ "月",
+ "悦",
+ "越",
+ "允",
+ "运",
+ "恽",
+ "载",
+ "宰",
+ "迮",
+ "增",
+ "甑",
+ "粘",
+ "斩",
+ "长",
+ "长孙",
+ "仉",
+ "掌",
+ "钊",
+ "兆",
+ "肇",
+ "折",
+ "真",
+ "阵",
+ "镇",
+ "征",
+ "正",
+ "政",
+ "只",
+ "职",
+ "郅",
+ "治",
+ "中",
+ "忠",
+ "钟离",
+ "衷",
+ "仲孙",
+ "皱",
+ "邾",
+ "珠",
+ "诸葛",
+ "竹",
+ "主",
+ "颛孙",
+ "壮",
+ "禚",
+ "资",
+ "子",
+ "紫",
+ "自",
+ "字",
+ "宗政",
+ "纵",
+ "俎",
+ "左丘",
+ "佐"
+ ]
+middleNames: [
+ "小",
+ "晓",
+ "佳",
+ "海",
+ "嘉",
+ "云",
+ "雪",
+ "新",
+ "瑞",
+ "亚",
+ "丽",
+ "梦",
+ "雅",
+ "秀",
+ "欣",
+ "锦",
+ "梓",
+ "静",
+ "艳",
+ "若",
+ "慧",
+ "彦",
+ "艺",
+ "书",
+ "诗",
+ "淑",
+ "晨",
+ "凤",
+ "燕",
+ "怡",
+ "惠",
+ "乐",
+ "钰",
+ "利",
+ "敏",
+ "奕",
+ "悦",
+ "芳",
+ "鸿",
+ "丹",
+ "培",
+ "彩",
+ "颖",
+ "馨",
+ "琳",
+ "舒",
+ "雯",
+ "睿",
+ "宜",
+ "婉",
+ "香",
+ "亦",
+ "依",
+ "韵",
+ "如",
+ "玲",
+ "凡",
+ "素",
+ "巧",
+ "莉",
+ "洁",
+ "育",
+ "靖",
+ "宁",
+ "楚",
+ "卓",
+ "碧",
+ "翠",
+ "涵",
+ "倩",
+ "芝",
+ "珍",
+ "灵",
+ "语",
+ "凌",
+ "芸",
+ "妍",
+ "贞",
+ "笑",
+ "玮",
+ "煜",
+ "毓",
+ "婷",
+ "琦",
+ "琼",
+ "晶",
+ "仪",
+ "茗",
+ "昕",
+ "昱",
+ "真",
+ "曼",
+ "梅",
+ "虹",
+ "茜",
+ "璐",
+ "雁",
+ "菲",
+ "娅",
+ "聪",
+ "寒",
+ "沛",
+ "竹",
+ "欢",
+ "薇",
+ "莲",
+ "言",
+ "柔",
+ "瑾",
+ "勤",
+ "蓉",
+ "琪",
+ "娜",
+ "泓",
+ "莹",
+ "萍",
+ "瑜",
+ "焕",
+ "蕊",
+ "霞",
+ "娟",
+ "弘",
+ "晋",
+ "柳",
+ "枫",
+ "京",
+ "阳",
+ "珊",
+ "姿",
+ "滢",
+ "菁",
+ "群",
+ "岚",
+ "芬",
+ "映",
+ "霄",
+ "园",
+ "沁",
+ "朋",
+ "婕",
+ "蓓",
+ "岩",
+ "苑",
+ "琰",
+ "意",
+ "影",
+ "莎",
+ "荷",
+ "知",
+ "泳",
+ "妙",
+ "珠",
+ "迎",
+ "迪",
+ "忆",
+ "婵",
+ "念",
+ "娴",
+ "姣",
+ "宣",
+ "品",
+ "盈",
+ "烨",
+ "荔",
+ "芯",
+ "姗",
+ "越",
+ "歆",
+ "灿",
+ "珂",
+ "榕",
+ "曦",
+ "扬",
+ "洋",
+ "琛",
+ "容",
+ "轶",
+ "焱",
+ "晴",
+ "蕴",
+ "小",
+ "晓",
+ "佳",
+ "海",
+ "志",
+ "嘉",
+ "俊",
+ "云",
+ "新",
+ "瑞",
+ "宇",
+ "亚",
+ "伟",
+ "庆",
+ "欣",
+ "锦",
+ "梓",
+ "振",
+ "学",
+ "浩",
+ "若",
+ "宝",
+ "彦",
+ "艺",
+ "书",
+ "智",
+ "诗",
+ "旭",
+ "晨",
+ "乐",
+ "钰",
+ "承",
+ "铭",
+ "福",
+ "利",
+ "博",
+ "奕",
+ "树",
+ "悦",
+ "鸿",
+ "凯",
+ "培",
+ "睿",
+ "宜",
+ "恩",
+ "亦",
+ "健",
+ "胜",
+ "飞",
+ "远",
+ "如",
+ "鹏",
+ "凡",
+ "哲",
+ "育",
+ "富",
+ "靖",
+ "宁",
+ "朝",
+ "楚",
+ "卓",
+ "茂",
+ "杰",
+ "松",
+ "涵",
+ "耀",
+ "善",
+ "柏",
+ "超",
+ "昊",
+ "灵",
+ "勇",
+ "凌",
+ "炳",
+ "盛",
+ "裕",
+ "玮",
+ "煜",
+ "毓",
+ "琦",
+ "茗",
+ "昕",
+ "昱",
+ "毅",
+ "奇",
+ "皓",
+ "喜",
+ "聪",
+ "才",
+ "寒",
+ "沛",
+ "增",
+ "欢",
+ "言",
+ "炜",
+ "勤",
+ "辉",
+ "丰",
+ "琪",
+ "泓",
+ "锐",
+ "业",
+ "彬",
+ "焕",
+ "弘",
+ "跃",
+ "晋",
+ "枫",
+ "京",
+ "阳",
+ "同",
+ "翔",
+ "钧",
+ "群",
+ "添",
+ "泰",
+ "霄",
+ "亮",
+ "宸",
+ "朋",
+ "诚",
+ "腾",
+ "山",
+ "岩",
+ "斌",
+ "鸣",
+ "意",
+ "森",
+ "知",
+ "翰",
+ "泳",
+ "存",
+ "迎",
+ "迪",
+ "威",
+ "照",
+ "骏",
+ "峰",
+ "展",
+ "栋",
+ "念",
+ "豪",
+ "功",
+ "均",
+ "宣",
+ "品",
+ "晟",
+ "绪",
+ "波",
+ "瀚",
+ "烨",
+ "宪",
+ "航",
+ "帅",
+ "越",
+ "歆",
+ "峻",
+ "灿",
+ "岳",
+ "珂",
+ "榕",
+ "曦",
+ "泉",
+ "扬",
+ "洋",
+ "琛",
+ "轶",
+ "焱",
+ "涛",
+ "城",
+ "令",
+ "坚",
+ "升",
+ "夕",
+ "河",
+ "誉",
+ "祺",
+ "理",
+ "磊",
+ "蔚",
+ "小",
+ "晓",
+ "佳",
+ "海",
+ "志",
+ "嘉",
+ "俊",
+ "云",
+ "雪",
+ "新",
+ "瑞",
+ "宇",
+ "亚",
+ "丽",
+ "梦",
+ "伟",
+ "雅",
+ "庆",
+ "秀",
+ "欣",
+ "锦",
+ "梓",
+ "振",
+ "学",
+ "浩",
+ "静",
+ "艳",
+ "若",
+ "慧",
+ "宝",
+ "彦",
+ "艺",
+ "书",
+ "智",
+ "诗",
+ "淑",
+ "旭",
+ "晨",
+ "凤",
+ "燕",
+ "怡",
+ "惠",
+ "乐",
+ "钰",
+ "承",
+ "铭",
+ "福",
+ "利",
+ "敏",
+ "博",
+ "奕",
+ "树",
+ "悦",
+ "芳",
+ "鸿",
+ "凯",
+ "丹",
+ "培",
+ "彩",
+ "颖",
+ "馨",
+ "琳",
+ "舒",
+ "雯",
+ "睿",
+ "宜",
+ "恩",
+ "婉",
+ "香",
+ "亦",
+ "健",
+ "胜",
+ "飞",
+ "远",
+ "依",
+ "韵",
+ "如",
+ "鹏",
+ "玲",
+ "凡",
+ "素",
+ "巧",
+ "哲",
+ "莉",
+ "洁",
+ "育",
+ "富",
+ "靖",
+ "宁",
+ "朝",
+ "楚",
+ "卓",
+ "茂",
+ "碧",
+ "杰",
+ "翠",
+ "松",
+ "涵",
+ "耀",
+ "倩",
+ "善",
+ "柏",
+ "芝",
+ "珍",
+ "超",
+ "昊",
+ "灵",
+ "语",
+ "勇",
+ "凌",
+ "芸",
+ "妍",
+ "炳",
+ "贞",
+ "盛",
+ "笑",
+ "裕",
+ "玮",
+ "煜",
+ "毓",
+ "婷",
+ "琦",
+ "琼",
+ "晶",
+ "仪",
+ "茗",
+ "昕",
+ "昱",
+ "真",
+ "曼",
+ "毅",
+ "梅",
+ "虹",
+ "茜",
+ "奇",
+ "璐",
+ "皓",
+ "雁",
+ "菲",
+ "娅",
+ "喜",
+ "聪",
+ "才",
+ "寒",
+ "沛",
+ "竹",
+ "增",
+ "欢",
+ "薇",
+ "莲",
+ "言",
+ "炜",
+ "柔",
+ "瑾",
+ "勤",
+ "辉",
+ "丰",
+ "蓉",
+ "琪",
+ "娜",
+ "泓",
+ "莹",
+ "萍",
+ "瑜",
+ "锐",
+ "业",
+ "彬",
+ "焕",
+ "蕊",
+ "霞",
+ "娟",
+ "弘",
+ "跃",
+ "晋",
+ "柳",
+ "枫",
+ "京",
+ "阳",
+ "同",
+ "翔",
+ "钧",
+ "珊",
+ "姿",
+ "滢",
+ "菁",
+ "群",
+ "岚",
+ "芬",
+ "添",
+ "映",
+ "泰",
+ "霄",
+ "园",
+ "亮",
+ "宸",
+ "沁",
+ "朋",
+ "婕",
+ "诚",
+ "蓓",
+ "腾",
+ "山",
+ "岩",
+ "斌",
+ "苑",
+ "鸣",
+ "琰",
+ "意",
+ "森",
+ "影",
+ "莎",
+ "荷",
+ "知",
+ "翰",
+ "泳",
+ "妙",
+ "存",
+ "珠",
+ "迎",
+ "迪",
+ "威",
+ "忆",
+ "照",
+ "骏",
+ "婵",
+ "峰",
+ "展",
+ "栋",
+ "念",
+ "娴"
+ ]
+afterNames: [
+ "静",
+ "敏",
+ "丹",
+ "丽",
+ "燕",
+ "婷",
+ "娟",
+ "艳",
+ "芳",
+ "娜",
+ "玲",
+ "洋",
+ "洁",
+ "佳",
+ "琳",
+ "雪",
+ "阳",
+ "颖",
+ "莉",
+ "慧",
+ "萍",
+ "倩",
+ "宁",
+ "莹",
+ "霞",
+ "云",
+ "晶",
+ "欣",
+ "晨",
+ "欢",
+ "乐",
+ "璐",
+ "梅",
+ "瑞",
+ "琪",
+ "琦",
+ "岩",
+ "海",
+ "琼",
+ "迪",
+ "聪",
+ "菲",
+ "珊",
+ "薇",
+ "蓉",
+ "凡",
+ "茜",
+ "悦",
+ "雯",
+ "扬",
+ "睿",
+ "瑜",
+ "萌",
+ "妍",
+ "群",
+ "娇",
+ "玮",
+ "虹",
+ "凤",
+ "珍",
+ "梦",
+ "越",
+ "枫",
+ "怡",
+ "蕊",
+ "畅",
+ "芬",
+ "靖",
+ "惠",
+ "硕",
+ "柳",
+ "晴",
+ "莎",
+ "曦",
+ "昕",
+ "艺",
+ "烨",
+ "婕",
+ "朋",
+ "涵",
+ "瑾",
+ "灿",
+ "芸",
+ "岚",
+ "钰",
+ "嘉",
+ "曼",
+ "捷",
+ "彤",
+ "卓",
+ "珂",
+ "彦",
+ "真",
+ "菁",
+ "培",
+ "容",
+ "瑛",
+ "馨",
+ "灵",
+ "雅",
+ "鸿",
+ "园",
+ "凌",
+ "翠",
+ "琛",
+ "煜",
+ "锦",
+ "勤",
+ "霖",
+ "秀",
+ "蓓",
+ "香",
+ "寒",
+ "言",
+ "斐",
+ "奕",
+ "巧",
+ "焱",
+ "莲",
+ "蔚",
+ "姗",
+ "姣",
+ "雁",
+ "舒",
+ "娅",
+ "韵",
+ "琰",
+ "桐",
+ "昱",
+ "盈",
+ "婵",
+ "娴",
+ "竹",
+ "臻",
+ "媚",
+ "霄",
+ "仪",
+ "榕",
+ "贞",
+ "依",
+ "珠",
+ "滢",
+ "焕",
+ "婉",
+ "沁",
+ "弘",
+ "芝",
+ "语",
+ "淇",
+ "诗",
+ "茗",
+ "宜",
+ "彩",
+ "宣",
+ "素",
+ "毓",
+ "碧",
+ "云",
+ "芳",
+ "玲",
+ "萍",
+ "婷",
+ "丽",
+ "阳",
+ "梅",
+ "霞",
+ "敏",
+ "宁",
+ "琳",
+ "娟",
+ "燕",
+ "佳",
+ "欣",
+ "涵",
+ "慧",
+ "珍",
+ "海",
+ "娜",
+ "艳",
+ "静",
+ "雪",
+ "洁",
+ "瑞",
+ "颖",
+ "香",
+ "秀",
+ "琪",
+ "凤",
+ "洋",
+ "雅",
+ "丹",
+ "菲",
+ "怡",
+ "新",
+ "雯",
+ "琦",
+ "嘉",
+ "莹",
+ "芬",
+ "莉",
+ "乐",
+ "利",
+ "凡",
+ "晨",
+ "莲",
+ "蓉",
+ "仪",
+ "霖",
+ "睿",
+ "悦",
+ "馨",
+ "琼",
+ "妍",
+ "珊",
+ "彤",
+ "芝",
+ "芸",
+ "惠",
+ "倩",
+ "聪",
+ "瑜",
+ "群",
+ "灵",
+ "晓",
+ "薇",
+ "茜",
+ "玮",
+ "锦",
+ "欢",
+ "枫",
+ "钰",
+ "艺",
+ "岚",
+ "如",
+ "真",
+ "竹",
+ "宜",
+ "鸿",
+ "蕊",
+ "晶",
+ "梦",
+ "璐",
+ "珠",
+ "亚",
+ "柔",
+ "娴",
+ "翠",
+ "寒",
+ "虹",
+ "勤",
+ "娇",
+ "韵",
+ "贞",
+ "晴",
+ "茗",
+ "扬",
+ "婕",
+ "影",
+ "容",
+ "言",
+ "瑾",
+ "桐",
+ "迪",
+ "舒",
+ "蕾",
+ "淇",
+ "巧",
+ "岩",
+ "莎",
+ "彦",
+ "璇",
+ "滢",
+ "雁",
+ "园",
+ "朋",
+ "淑",
+ "彩",
+ "昕",
+ "萌",
+ "烨",
+ "菁",
+ "姣",
+ "榕",
+ "曼",
+ "语",
+ "书",
+ "娅",
+ "媚",
+ "蓓",
+ "婉",
+ "琛",
+ "曦",
+ "姿",
+ "毓",
+ "凌",
+ "姗",
+ "荷",
+ "婵",
+ "育",
+ "捷",
+ "煜",
+ "盈",
+ "越",
+ "琰",
+ "霄",
+ "依",
+ "碧",
+ "霜",
+ "斐",
+ "卓",
+ "培",
+ "灿",
+ "素",
+ "弘",
+ "畅",
+ "诗",
+ "宣",
+ "瑛",
+ "亭",
+ "苑",
+ "溪",
+ "荔",
+ "原",
+ "靖",
+ "臻",
+ "硕",
+ "若",
+ "茵",
+ "微",
+ "奕",
+ "意",
+ "珂",
+ "芯",
+ "铃",
+ "柳",
+ "淳",
+ "焱",
+ "伟",
+ "磊",
+ "超",
+ "杰",
+ "涛",
+ "鹏",
+ "勇",
+ "斌",
+ "飞",
+ "波",
+ "辉",
+ "亮",
+ "洋",
+ "浩",
+ "凯",
+ "俊",
+ "峰",
+ "佳",
+ "阳",
+ "健",
+ "宇",
+ "宁",
+ "云",
+ "旭",
+ "欣",
+ "彬",
+ "博",
+ "晨",
+ "欢",
+ "乐",
+ "毅",
+ "威",
+ "翔",
+ "瑞",
+ "琪",
+ "琦",
+ "岩",
+ "海",
+ "松",
+ "迪",
+ "聪",
+ "凡",
+ "悦",
+ "哲",
+ "昊",
+ "锐",
+ "扬",
+ "睿",
+ "群",
+ "玮",
+ "航",
+ "庆",
+ "越",
+ "枫",
+ "畅",
+ "森",
+ "炜",
+ "振",
+ "靖",
+ "奇",
+ "智",
+ "铭",
+ "栋",
+ "硕",
+ "胜",
+ "曦",
+ "昕",
+ "艺",
+ "烨",
+ "朋",
+ "涵",
+ "灿",
+ "诚",
+ "腾",
+ "钰",
+ "嘉",
+ "捷",
+ "卓",
+ "豪",
+ "远",
+ "宝",
+ "珂",
+ "彦",
+ "坚",
+ "培",
+ "灵",
+ "鸿",
+ "皓",
+ "骏",
+ "凌",
+ "昆",
+ "琛",
+ "煜",
+ "泉",
+ "跃",
+ "锦",
+ "勤",
+ "霖",
+ "鸣",
+ "耀",
+ "寒",
+ "言",
+ "旺",
+ "斐",
+ "城",
+ "奕",
+ "盛",
+ "焱",
+ "谦",
+ "渊",
+ "蔚",
+ "桐",
+ "昱",
+ "晟",
+ "岳",
+ "臻",
+ "霄",
+ "茂",
+ "喜",
+ "祺",
+ "榕",
+ "钧",
+ "恩",
+ "焕",
+ "富",
+ "弘",
+ "添",
+ "裕",
+ "淇",
+ "均",
+ "棋",
+ "瀚",
+ "宸",
+ "诗",
+ "茗",
+ "宜",
+ "宣",
+ "翰",
+ "柏",
+ "毓",
+ "杰",
+ "宇",
+ "云",
+ "伟",
+ "辉",
+ "峰",
+ "飞",
+ "阳",
+ "宁",
+ "波",
+ "涛",
+ "超",
+ "佳",
+ "斌",
+ "欣",
+ "鹏",
+ "涵",
+ "海",
+ "亮",
+ "锋",
+ "铭",
+ "瑞",
+ "彬",
+ "琪",
+ "洋",
+ "勇",
+ "新",
+ "浩",
+ "博",
+ "琦",
+ "俊",
+ "翔",
+ "嘉",
+ "豪",
+ "松",
+ "乐",
+ "利",
+ "凡",
+ "山",
+ "晨",
+ "哲",
+ "凯",
+ "霖",
+ "睿",
+ "悦",
+ "志",
+ "旭",
+ "奇",
+ "磊",
+ "聪",
+ "群",
+ "宝",
+ "灵",
+ "晓",
+ "胜",
+ "庆",
+ "玮",
+ "锦",
+ "福",
+ "毅",
+ "森",
+ "欢",
+ "枫",
+ "钰",
+ "艺",
+ "航",
+ "健",
+ "才",
+ "权",
+ "远",
+ "如",
+ "泉",
+ "宜",
+ "鸿",
+ "威",
+ "富",
+ "亚",
+ "诚",
+ "鸣",
+ "寒",
+ "智",
+ "城",
+ "勤",
+ "昊",
+ "茗",
+ "扬",
+ "丰",
+ "洲",
+ "栋",
+ "恩",
+ "言",
+ "桐",
+ "迪",
+ "淇",
+ "钧",
+ "岩",
+ "彦",
+ "盛",
+ "学",
+ "宸",
+ "旺",
+ "朋",
+ "喜",
+ "皓",
+ "昕",
+ "升",
+ "烨",
+ "炜",
+ "锐",
+ "榕",
+ "祺",
+ "帅",
+ "书",
+ "琛",
+ "曦",
+ "毓",
+ "耀",
+ "凌",
+ "坚",
+ "育",
+ "翰",
+ "均",
+ "骏",
+ "捷",
+ "煜",
+ "振",
+ "腾",
+ "越",
+ "霄",
+ "茂",
+ "财",
+ "棋",
+ "谦",
+ "河",
+ "斐",
+ "业",
+ "泰",
+ "裕",
+ "卓",
+ "培",
+ "灿",
+ "善",
+ "弘",
+ "树",
+ "畅",
+ "诗",
+ "钊",
+ "宣",
+ "亭",
+ "渊",
+ "溪",
+ "昆",
+ "原",
+ "靖",
+ "柏",
+ "臻",
+ "硕",
+ "若",
+ "奕",
+ "承",
+ "意",
+ "珂",
+ "通",
+ "瀚",
+ "朝",
+ "朗",
+ "韬",
+ "跃",
+ "淳",
+ "同",
+ "岳",
+ "焱",
+ "晟",
+ "蔚",
+ "昱",
+ "衡",
+ "泓",
+ "添",
+ "恺",
+ "功",
+ "林",
+ "龙",
+ "英",
+ "芳",
+ "红",
+ "丽",
+ "阳",
+ "宁",
+ "清",
+ "琳",
+ "涛",
+ "燕",
+ "春",
+ "青",
+ "艳",
+ "铭",
+ "雨",
+ "菲",
+ "新",
+ "雯",
+ "元",
+ "楠",
+ "莹",
+ "莉",
+ "萱",
+ "霖",
+ "辰",
+ "花",
+ "彤",
+ "群",
+ "茹",
+ "森",
+ "钰",
+ "岚",
+ "如",
+ "竹",
+ "蕊",
+ "鸣",
+ "翠",
+ "晴",
+ "虎",
+ "叶",
+ "蕾",
+ "莎",
+ "宸",
+ "杨",
+ "萌",
+ "菁",
+ "榕",
+ "雷",
+ "枝",
+ "蓓",
+ "霄",
+ "碧",
+ "泰",
+ "蓝",
+ "霏",
+ "树",
+ "莺",
+ "瑛",
+ "乔",
+ "原",
+ "芷",
+ "茵",
+ "卉",
+ "苗",
+ "桃",
+ "初",
+ "柳",
+ "木",
+ "苹",
+ "震",
+ "音",
+ "遥",
+ "柯",
+ "蒙",
+ "姝",
+ "菱",
+ "苏",
+ "征",
+ "樱",
+ "苇",
+ "庚",
+ "棠",
+ "迎",
+ "鹃",
+ "杏",
+ "苓",
+ "珑",
+ "煦",
+ "暖",
+ "澎",
+ "荟",
+ "霁",
+ "宵",
+ "朔",
+ "旦",
+ "浩",
+ "泽",
+ "洋",
+ "江",
+ "洪",
+ "涵",
+ "彦",
+ "源",
+ "蓉",
+ "莲",
+ "昊",
+ "风",
+ "水",
+ "薇",
+ "茜",
+ "虹",
+ "扬",
+ "川",
+ "润",
+ "泉",
+ "益",
+ "潇",
+ "淼",
+ "湘",
+ "夏",
+ "淇",
+ "禹",
+ "盈",
+ "沛",
+ "泓",
+ "童",
+ "滨",
+ "满",
+ "瀚",
+ "乾",
+ "鹤",
+ "菡",
+ "河",
+ "荷",
+ "溪",
+ "泳",
+ "津",
+ "韬",
+ "芙",
+ "玫",
+ "衍",
+ "研",
+ "沐",
+ "芊",
+ "淋",
+ "澄",
+ "池",
+ "潮",
+ "滔",
+ "陆",
+ "端",
+ "汐",
+ "游",
+ "漪",
+ "潼",
+ "泊",
+ "罡",
+ "淞",
+ "潞",
+ "汀",
+ "涓",
+ "宇",
+ "辉",
+ "萍",
+ "霞",
+ "荣",
+ "兰",
+ "庆",
+ "旭",
+ "秋",
+ "月",
+ "晨",
+ "星",
+ "琪",
+ "桂",
+ "香",
+ "翔",
+ "芬",
+ "琼",
+ "鸿",
+ "贵",
+ "恒",
+ "羽",
+ "娇",
+ "希",
+ "灵",
+ "耀",
+ "菊",
+ "露",
+ "朝",
+ "巧",
+ "熙",
+ "芸",
+ "丰",
+ "炜",
+ "皓",
+ "枫",
+ "煜",
+ "楚",
+ "烨",
+ "昭",
+ "灿",
+ "炳",
+ "爽",
+ "昱",
+ "影",
+ "桐",
+ "雁",
+ "晖",
+ "炎",
+ "焕",
+ "圆",
+ "祺",
+ "姣",
+ "硕",
+ "齐",
+ "定",
+ "婵",
+ "凝",
+ "焱",
+ "琰",
+ "照",
+ "果",
+ "桦",
+ "棋",
+ "霜",
+ "珏",
+ "晔",
+ "壮",
+ "申",
+ "驰",
+ "望",
+ "朗",
+ "衡",
+ "煌",
+ "栩",
+ "旻",
+ "烁",
+ "禾",
+ "实",
+ "鹰",
+ "皎",
+ "谷",
+ "烽",
+ "昶",
+ "骐",
+ "迅",
+ "嫦",
+ "穗",
+ "焰",
+ "耘",
+ "葵",
+ "荧",
+ "宙",
+ "曜",
+ "东",
+ "雪",
+ "梅",
+ "慧",
+ "琴",
+ "松",
+ "冰",
+ "坤",
+ "冬",
+ "银",
+ "栋",
+ "素",
+ "凌",
+ "柏",
+ "寒",
+ "坚",
+ "易",
+ "咏",
+ "白",
+ "严",
+ "玄"
+ ]
diff --git a/app/deploy_prod.sh b/app/deploy_prod.sh
new file mode 100755
index 0000000..5978860
--- /dev/null
+++ b/app/deploy_prod.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+echo "开始"
+rm -rf ./web_hghl_app ./web_hghl_app.tar.gz
+GOOS=linux GOARCH=amd64 go build -o web_hghl_app
+tar zvcf web_hghl_app.tar.gz web_hghl_app
+
+
+scp web_hghl_app.tar.gz root@172.104.166.87:/data/uzip
+ssh root@172.104.166.87 "/server/web_hghl/app/up_app.sh"
+
+rm -rf ./web_hghl_app ./web_hghl_app.tar.gz
\ No newline at end of file
diff --git a/app/docs/docs.go b/app/docs/docs.go
new file mode 100644
index 0000000..dbdfca7
--- /dev/null
+++ b/app/docs/docs.go
@@ -0,0 +1,6740 @@
+// Package swag_app_docs Code generated by swaggo/swag. DO NOT EDIT
+package swag_app_docs
+
+import "github.com/swaggo/swag"
+
+const docTemplate = `{
+ "schemes": {{ marshal .Schemes }},
+ "swagger": "2.0",
+ "info": {
+ "description": "{{escape .Description}}",
+ "title": "{{.Title}}",
+ "contact": {},
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "{{.Version}}"
+ },
+ "host": "{{.Host}}",
+ "basePath": "{{.BasePath}}",
+ "paths": {
+ "/api/app/actress/list": {
+ "get": {
+ "description": "返回导航、广告和按热度值降序排列的女优列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优相关接口"
+ ],
+ "summary": "女优榜单列表 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.QueryActressListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/actress/work/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "查询指定女优详情和作品列表,支持上架时间/观看量/收藏量升降序,每间隔8条作品数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优相关接口"
+ ],
+ "summary": "指定女优详情和作品列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "上架时间升序/降序(1-升序 2降序)",
+ "name": "addedTimeSort",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "女优ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "按收藏(点赞)量升序/降序(1-升序 2降序)",
+ "name": "likesSort",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "女优slug,非空时优先按slug解析女优ID",
+ "name": "slug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "按观看量升序/降序(1-升序 2降序)",
+ "name": "watchTimesSort",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryActressWorkListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoListItem"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "VideoInfo": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/applist/list": {
+ "get": {
+ "description": "返回应用分类列表,每个分类下含应用信息列表;categoryId=0 返回第一个分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用列表"
+ ],
+ "summary": "查询应用列表",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "当前分类ID,0表示默认第一个分类",
+ "name": "categoryId",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/getInfo": {
+ "get": {
+ "description": "根据当前登录 token 查询用户基础信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "获取当前登录用户信息",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/authser.UserInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/login": {
+ "post": {
+ "description": "账号均可作为登录凭证",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "登录",
+ "parameters": [
+ {
+ "description": "登录参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.loginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "登录成功,返回 token 及用户基本信息",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/profile": {
+ "post": {
+ "description": "账号必填;avatar、oldPassword+newPassword 均为可选,至少提供一项。修改密码时旧密码和新密码必须同时提供。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "修改用户资料(头像 / 密码,可任意组合)",
+ "parameters": [
+ {
+ "description": "修改资料参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.updateProfileReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/register": {
+ "post": {
+ "description": "已登录游客传入账号密码完成升级;未登录用户直接注册新账号。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "注册",
+ "parameters": [
+ {
+ "description": "注册参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.registerReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "注册成功,返回 token 及用户基本信息",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/list": {
+ "get": {
+ "description": "按对象查询树形评论列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论APP"
+ ],
+ "summary": "评论列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "对象ID",
+ "name": "objectId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 4
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "CommentObjectTypePlaylet": "短剧",
+ "CommentObjectTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "CommentObjectTypeVideo",
+ "CommentObjectTypePlaylet"
+ ],
+ "description": "对象类型 0=视频 4=短剧",
+ "name": "objectType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.CommentListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/send": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "发布评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论APP"
+ ],
+ "summary": "发布评论 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.CommentSendReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/config/footer/info": {
+ "get": {
+ "description": "返回APP底部品牌、底部导航和联系方式",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app配置相关接口"
+ ],
+ "summary": "查询APP底部信息 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FooterInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/config/richtext/list": {
+ "get": {
+ "description": "返回启用状态的富文本内容,支持按ids或titles过滤",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app配置相关接口"
+ ],
+ "summary": "查询APP富文本内容列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "富文本ID,多个用英文逗号分隔",
+ "name": "ids",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "富文本标题,多个用英文逗号分隔",
+ "name": "titles",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppRichTextListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/content/tag/list": {
+ "get": {
+ "description": "返回启用且有绑定内容数量的统一标签,contentTagType非必填,按绑定内容数量倒序排序",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app配置相关接口"
+ ],
+ "summary": "查询APP统一标签列表 -- by 王鸣",
+ "parameters": [
+ {
+ "enum": [
+ 1,
+ 5
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ],
+ "description": "内容类型 1-视频 5-短剧,不传返回全部有绑定数量的标签",
+ "name": "contentTagType",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppContentTagListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/home": {
+ "get": {
+ "description": "首页分类数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "首页"
+ ],
+ "summary": "首页 --by 旺鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.DataResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/home/base": {
+ "get": {
+ "description": "只返回翻译、配置、底部翻译、广告和公告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "首页"
+ ],
+ "summary": "首页公共数据 --by 旺鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBaseResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/home/tags": {
+ "get": {
+ "description": "返回视频、短剧的标签信息,以及基础导航、配置、广告和公告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "首页"
+ ],
+ "summary": "首页标签数据 --by 旺鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeTagsResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/login/guest": {
+ "post": {
+ "description": "按设备 ID 查找已有游客账号,找不到则自动注册。若请求携带有效 token 则优先复用该用户。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "游客登录",
+ "parameters": [
+ {
+ "description": "设备信息",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.guestLoginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "登录成功",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/navigation/header": {
+ "get": {
+ "description": "查询导航类型为URL模式和二级导航的头部导航配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app导航相关接口"
+ ],
+ "summary": "查询头部导航配置 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/info": {
+ "get": {
+ "description": "根据剧集ID查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集详情 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "剧集ID",
+ "name": "id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryPlayletChapterInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "chapterInfo": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ },
+ "chapterList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "hotPlayletList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "playletInfo": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/info": {
+ "get": {
+ "description": "查询短剧详情、剧集列表、热门短剧列表和点赞状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "短剧详情 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "slug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryPlayletInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "chapterList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "hotPlayletList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "playletInfo": {
+ "$ref": "#/definitions/proto.PlayletDetailInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/like": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户点赞或取消点赞指定短剧,isLike=true表示点赞,isLike=false表示取消点赞",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "点赞或取消点赞短剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "点赞或取消点赞请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.LikePlayletReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LikePlayletResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/rank/list": {
+ "get": {
+ "description": "分页查询短剧排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "短剧排行榜分页列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜",
+ "name": "rankPeriod",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRankResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletRankData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/recommend/list": {
+ "get": {
+ "description": "分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端推荐短剧列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧分类ID,不传查询全部分类",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类slug,非空时优先按slug解析分类ID",
+ "name": "categorySlug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "排序方式 1-推荐 2-最新 3-最热",
+ "name": "sortList",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "标签ID,不传查询全部标签",
+ "name": "tagId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签slug,非空时优先按slug解析标签ID",
+ "name": "tagSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/tag/{id}": {
+ "get": {
+ "description": "根据统一标签ID分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据标签ID查询短剧列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量,默认20,最大100",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/questionnaire/submit": {
+ "post": {
+ "description": "用户提交问题反馈问卷",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "问卷调查APP"
+ ],
+ "summary": "提交问卷",
+ "parameters": [
+ {
+ "description": "提交参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.QuestionnaireSubmitReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/richtext": {
+ "get": {
+ "description": "从缓存中查询启用状态的富文本信息",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本"
+ ],
+ "summary": "按 title 查询富文本",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "富文本内部标题",
+ "name": "title",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.RichTextInfo"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/upload/image": {
+ "post": {
+ "description": "将图片转发至存储服务 /media/web/autopublish/image",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "上传"
+ ],
+ "summary": "图片上传",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "图片文件",
+ "name": "file",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.ImageUploadResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/upload/videoBody": {
+ "post": {
+ "description": "将视频分片转发至存储服务 /media/web/autopublish/videoBody",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "上传"
+ ],
+ "summary": "视频分片上传",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "视频任务ID",
+ "name": "taskId",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "当前分片序号,从0开始",
+ "name": "pos",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "上传用户名,为空默认 questionnaires",
+ "name": "uploadUserName",
+ "in": "formData"
+ },
+ {
+ "type": "file",
+ "description": "分片文件数据",
+ "name": "file",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.VideoBodyResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/upload/videoOk": {
+ "post": {
+ "description": "所有分片上传完毕后调用,转发至存储服务 /media/web/autopublish/videoOk",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "上传"
+ ],
+ "summary": "视频分片上传完成通知",
+ "parameters": [
+ {
+ "description": "{ \\",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.VideoOkResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/favorite": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户收藏或取消收藏指定视频,isFavorite=true表示收藏,isFavorite=false表示取消收藏",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "收藏或取消收藏视频 --by 旺鸣",
+ "parameters": [
+ {
+ "description": "收藏或取消收藏请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.FavoriteVideoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FavoriteVideoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/favorite/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户收藏视频分页列表,按收藏时间倒序排序",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "当前登录用户收藏视频分页列表 --by 旺鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryFavoriteVideoListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.FavoriteVideoListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/info": {
+ "get": {
+ "description": "查询视频详情、热门视频列表、收藏状态和点赞状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "视频详情 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "视频slug,非空时优先按slug解析视频ID",
+ "name": "slug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "hotVideoList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoData"
+ }
+ },
+ "videoInfo": {
+ "$ref": "#/definitions/proto.VideoDetailInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/like": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户点赞或取消点赞指定视频,isLike=true表示点赞,isLike=false表示取消点赞",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "点赞或取消点赞视频 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "点赞或取消点赞请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.LikeVideoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LikeVideoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/more": {
+ "get": {
+ "description": "根据分类id查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "根据分类id查询视频列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "视频分类id",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类slug,非空时优先按slug解析分类ID",
+ "name": "categorySlug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoListItem"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "VideoInfo": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/rank": {
+ "get": {
+ "description": "视频收藏排行榜",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "视频收藏排行榜",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.RankResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/rank/list": {
+ "get": {
+ "description": "分页查询视频排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "视频排行榜分页列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜",
+ "name": "rankPeriod",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRankResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/recommend/list": {
+ "get": {
+ "description": "分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "app端推荐视频列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "分类id,不传查询全部分类id",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类slug,非空时优先按slug解析分类ID",
+ "name": "categorySlug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "排序方式 1-推荐 2-最新 3-最热",
+ "name": "sortList",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "标签ID,不传查询全部标签",
+ "name": "tagId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签slug,非空时优先按slug解析标签ID",
+ "name": "tagSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/tag/{id}": {
+ "get": {
+ "description": "根据统一标签ID分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "app端根据标签ID查询视频列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量,默认20,最大100",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/businessCooperation": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "导航",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航"
+ ],
+ "summary": "导航",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/consulting": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "招募",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "招募"
+ ],
+ "summary": "招募",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/consultinginfo/:id": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "招募详情",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "招募详情"
+ ],
+ "summary": "招募详情",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/dh": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "导航",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航"
+ ],
+ "summary": "导航",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.DHHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/dmca": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "dmca",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "dmca"
+ ],
+ "summary": "dmca",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/privacyPolicy": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "隐私策略",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "隐私策略"
+ ],
+ "summary": "隐私策略",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/search": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "搜索,支持查询参数:title=关键词\u0026page=1\u0026type=0\u0026sort=default(不传type=全部;type: 0=视频 1=女优 3=AV 6=短剧;sort: default/hot/new)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "搜索"
+ ],
+ "summary": "搜索",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "搜索关键词",
+ "name": "title",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "媒体类型,不传=全部,0=视频 1=女优 3=AV 6=短剧",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "排序方式 default/hot/new,默认default",
+ "name": "sort",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.SearchResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/servicesProject": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "导航",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航"
+ ],
+ "summary": "导航",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/statement": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "合规声明",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合规声明"
+ ],
+ "summary": "合规声明",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/useragreement": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "用户协议",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户协议"
+ ],
+ "summary": "用户协议",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "actress.Meas": {
+ "type": "object",
+ "properties": {
+ "blood": {
+ "description": "血型",
+ "type": "string"
+ },
+ "bust": {
+ "description": "胸围 英寸",
+ "type": "integer"
+ },
+ "cup": {
+ "description": "罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I",
+ "type": "integer"
+ },
+ "dia": {
+ "description": "直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)",
+ "type": "integer"
+ },
+ "gender": {
+ "description": "性别 0:女 1:男 2:其他",
+ "type": "integer"
+ },
+ "height": {
+ "description": "身高 厘米",
+ "type": "integer"
+ },
+ "hip": {
+ "description": "臀围 英寸",
+ "type": "integer"
+ },
+ "len": {
+ "description": "长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)",
+ "type": "integer"
+ },
+ "waist": {
+ "description": "腰围 英寸",
+ "type": "integer"
+ },
+ "weight": {
+ "description": "体重 公斤",
+ "type": "integer"
+ }
+ }
+ },
+ "authapi.guestLoginReq": {
+ "type": "object",
+ "properties": {
+ "devID": {
+ "description": "设备 ID(首选)",
+ "type": "string"
+ },
+ "deviceId": {
+ "description": "设备 ID 第二版本(备用)",
+ "type": "string"
+ },
+ "mac": {
+ "description": "设备 MAC 地址(备用)",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.loginReq": {
+ "type": "object",
+ "required": [
+ "account",
+ "password"
+ ],
+ "properties": {
+ "account": {
+ "description": "账号",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.registerReq": {
+ "type": "object",
+ "required": [
+ "accountNo",
+ "password"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "agentInviteCode": {
+ "description": "邀请人的邀请码(可选)",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.updateProfileReq": {
+ "type": "object",
+ "required": [
+ "accountNo"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号(必填,用于定位用户)",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "新头像 URL(可选)",
+ "type": "string"
+ },
+ "newPassword": {
+ "description": "新密码明文(修改密码时必填)",
+ "type": "string"
+ },
+ "nikeName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oldPassword": {
+ "description": "旧密码明文(修改密码时必填)",
+ "type": "string"
+ }
+ }
+ },
+ "authser.LoginResp": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "isNewUser": {
+ "description": "是否本次请求新创建的用户",
+ "type": "boolean"
+ },
+ "isRegister": {
+ "description": "是否已完成账号注册",
+ "type": "boolean"
+ },
+ "loginType": {
+ "description": "登录方式 0=设备 3=账号",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "nickName": {
+ "type": "string"
+ },
+ "recharge": {
+ "description": "当前金币个数",
+ "type": "integer"
+ },
+ "status": {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ },
+ "token": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "authser.UserInfoResp": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "type": "string"
+ },
+ "avatar": {
+ "type": "string"
+ },
+ "inviteCode": {
+ "type": "string"
+ },
+ "isRegister": {
+ "type": "boolean"
+ },
+ "loginType": {
+ "$ref": "#/definitions/pubdef.LoginType"
+ },
+ "nickName": {
+ "type": "string"
+ },
+ "recharge": {
+ "type": "integer"
+ },
+ "status": {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "contenttagmod.ContentTagType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 5
+ ],
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ]
+ },
+ "playletchaptermod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletchaptermod.WatermarkStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusFailed": "处理失败",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone",
+ "WatermarkStatusFailed"
+ ]
+ },
+ "playletmod.ContentType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集(对应 media_type=6)",
+ "ContentTypeAnime": "动漫(对应 media_type=1)",
+ "ContentTypeComic": "漫画短剧(对应 media_type=4)",
+ "ContentTypeManga": "漫画(对应 media_type=2)",
+ "ContentTypeTheater": "短剧(对应 media_type=5)"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ]
+ },
+ "playletmod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletmod.Status": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ]
+ },
+ "proto.ActressInfo": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ActressInfoData": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "comments": {
+ "description": "评论条数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "measValue": {
+ "description": "三围值",
+ "type": "string"
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "watchTimes": {
+ "description": "观看次数",
+ "type": "integer"
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.AdvitiseData": {
+ "type": "object",
+ "properties": {
+ "AList": {
+ "description": "固定12广告位",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "BList": {
+ "description": "固定13广告位",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "detailTopList": {
+ "description": "详情页顶部广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "detailbannerList": {
+ "description": "详情banner",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "dialogList": {
+ "description": "弹窗广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "floatingList": {
+ "description": "悬浮小方块",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "friendList": {
+ "description": "友链",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "homeList": {
+ "description": "首页列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "launchScreenList": {
+ "description": "启动页广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "mustAppList": {
+ "description": "必备精品",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "nineGridList": {
+ "description": "9宫格",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "postList": {
+ "description": "列表穿插广告(视频、短剧、女优列表使用)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "titleAppList": {
+ "description": "标题广告(APP名称)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "videoList": {
+ "description": "视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ }
+ }
+ },
+ "proto.AnnouncementInfo": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "吃瓜app (免费吃瓜 每日更新)",
+ "type": "string"
+ },
+ "foreverUrlAddr": {
+ "description": "永久海外地址(翻墙访问 永不迷路)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "新国内地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "videoInfo": {
+ "description": "热门信息",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfoData"
+ }
+ }
+ }
+ },
+ "proto.AppCategoryInfo": {
+ "type": "object",
+ "properties": {
+ "appList": {
+ "description": "应用列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppInfoData"
+ }
+ },
+ "code": {
+ "description": "分类编码(特殊值:12=广告位A 13=广告位B)",
+ "type": "integer"
+ },
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "分类简介",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "title": {
+ "description": "分类标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.AppContentTagItem": {
+ "type": "object",
+ "properties": {
+ "bindCount": {
+ "description": "当前查询类型绑定数量,不传type时为各类型绑定数量合计",
+ "type": "integer"
+ },
+ "id": {
+ "description": "标签ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "playletCount": {
+ "description": "绑定短剧数量",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ },
+ "videoCount": {
+ "description": "绑定视频数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.AppContentTagListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "统一标签列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppContentTagItem"
+ }
+ }
+ }
+ },
+ "proto.AppInfoData": {
+ "type": "object",
+ "properties": {
+ "advitiseId": {
+ "description": "广告ID(广告条目时返回)",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "所属分类ID",
+ "type": "integer"
+ },
+ "description": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "应用ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "简介",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID",
+ "type": "integer"
+ },
+ "logo": {
+ "description": "Logo 图片地址",
+ "type": "string"
+ },
+ "name": {
+ "description": "应用名称",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "url": {
+ "description": "跳转链接(已含渠道码)",
+ "type": "string"
+ }
+ }
+ },
+ "proto.AppListResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "category": {
+ "description": "当前分类(含应用列表)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppCategoryInfo"
+ }
+ ]
+ },
+ "categoryList": {
+ "description": "应用分类列表(含各分类下的应用列表)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppCategoryInfo"
+ }
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.AppRichTextItemResp": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "id": {
+ "description": "富文本ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.AppRichTextListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "富文本内容列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppRichTextItemResp"
+ }
+ }
+ }
+ },
+ "proto.AppTranslationInfo": {
+ "type": "object",
+ "properties": {
+ "categoryList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationCategory"
+ }
+ }
+ }
+ },
+ "proto.CommentInfo": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "description": "用户头像",
+ "type": "string"
+ },
+ "commentCount": {
+ "description": "该评论回复数量",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "level": {
+ "description": "评论级别",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "上级评论ID",
+ "type": "integer"
+ },
+ "replyUserName": {
+ "description": "回复名称",
+ "type": "string"
+ },
+ "subComment": {
+ "description": "子评论",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommentListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.CommentSendReq": {
+ "type": "object",
+ "properties": {
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 0=视频 4=短剧",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CommentObjectType"
+ }
+ ]
+ },
+ "parentsId": {
+ "description": "上级评论ID",
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ }
+ }
+ },
+ "proto.ConfigInfo": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "dhDescription": {
+ "description": "导航SEO描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "导航SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhNewUrlAddr1": {
+ "description": "导航页-线路1",
+ "type": "string"
+ },
+ "dhNewUrlAddr2": {
+ "description": "导航页-线路2",
+ "type": "string"
+ },
+ "dhNewUrlAddr3": {
+ "description": "导航页-线路3",
+ "type": "string"
+ },
+ "dhSiteName": {
+ "description": "导航SEO网站名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航SEO标题",
+ "type": "string"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "freeVPN": {
+ "description": "免费vpn链接",
+ "type": "string"
+ },
+ "gitAddr": {
+ "description": "git地址",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgCdn": {
+ "description": "图片CDN",
+ "type": "string"
+ },
+ "m3u8DayRateLimitMax": {
+ "description": "每 IP 每天最多不同正片数",
+ "type": "integer"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "最新地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "potaoGroup": {
+ "description": "土豆群",
+ "type": "string"
+ },
+ "qq": {
+ "description": "qq群",
+ "type": "string"
+ },
+ "reportSdkConfig": {
+ "description": "数据上报SDK域名配置",
+ "type": "string"
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "telegramAdd": {
+ "description": "加入我们tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "tsCdn": {
+ "description": "视频CDN",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ },
+ "useIngAddress": {
+ "description": "所有使用中的域名",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webAddr": {
+ "description": "网站SEO网站地址",
+ "type": "string"
+ },
+ "webDescription": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "webKeyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webSiteName": {
+ "description": "网站SEO网站名称",
+ "type": "string"
+ },
+ "webTitle": {
+ "description": "网站SEO标题",
+ "type": "string"
+ },
+ "weiChart": {
+ "description": "微信群",
+ "type": "string"
+ }
+ }
+ },
+ "proto.ContentData": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.DHHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotNavs": {
+ "description": "热门导航",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotNavItem"
+ }
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "path": {
+ "type": "string"
+ },
+ "richTextInfo": {
+ "description": "富文本",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.RichTextInfo"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.DataResp": {
+ "type": "object",
+ "properties": {
+ "actressInfoList": {
+ "description": "女优信息",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfo"
+ }
+ },
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "homeBannerList": {
+ "description": "首页banner信息列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeBannerInfo"
+ }
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "playletData": {
+ "description": "首页短剧数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.HomePlayletDataResp"
+ }
+ ]
+ },
+ "videoData": {
+ "description": "首页视频数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.HomeVideoDataResp"
+ }
+ ]
+ }
+ }
+ },
+ "proto.FavoriteVideoListItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FavoriteVideoReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isFavorite"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isFavorite": {
+ "description": "是否收藏,true-收藏、false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.FavoriteVideoResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isFavorite": {
+ "description": "是否收藏,true-收藏、false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.FooterBrand": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "品牌标语",
+ "type": "string"
+ },
+ "title": {
+ "description": "品牌标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FooterContact": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "联系方式名称",
+ "type": "string"
+ },
+ "value": {
+ "description": "联系方式值",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FooterInfo": {
+ "type": "object",
+ "properties": {
+ "brand": {
+ "description": "品牌信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.FooterBrand"
+ }
+ ]
+ },
+ "contactList": {
+ "description": "联系方式",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.FooterContact"
+ }
+ },
+ "navList": {
+ "description": "底部导航",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationCategory"
+ }
+ }
+ }
+ },
+ "proto.FooterInfoResp": {
+ "type": "object",
+ "properties": {
+ "footerInfo": {
+ "description": "底部信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.FooterInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomeBannerInfo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "image": {
+ "description": "图片,已拼接图片域名",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "URL",
+ "type": "string"
+ },
+ "verticalImage": {
+ "description": "竖版图片,已拼接图片域名",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HomeBaseResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomeHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "description": "请求路径",
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "path": {
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomePlayletDataResp": {
+ "type": "object",
+ "properties": {
+ "hotList": {
+ "description": "最热短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "newestList": {
+ "description": "最新短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "recommendList": {
+ "description": "推荐短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ }
+ }
+ },
+ "proto.HomeRankData": {
+ "type": "object",
+ "properties": {
+ "month": {
+ "description": "月榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ },
+ "total": {
+ "description": "总榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ },
+ "week": {
+ "description": "周榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ },
+ "year": {
+ "description": "年榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ }
+ }
+ },
+ "proto.HomeTagsResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "playletTagsList": {
+ "description": "短剧标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "videoTagsList": {
+ "description": "视频标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ }
+ }
+ },
+ "proto.HomeVideoDataResp": {
+ "type": "object",
+ "properties": {
+ "hotList": {
+ "description": "最热视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ },
+ "newestList": {
+ "description": "最新视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ },
+ "recommendList": {
+ "description": "推荐视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ }
+ }
+ },
+ "proto.HotNavItem": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "description": "图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "介绍",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HotSearchItem": {
+ "type": "object",
+ "properties": {
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.LawAndTrustHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "path": {
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.LikePlayletReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isLike"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.LikePlayletResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ },
+ "likeNum": {
+ "description": "点赞数(真实点赞数加后台虚拟点赞数)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.LikeVideoReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isLike"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.LikeVideoResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ },
+ "likeNum": {
+ "description": "点赞数(真假点赞数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterInfo": {
+ "type": "object",
+ "properties": {
+ "countBrowse": {
+ "description": "浏览数",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "喜欢数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "剧集ID",
+ "type": "integer"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaContentId": {
+ "description": "媒资中心章节ID",
+ "type": "integer"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "视频播放地址,优先返回水印地址",
+ "type": "string"
+ },
+ "watermarkStatus": {
+ "description": "水印状态 0-未处理 1-已登记 2-处理中 3-已完成",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.WatermarkStatus"
+ }
+ ]
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletDetailInfo": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countComment": {
+ "description": "评论数量",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该短剧",
+ "type": "boolean"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRankData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "rankScore": {
+ "description": "榜单分数,点赞榜为累计展示点赞数,周/月/年榜为对应周期观看数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRankResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.PlayletRecommendItemType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 2
+ ],
+ "x-enum-comments": {
+ "PlayletRecommendItemTypeAdvertise": "广告数据",
+ "PlayletRecommendItemTypePlaylet": "短剧数据"
+ },
+ "x-enum-varnames": [
+ "PlayletRecommendItemTypePlaylet",
+ "PlayletRecommendItemTypeAdvertise"
+ ]
+ },
+ "proto.PlayletRecommendListItem": {
+ "type": "object",
+ "properties": {
+ "advertiseId": {
+ "description": "广告ID,type=2时返回",
+ "type": "string"
+ },
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "link": {
+ "description": "广告跳转链接,type=2时返回",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID,type=2时返回",
+ "type": "integer"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "type": {
+ "description": "数据类型 0-短剧数据 2-广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendItemType"
+ }
+ ]
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRecommendListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PostCreateInfo": {
+ "type": "object",
+ "properties": {
+ "background": {
+ "description": "背景",
+ "type": "string"
+ },
+ "createType": {
+ "description": "创作者类型 0=普通 1=官方",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "作者描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "image": {
+ "description": "头像",
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "works": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.QueryActressListResp": {
+ "type": "object",
+ "properties": {
+ "actressInfoList": {
+ "description": "女优列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ },
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryActressWorkListResp": {
+ "type": "object",
+ "properties": {
+ "actressInfo": {
+ "description": "女优信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryFavoriteVideoListResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryPlayletChapterInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "chapterInfo": {
+ "description": "当前剧集详情",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ ]
+ },
+ "chapterList": {
+ "description": "剧集列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotPlayletList": {
+ "description": "热门短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该短剧",
+ "type": "boolean"
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "playletInfo": {
+ "description": "所属短剧详情",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryPlayletInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "chapterList": {
+ "description": "剧集列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotPlayletList": {
+ "description": "热门短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该短剧",
+ "type": "boolean"
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "playletInfo": {
+ "description": "短剧详情数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletDetailInfo"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryVideoInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotVideoList": {
+ "description": "热门视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoData"
+ }
+ },
+ "isFavorite": {
+ "description": "当前登录用户是否已收藏该视频",
+ "type": "boolean"
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该视频",
+ "type": "boolean"
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "videoInfo": {
+ "description": "视频详情数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoDetailInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryVideoItemType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 2
+ ],
+ "x-enum-varnames": [
+ "QueryVideoItemTypeVideo",
+ "QueryVideoItemTypeAdvertise"
+ ]
+ },
+ "proto.QueryVideoListItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "advertiseId": {
+ "description": "广告ID,type=2时返回",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "link": {
+ "description": "广告跳转链接,type=2时返回",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID,type=2时返回",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "数据类型:0-视频数据、2-广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoItemType"
+ }
+ ]
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.QueryVideoResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "categoryInfo": {
+ "description": "当前分类信息(含 SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoCategoryInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QuestionnaireSubmitReq": {
+ "type": "object",
+ "required": [
+ "description",
+ "deviceType",
+ "frequency",
+ "networkType",
+ "operator",
+ "province",
+ "questionType"
+ ],
+ "properties": {
+ "attachments": {
+ "description": "附件列表,最多3个(选填)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/questionnairemod.Attachment"
+ }
+ },
+ "city": {
+ "description": "所在城市(选填)",
+ "type": "string"
+ },
+ "contact": {
+ "description": "联系方式(选填)",
+ "type": "string"
+ },
+ "description": {
+ "description": "问题描述(必填)",
+ "type": "string"
+ },
+ "deviceModel": {
+ "description": "设备型号(选填)",
+ "type": "string"
+ },
+ "deviceType": {
+ "description": "设备类型:iPhone、Android、iPad、其他",
+ "type": "string"
+ },
+ "frequency": {
+ "description": "出现频率:每次都会、经常出现、偶尔出现、仅现一次",
+ "type": "string"
+ },
+ "networkType": {
+ "description": "网络类型:WI-FI、5G、4G、其他",
+ "type": "string"
+ },
+ "operator": {
+ "description": "运营商:电信、联通、移动、广电",
+ "type": "string"
+ },
+ "province": {
+ "description": "所在省份(必填)",
+ "type": "string"
+ },
+ "questionType": {
+ "description": "问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题",
+ "type": "string"
+ },
+ "userId": {
+ "description": "登录用户ID,0表示匿名(选填)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.RankResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "rankData": {
+ "$ref": "#/definitions/proto.HomeRankData"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.RichTextInfo": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.SearchResp": {
+ "type": "object",
+ "properties": {
+ "actress": {
+ "description": "女优资源",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ },
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "inTitle": {
+ "description": "输入标题",
+ "type": "string"
+ },
+ "inType": {
+ "description": "输入类型 -1=全部 0=视频 1=女优 3=AV 6=短剧",
+ "type": "integer"
+ },
+ "media": {
+ "description": "视频资源",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfoData"
+ }
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "path": {
+ "type": "string"
+ },
+ "playlets": {
+ "description": "短剧资源",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "searchRecommend": {
+ "description": "搜索推荐",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "sorts": {
+ "description": "排序选项",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.SearchSortItem"
+ }
+ },
+ "totalSize": {
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.SearchSortItem": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "description": "值",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TagsInfoData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "images": {
+ "description": "背景",
+ "type": "string"
+ },
+ "name": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.TranslationCategory": {
+ "type": "object",
+ "properties": {
+ "categoryType": {
+ "description": "导航类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.TranslationType"
+ }
+ ]
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "translationList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationInfo"
+ }
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TranslationInfo": {
+ "type": "object",
+ "properties": {
+ "displayMode": {
+ "description": "展示方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.DisplayMode"
+ }
+ ]
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.VideoCategoryInfo": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ }
+ }
+ },
+ "proto.VideoData": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoDetailInfo": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "desc": {
+ "description": "作品简介",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "isFavorite": {
+ "description": "当前登录用户是否已收藏该视频",
+ "type": "boolean"
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该视频",
+ "type": "boolean"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频播放信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoInfo": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoInfoData": {
+ "type": "object",
+ "properties": {
+ "actressInfo": {
+ "description": "女优信息",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ },
+ "addedTime": {
+ "description": "帖子上架时间",
+ "type": "string"
+ },
+ "advitiseId": {
+ "description": "广告Id",
+ "type": "string"
+ },
+ "bango": {
+ "description": "番号,唯一",
+ "type": "string"
+ },
+ "collectionInfo": {
+ "description": "合集数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfoData"
+ }
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createInfo": {
+ "description": "作者信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PostCreateInfo"
+ }
+ ]
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "issuedAt": {
+ "description": "发行时间",
+ "type": "string"
+ },
+ "issuedBrand": {
+ "description": "发行商",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "link": {
+ "description": "跳转链接",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置id",
+ "type": "integer"
+ },
+ "previewVtt": {
+ "description": "雪碧图 vtt",
+ "type": "string"
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoType"
+ }
+ ]
+ },
+ "updatedAt": {
+ "description": "页面更新时间",
+ "type": "string"
+ },
+ "video": {
+ "description": "Nodes []videoinformationmod.Node ` + "`" + `json:\"nodes\"` + "`" + ` // 节点",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "watchTimes": {
+ "description": "观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoRankResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.VideoRecommendItemType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 2
+ ],
+ "x-enum-comments": {
+ "VideoRecommendItemTypeAdvertise": "广告数据",
+ "VideoRecommendItemTypeVideo": "视频数据"
+ },
+ "x-enum-varnames": [
+ "VideoRecommendItemTypeVideo",
+ "VideoRecommendItemTypeAdvertise"
+ ]
+ },
+ "proto.VideoRecommendListItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "advertiseId": {
+ "description": "广告ID,type=2时返回",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "link": {
+ "description": "广告跳转链接,type=2时返回",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID,type=2时返回",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "数据类型 0-视频数据 2-广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRecommendItemType"
+ }
+ ]
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoRecommendListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "VideoTypeAdvitise": "广告",
+ "VideoTypeCollection": "合集",
+ "VideoTypeVideo": "视频",
+ "VideoType_AV": "AV视频"
+ },
+ "x-enum-varnames": [
+ "VideoTypeVideo",
+ "VideoTypeCollection",
+ "VideoTypeAdvitise",
+ "VideoType_AV"
+ ]
+ },
+ "proxy.AdvitiseInfo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "广告id",
+ "type": "string"
+ },
+ "images": {
+ "description": "视频banner",
+ "type": "string"
+ },
+ "link": {
+ "description": "链接",
+ "type": "string"
+ },
+ "location": {
+ "description": "位置id",
+ "type": "integer"
+ },
+ "title": {
+ "description": "广告标题",
+ "type": "string"
+ }
+ }
+ },
+ "pubdef.CommentObjectType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 4
+ ],
+ "x-enum-comments": {
+ "CommentObjectTypePlaylet": "短剧",
+ "CommentObjectTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "CommentObjectTypeVideo",
+ "CommentObjectTypePlaylet"
+ ]
+ },
+ "pubdef.DisplayMode": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "DisplayModeAvatarList": "头像列表",
+ "DisplayModeBigCover": "大封面列表(一大图两列)",
+ "DisplayModeCardCarousel": "卡片层叠轮播",
+ "DisplayModeDefault": "默认(上图下标题多列)",
+ "DisplayModeImageText": "图文列表(左图右标题多列)"
+ },
+ "x-enum-varnames": [
+ "DisplayModeDefault",
+ "DisplayModeCardCarousel",
+ "DisplayModeAvatarList",
+ "DisplayModeImageText",
+ "DisplayModeBigCover"
+ ]
+ },
+ "pubdef.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeMobile": "手机登陆",
+ "LoginTypeQrCode": "二维码登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeMobile",
+ "LoginTypeQrCode",
+ "LoginTypeAccount"
+ ]
+ },
+ "pubdef.SeoValue": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "网站SEO标题",
+ "type": "string"
+ }
+ }
+ },
+ "pubdef.TranslationType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "TranslationTypeBottom": "底部导航",
+ "TranslationTypeSub": "二级导航",
+ "TranslationTypeUrl": "URL模式"
+ },
+ "x-enum-varnames": [
+ "TranslationTypeUrl",
+ "TranslationTypeSub",
+ "TranslationTypeBottom"
+ ]
+ },
+ "pubdef.UserStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "UserStatusDisable": "封禁",
+ "UserStatusNormal": "正常",
+ "UserStatusSlient": "禁言"
+ },
+ "x-enum-varnames": [
+ "UserStatusNormal",
+ "UserStatusDisable",
+ "UserStatusSlient"
+ ]
+ },
+ "questionnairemod.Attachment": {
+ "type": "object",
+ "properties": {
+ "fileType": {
+ "description": "附件类型(img / video)",
+ "type": "string"
+ },
+ "url": {
+ "description": "附件URL地址",
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.ImageUploadData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sourceNameFs": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.ImageUploadResp": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "$ref": "#/definitions/uploadser.ImageUploadData"
+ },
+ "hash": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "time": {
+ "type": "string"
+ },
+ "tip": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.VideoBodyData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sourceNameFs": {
+ "type": "string"
+ },
+ "taskId": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.VideoBodyResp": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "$ref": "#/definitions/uploadser.VideoBodyData"
+ },
+ "hash": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "time": {
+ "type": "string"
+ },
+ "tip": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.VideoOkResp": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "tip": {
+ "type": "string"
+ }
+ }
+ },
+ "videoinformationmod.VideoInfo": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "fileId": {
+ "description": "媒资库ID",
+ "type": "string"
+ },
+ "h265Url": {
+ "description": "H.265 m3u8 播放地址,空字符串表示暂无该编码源",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "H.264 m3u8 播放地址(始终存在)",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "ApiKeyAuth": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ },
+ "BasicAuth": {
+ "type": "basic"
+ },
+ "OAuth2AccessCode": {
+ "type": "oauth2",
+ "flow": "accessCode",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information"
+ }
+ },
+ "OAuth2Application": {
+ "type": "oauth2",
+ "flow": "application",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Implicit": {
+ "type": "oauth2",
+ "flow": "implicit",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Password": {
+ "type": "oauth2",
+ "flow": "password",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "read": "Grants read access",
+ "write": "Grants write access"
+ }
+ }
+ }
+}`
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = &swag.Spec{
+ Version: "1.0",
+ Host: "视频站-api.lcncctn.com",
+ BasePath: "/",
+ Schemes: []string{"http", "https"},
+ Title: "视频站APP接口",
+ Description: "这是视频站APP接口服务.",
+ InfoInstanceName: "swagger",
+ SwaggerTemplate: docTemplate,
+ LeftDelim: "{{",
+ RightDelim: "}}",
+}
+
+func init() {
+ swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
+}
diff --git a/app/docs/swagger.json b/app/docs/swagger.json
new file mode 100644
index 0000000..3af08bb
--- /dev/null
+++ b/app/docs/swagger.json
@@ -0,0 +1,6720 @@
+{
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "swagger": "2.0",
+ "info": {
+ "description": "这是视频站APP接口服务.",
+ "title": "视频站APP接口",
+ "contact": {},
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0"
+ },
+ "host": "视频站-api.lcncctn.com",
+ "basePath": "/",
+ "paths": {
+ "/api/app/actress/list": {
+ "get": {
+ "description": "返回导航、广告和按热度值降序排列的女优列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优相关接口"
+ ],
+ "summary": "女优榜单列表 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.QueryActressListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/actress/work/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "查询指定女优详情和作品列表,支持上架时间/观看量/收藏量升降序,每间隔8条作品数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优相关接口"
+ ],
+ "summary": "指定女优详情和作品列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "上架时间升序/降序(1-升序 2降序)",
+ "name": "addedTimeSort",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "女优ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "按收藏(点赞)量升序/降序(1-升序 2降序)",
+ "name": "likesSort",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "女优slug,非空时优先按slug解析女优ID",
+ "name": "slug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "按观看量升序/降序(1-升序 2降序)",
+ "name": "watchTimesSort",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryActressWorkListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoListItem"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "VideoInfo": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/applist/list": {
+ "get": {
+ "description": "返回应用分类列表,每个分类下含应用信息列表;categoryId=0 返回第一个分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用列表"
+ ],
+ "summary": "查询应用列表",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "当前分类ID,0表示默认第一个分类",
+ "name": "categoryId",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/getInfo": {
+ "get": {
+ "description": "根据当前登录 token 查询用户基础信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "获取当前登录用户信息",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/authser.UserInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/login": {
+ "post": {
+ "description": "账号均可作为登录凭证",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "登录",
+ "parameters": [
+ {
+ "description": "登录参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.loginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "登录成功,返回 token 及用户基本信息",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/profile": {
+ "post": {
+ "description": "账号必填;avatar、oldPassword+newPassword 均为可选,至少提供一项。修改密码时旧密码和新密码必须同时提供。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "修改用户资料(头像 / 密码,可任意组合)",
+ "parameters": [
+ {
+ "description": "修改资料参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.updateProfileReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/register": {
+ "post": {
+ "description": "已登录游客传入账号密码完成升级;未登录用户直接注册新账号。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "注册",
+ "parameters": [
+ {
+ "description": "注册参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.registerReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "注册成功,返回 token 及用户基本信息",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/list": {
+ "get": {
+ "description": "按对象查询树形评论列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论APP"
+ ],
+ "summary": "评论列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "对象ID",
+ "name": "objectId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 4
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "CommentObjectTypePlaylet": "短剧",
+ "CommentObjectTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "CommentObjectTypeVideo",
+ "CommentObjectTypePlaylet"
+ ],
+ "description": "对象类型 0=视频 4=短剧",
+ "name": "objectType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.CommentListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/send": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "发布评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论APP"
+ ],
+ "summary": "发布评论 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.CommentSendReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/config/footer/info": {
+ "get": {
+ "description": "返回APP底部品牌、底部导航和联系方式",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app配置相关接口"
+ ],
+ "summary": "查询APP底部信息 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FooterInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/config/richtext/list": {
+ "get": {
+ "description": "返回启用状态的富文本内容,支持按ids或titles过滤",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app配置相关接口"
+ ],
+ "summary": "查询APP富文本内容列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "富文本ID,多个用英文逗号分隔",
+ "name": "ids",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "富文本标题,多个用英文逗号分隔",
+ "name": "titles",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppRichTextListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/content/tag/list": {
+ "get": {
+ "description": "返回启用且有绑定内容数量的统一标签,contentTagType非必填,按绑定内容数量倒序排序",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app配置相关接口"
+ ],
+ "summary": "查询APP统一标签列表 -- by 王鸣",
+ "parameters": [
+ {
+ "enum": [
+ 1,
+ 5
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ],
+ "description": "内容类型 1-视频 5-短剧,不传返回全部有绑定数量的标签",
+ "name": "contentTagType",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppContentTagListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/home": {
+ "get": {
+ "description": "首页分类数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "首页"
+ ],
+ "summary": "首页 --by 旺鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.DataResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/home/base": {
+ "get": {
+ "description": "只返回翻译、配置、底部翻译、广告和公告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "首页"
+ ],
+ "summary": "首页公共数据 --by 旺鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBaseResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/home/tags": {
+ "get": {
+ "description": "返回视频、短剧的标签信息,以及基础导航、配置、广告和公告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "首页"
+ ],
+ "summary": "首页标签数据 --by 旺鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeTagsResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/login/guest": {
+ "post": {
+ "description": "按设备 ID 查找已有游客账号,找不到则自动注册。若请求携带有效 token 则优先复用该用户。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "游客登录",
+ "parameters": [
+ {
+ "description": "设备信息",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.guestLoginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "登录成功",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/navigation/header": {
+ "get": {
+ "description": "查询导航类型为URL模式和二级导航的头部导航配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app导航相关接口"
+ ],
+ "summary": "查询头部导航配置 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/info": {
+ "get": {
+ "description": "根据剧集ID查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集详情 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "剧集ID",
+ "name": "id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryPlayletChapterInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "chapterInfo": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ },
+ "chapterList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "hotPlayletList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "playletInfo": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/info": {
+ "get": {
+ "description": "查询短剧详情、剧集列表、热门短剧列表和点赞状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "短剧详情 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "slug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryPlayletInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "chapterList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "hotPlayletList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "playletInfo": {
+ "$ref": "#/definitions/proto.PlayletDetailInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/like": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户点赞或取消点赞指定短剧,isLike=true表示点赞,isLike=false表示取消点赞",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "点赞或取消点赞短剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "点赞或取消点赞请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.LikePlayletReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LikePlayletResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/rank/list": {
+ "get": {
+ "description": "分页查询短剧排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "短剧排行榜分页列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜",
+ "name": "rankPeriod",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRankResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletRankData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/recommend/list": {
+ "get": {
+ "description": "分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端推荐短剧列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧分类ID,不传查询全部分类",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类slug,非空时优先按slug解析分类ID",
+ "name": "categorySlug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "排序方式 1-推荐 2-最新 3-最热",
+ "name": "sortList",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "标签ID,不传查询全部标签",
+ "name": "tagId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签slug,非空时优先按slug解析标签ID",
+ "name": "tagSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/tag/{id}": {
+ "get": {
+ "description": "根据统一标签ID分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据标签ID查询短剧列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量,默认20,最大100",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/questionnaire/submit": {
+ "post": {
+ "description": "用户提交问题反馈问卷",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "问卷调查APP"
+ ],
+ "summary": "提交问卷",
+ "parameters": [
+ {
+ "description": "提交参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.QuestionnaireSubmitReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/richtext": {
+ "get": {
+ "description": "从缓存中查询启用状态的富文本信息",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本"
+ ],
+ "summary": "按 title 查询富文本",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "富文本内部标题",
+ "name": "title",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.RichTextInfo"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/upload/image": {
+ "post": {
+ "description": "将图片转发至存储服务 /media/web/autopublish/image",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "上传"
+ ],
+ "summary": "图片上传",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "图片文件",
+ "name": "file",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.ImageUploadResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/upload/videoBody": {
+ "post": {
+ "description": "将视频分片转发至存储服务 /media/web/autopublish/videoBody",
+ "consumes": [
+ "multipart/form-data"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "上传"
+ ],
+ "summary": "视频分片上传",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "视频任务ID",
+ "name": "taskId",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "当前分片序号,从0开始",
+ "name": "pos",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "上传用户名,为空默认 questionnaires",
+ "name": "uploadUserName",
+ "in": "formData"
+ },
+ {
+ "type": "file",
+ "description": "分片文件数据",
+ "name": "file",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.VideoBodyResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/upload/videoOk": {
+ "post": {
+ "description": "所有分片上传完毕后调用,转发至存储服务 /media/web/autopublish/videoOk",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "上传"
+ ],
+ "summary": "视频分片上传完成通知",
+ "parameters": [
+ {
+ "description": "{ \\",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.VideoOkResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/favorite": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户收藏或取消收藏指定视频,isFavorite=true表示收藏,isFavorite=false表示取消收藏",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "收藏或取消收藏视频 --by 旺鸣",
+ "parameters": [
+ {
+ "description": "收藏或取消收藏请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.FavoriteVideoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FavoriteVideoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/favorite/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户收藏视频分页列表,按收藏时间倒序排序",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "当前登录用户收藏视频分页列表 --by 旺鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryFavoriteVideoListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.FavoriteVideoListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/info": {
+ "get": {
+ "description": "查询视频详情、热门视频列表、收藏状态和点赞状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "视频详情 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "视频slug,非空时优先按slug解析视频ID",
+ "name": "slug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "hotVideoList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoData"
+ }
+ },
+ "videoInfo": {
+ "$ref": "#/definitions/proto.VideoDetailInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/like": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "当前登录用户点赞或取消点赞指定视频,isLike=true表示点赞,isLike=false表示取消点赞",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "点赞或取消点赞视频 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "点赞或取消点赞请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.LikeVideoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LikeVideoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/more": {
+ "get": {
+ "description": "根据分类id查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "根据分类id查询视频列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "视频分类id",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类slug,非空时优先按slug解析分类ID",
+ "name": "categorySlug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoListItem"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "VideoInfo": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/rank": {
+ "get": {
+ "description": "视频收藏排行榜",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "视频收藏排行榜",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.RankResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/rank/list": {
+ "get": {
+ "description": "分页查询视频排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "视频排行榜分页列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜",
+ "name": "rankPeriod",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRankResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/recommend/list": {
+ "get": {
+ "description": "分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "app端推荐视频列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "分类id,不传查询全部分类id",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类slug,非空时优先按slug解析分类ID",
+ "name": "categorySlug",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "排序方式 1-推荐 2-最新 3-最热",
+ "name": "sortList",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "标签ID,不传查询全部标签",
+ "name": "tagId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签slug,非空时优先按slug解析标签ID",
+ "name": "tagSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/tag/{id}": {
+ "get": {
+ "description": "根据统一标签ID分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "app端根据标签ID查询视频列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量,默认20,最大100",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoRecommendListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/businessCooperation": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "导航",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航"
+ ],
+ "summary": "导航",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/consulting": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "招募",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "招募"
+ ],
+ "summary": "招募",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/consultinginfo/:id": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "招募详情",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "招募详情"
+ ],
+ "summary": "招募详情",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/dh": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "导航",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航"
+ ],
+ "summary": "导航",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.DHHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/dmca": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "dmca",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "dmca"
+ ],
+ "summary": "dmca",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/privacyPolicy": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "隐私策略",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "隐私策略"
+ ],
+ "summary": "隐私策略",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/search": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "搜索,支持查询参数:title=关键词\u0026page=1\u0026type=0\u0026sort=default(不传type=全部;type: 0=视频 1=女优 3=AV 6=短剧;sort: default/hot/new)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "搜索"
+ ],
+ "summary": "搜索",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "搜索关键词",
+ "name": "title",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "媒体类型,不传=全部,0=视频 1=女优 3=AV 6=短剧",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "排序方式 default/hot/new,默认default",
+ "name": "sort",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.SearchResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/servicesProject": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "导航",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航"
+ ],
+ "summary": "导航",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/statement": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "合规声明",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合规声明"
+ ],
+ "summary": "合规声明",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/useragreement": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "用户协议",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户协议"
+ ],
+ "summary": "用户协议",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.LawAndTrustHtmlData"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "actress.Meas": {
+ "type": "object",
+ "properties": {
+ "blood": {
+ "description": "血型",
+ "type": "string"
+ },
+ "bust": {
+ "description": "胸围 英寸",
+ "type": "integer"
+ },
+ "cup": {
+ "description": "罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I",
+ "type": "integer"
+ },
+ "dia": {
+ "description": "直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)",
+ "type": "integer"
+ },
+ "gender": {
+ "description": "性别 0:女 1:男 2:其他",
+ "type": "integer"
+ },
+ "height": {
+ "description": "身高 厘米",
+ "type": "integer"
+ },
+ "hip": {
+ "description": "臀围 英寸",
+ "type": "integer"
+ },
+ "len": {
+ "description": "长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)",
+ "type": "integer"
+ },
+ "waist": {
+ "description": "腰围 英寸",
+ "type": "integer"
+ },
+ "weight": {
+ "description": "体重 公斤",
+ "type": "integer"
+ }
+ }
+ },
+ "authapi.guestLoginReq": {
+ "type": "object",
+ "properties": {
+ "devID": {
+ "description": "设备 ID(首选)",
+ "type": "string"
+ },
+ "deviceId": {
+ "description": "设备 ID 第二版本(备用)",
+ "type": "string"
+ },
+ "mac": {
+ "description": "设备 MAC 地址(备用)",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.loginReq": {
+ "type": "object",
+ "required": [
+ "account",
+ "password"
+ ],
+ "properties": {
+ "account": {
+ "description": "账号",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.registerReq": {
+ "type": "object",
+ "required": [
+ "accountNo",
+ "password"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "agentInviteCode": {
+ "description": "邀请人的邀请码(可选)",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.updateProfileReq": {
+ "type": "object",
+ "required": [
+ "accountNo"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号(必填,用于定位用户)",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "新头像 URL(可选)",
+ "type": "string"
+ },
+ "newPassword": {
+ "description": "新密码明文(修改密码时必填)",
+ "type": "string"
+ },
+ "nikeName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oldPassword": {
+ "description": "旧密码明文(修改密码时必填)",
+ "type": "string"
+ }
+ }
+ },
+ "authser.LoginResp": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "isNewUser": {
+ "description": "是否本次请求新创建的用户",
+ "type": "boolean"
+ },
+ "isRegister": {
+ "description": "是否已完成账号注册",
+ "type": "boolean"
+ },
+ "loginType": {
+ "description": "登录方式 0=设备 3=账号",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "nickName": {
+ "type": "string"
+ },
+ "recharge": {
+ "description": "当前金币个数",
+ "type": "integer"
+ },
+ "status": {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ },
+ "token": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "authser.UserInfoResp": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "type": "string"
+ },
+ "avatar": {
+ "type": "string"
+ },
+ "inviteCode": {
+ "type": "string"
+ },
+ "isRegister": {
+ "type": "boolean"
+ },
+ "loginType": {
+ "$ref": "#/definitions/pubdef.LoginType"
+ },
+ "nickName": {
+ "type": "string"
+ },
+ "recharge": {
+ "type": "integer"
+ },
+ "status": {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "contenttagmod.ContentTagType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 5
+ ],
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ]
+ },
+ "playletchaptermod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletchaptermod.WatermarkStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusFailed": "处理失败",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone",
+ "WatermarkStatusFailed"
+ ]
+ },
+ "playletmod.ContentType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集(对应 media_type=6)",
+ "ContentTypeAnime": "动漫(对应 media_type=1)",
+ "ContentTypeComic": "漫画短剧(对应 media_type=4)",
+ "ContentTypeManga": "漫画(对应 media_type=2)",
+ "ContentTypeTheater": "短剧(对应 media_type=5)"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ]
+ },
+ "playletmod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletmod.Status": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ]
+ },
+ "proto.ActressInfo": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ActressInfoData": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "comments": {
+ "description": "评论条数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "measValue": {
+ "description": "三围值",
+ "type": "string"
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "watchTimes": {
+ "description": "观看次数",
+ "type": "integer"
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.AdvitiseData": {
+ "type": "object",
+ "properties": {
+ "AList": {
+ "description": "固定12广告位",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "BList": {
+ "description": "固定13广告位",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "detailTopList": {
+ "description": "详情页顶部广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "detailbannerList": {
+ "description": "详情banner",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "dialogList": {
+ "description": "弹窗广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "floatingList": {
+ "description": "悬浮小方块",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "friendList": {
+ "description": "友链",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "homeList": {
+ "description": "首页列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "launchScreenList": {
+ "description": "启动页广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "mustAppList": {
+ "description": "必备精品",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "nineGridList": {
+ "description": "9宫格",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "postList": {
+ "description": "列表穿插广告(视频、短剧、女优列表使用)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "titleAppList": {
+ "description": "标题广告(APP名称)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "videoList": {
+ "description": "视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ }
+ }
+ },
+ "proto.AnnouncementInfo": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "吃瓜app (免费吃瓜 每日更新)",
+ "type": "string"
+ },
+ "foreverUrlAddr": {
+ "description": "永久海外地址(翻墙访问 永不迷路)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "新国内地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "videoInfo": {
+ "description": "热门信息",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfoData"
+ }
+ }
+ }
+ },
+ "proto.AppCategoryInfo": {
+ "type": "object",
+ "properties": {
+ "appList": {
+ "description": "应用列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppInfoData"
+ }
+ },
+ "code": {
+ "description": "分类编码(特殊值:12=广告位A 13=广告位B)",
+ "type": "integer"
+ },
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "分类简介",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "title": {
+ "description": "分类标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.AppContentTagItem": {
+ "type": "object",
+ "properties": {
+ "bindCount": {
+ "description": "当前查询类型绑定数量,不传type时为各类型绑定数量合计",
+ "type": "integer"
+ },
+ "id": {
+ "description": "标签ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "playletCount": {
+ "description": "绑定短剧数量",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ },
+ "videoCount": {
+ "description": "绑定视频数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.AppContentTagListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "统一标签列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppContentTagItem"
+ }
+ }
+ }
+ },
+ "proto.AppInfoData": {
+ "type": "object",
+ "properties": {
+ "advitiseId": {
+ "description": "广告ID(广告条目时返回)",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "所属分类ID",
+ "type": "integer"
+ },
+ "description": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "应用ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "简介",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID",
+ "type": "integer"
+ },
+ "logo": {
+ "description": "Logo 图片地址",
+ "type": "string"
+ },
+ "name": {
+ "description": "应用名称",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "url": {
+ "description": "跳转链接(已含渠道码)",
+ "type": "string"
+ }
+ }
+ },
+ "proto.AppListResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "category": {
+ "description": "当前分类(含应用列表)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppCategoryInfo"
+ }
+ ]
+ },
+ "categoryList": {
+ "description": "应用分类列表(含各分类下的应用列表)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppCategoryInfo"
+ }
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.AppRichTextItemResp": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "id": {
+ "description": "富文本ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.AppRichTextListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "富文本内容列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AppRichTextItemResp"
+ }
+ }
+ }
+ },
+ "proto.AppTranslationInfo": {
+ "type": "object",
+ "properties": {
+ "categoryList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationCategory"
+ }
+ }
+ }
+ },
+ "proto.CommentInfo": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "description": "用户头像",
+ "type": "string"
+ },
+ "commentCount": {
+ "description": "该评论回复数量",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "level": {
+ "description": "评论级别",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "上级评论ID",
+ "type": "integer"
+ },
+ "replyUserName": {
+ "description": "回复名称",
+ "type": "string"
+ },
+ "subComment": {
+ "description": "子评论",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommentListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.CommentSendReq": {
+ "type": "object",
+ "properties": {
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 0=视频 4=短剧",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CommentObjectType"
+ }
+ ]
+ },
+ "parentsId": {
+ "description": "上级评论ID",
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ }
+ }
+ },
+ "proto.ConfigInfo": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "dhDescription": {
+ "description": "导航SEO描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "导航SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhNewUrlAddr1": {
+ "description": "导航页-线路1",
+ "type": "string"
+ },
+ "dhNewUrlAddr2": {
+ "description": "导航页-线路2",
+ "type": "string"
+ },
+ "dhNewUrlAddr3": {
+ "description": "导航页-线路3",
+ "type": "string"
+ },
+ "dhSiteName": {
+ "description": "导航SEO网站名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航SEO标题",
+ "type": "string"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "freeVPN": {
+ "description": "免费vpn链接",
+ "type": "string"
+ },
+ "gitAddr": {
+ "description": "git地址",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgCdn": {
+ "description": "图片CDN",
+ "type": "string"
+ },
+ "m3u8DayRateLimitMax": {
+ "description": "每 IP 每天最多不同正片数",
+ "type": "integer"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "最新地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "potaoGroup": {
+ "description": "土豆群",
+ "type": "string"
+ },
+ "qq": {
+ "description": "qq群",
+ "type": "string"
+ },
+ "reportSdkConfig": {
+ "description": "数据上报SDK域名配置",
+ "type": "string"
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "telegramAdd": {
+ "description": "加入我们tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "tsCdn": {
+ "description": "视频CDN",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ },
+ "useIngAddress": {
+ "description": "所有使用中的域名",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webAddr": {
+ "description": "网站SEO网站地址",
+ "type": "string"
+ },
+ "webDescription": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "webKeyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webSiteName": {
+ "description": "网站SEO网站名称",
+ "type": "string"
+ },
+ "webTitle": {
+ "description": "网站SEO标题",
+ "type": "string"
+ },
+ "weiChart": {
+ "description": "微信群",
+ "type": "string"
+ }
+ }
+ },
+ "proto.ContentData": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.DHHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotNavs": {
+ "description": "热门导航",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotNavItem"
+ }
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "path": {
+ "type": "string"
+ },
+ "richTextInfo": {
+ "description": "富文本",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.RichTextInfo"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.DataResp": {
+ "type": "object",
+ "properties": {
+ "actressInfoList": {
+ "description": "女优信息",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfo"
+ }
+ },
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "homeBannerList": {
+ "description": "首页banner信息列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeBannerInfo"
+ }
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "playletData": {
+ "description": "首页短剧数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.HomePlayletDataResp"
+ }
+ ]
+ },
+ "videoData": {
+ "description": "首页视频数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.HomeVideoDataResp"
+ }
+ ]
+ }
+ }
+ },
+ "proto.FavoriteVideoListItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FavoriteVideoReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isFavorite"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isFavorite": {
+ "description": "是否收藏,true-收藏、false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.FavoriteVideoResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isFavorite": {
+ "description": "是否收藏,true-收藏、false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.FooterBrand": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "品牌标语",
+ "type": "string"
+ },
+ "title": {
+ "description": "品牌标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FooterContact": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "联系方式名称",
+ "type": "string"
+ },
+ "value": {
+ "description": "联系方式值",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FooterInfo": {
+ "type": "object",
+ "properties": {
+ "brand": {
+ "description": "品牌信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.FooterBrand"
+ }
+ ]
+ },
+ "contactList": {
+ "description": "联系方式",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.FooterContact"
+ }
+ },
+ "navList": {
+ "description": "底部导航",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationCategory"
+ }
+ }
+ }
+ },
+ "proto.FooterInfoResp": {
+ "type": "object",
+ "properties": {
+ "footerInfo": {
+ "description": "底部信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.FooterInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomeBannerInfo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "image": {
+ "description": "图片,已拼接图片域名",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "URL",
+ "type": "string"
+ },
+ "verticalImage": {
+ "description": "竖版图片,已拼接图片域名",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HomeBaseResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomeHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "description": "请求路径",
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "path": {
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomePlayletDataResp": {
+ "type": "object",
+ "properties": {
+ "hotList": {
+ "description": "最热短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "newestList": {
+ "description": "最新短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "recommendList": {
+ "description": "推荐短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ }
+ }
+ },
+ "proto.HomeRankData": {
+ "type": "object",
+ "properties": {
+ "month": {
+ "description": "月榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ },
+ "total": {
+ "description": "总榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ },
+ "week": {
+ "description": "周榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ },
+ "year": {
+ "description": "年榜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentData"
+ }
+ }
+ }
+ },
+ "proto.HomeTagsResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "playletTagsList": {
+ "description": "短剧标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "videoTagsList": {
+ "description": "视频标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ }
+ }
+ },
+ "proto.HomeVideoDataResp": {
+ "type": "object",
+ "properties": {
+ "hotList": {
+ "description": "最热视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ },
+ "newestList": {
+ "description": "最新视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ },
+ "recommendList": {
+ "description": "推荐视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfo"
+ }
+ }
+ }
+ },
+ "proto.HotNavItem": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "description": "图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "介绍",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HotSearchItem": {
+ "type": "object",
+ "properties": {
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.LawAndTrustHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "path": {
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.LikePlayletReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isLike"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.LikePlayletResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ },
+ "likeNum": {
+ "description": "点赞数(真实点赞数加后台虚拟点赞数)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.LikeVideoReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isLike"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.LikeVideoResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞、false-取消点赞",
+ "type": "boolean"
+ },
+ "likeNum": {
+ "description": "点赞数(真假点赞数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterInfo": {
+ "type": "object",
+ "properties": {
+ "countBrowse": {
+ "description": "浏览数",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "喜欢数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "剧集ID",
+ "type": "integer"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaContentId": {
+ "description": "媒资中心章节ID",
+ "type": "integer"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "视频播放地址,优先返回水印地址",
+ "type": "string"
+ },
+ "watermarkStatus": {
+ "description": "水印状态 0-未处理 1-已登记 2-处理中 3-已完成",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.WatermarkStatus"
+ }
+ ]
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletDetailInfo": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countComment": {
+ "description": "评论数量",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该短剧",
+ "type": "boolean"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRankData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "rankScore": {
+ "description": "榜单分数,点赞榜为累计展示点赞数,周/月/年榜为对应周期观看数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRankResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.PlayletRecommendItemType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 2
+ ],
+ "x-enum-comments": {
+ "PlayletRecommendItemTypeAdvertise": "广告数据",
+ "PlayletRecommendItemTypePlaylet": "短剧数据"
+ },
+ "x-enum-varnames": [
+ "PlayletRecommendItemTypePlaylet",
+ "PlayletRecommendItemTypeAdvertise"
+ ]
+ },
+ "proto.PlayletRecommendListItem": {
+ "type": "object",
+ "properties": {
+ "advertiseId": {
+ "description": "广告ID,type=2时返回",
+ "type": "string"
+ },
+ "anchor": {
+ "description": "作者或演员",
+ "type": "string"
+ },
+ "contentType": {
+ "description": "内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "countBrowse": {
+ "description": "累计点击量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "真实点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "displayCountLike": {
+ "description": "展示点赞数,真实点赞数加后台虚拟点赞数",
+ "type": "integer"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isNew": {
+ "description": "是否上新 本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "link": {
+ "description": "广告跳转链接,type=2时返回",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID,type=2时返回",
+ "type": "integer"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告,已拼接图片域名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0-下架 1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "type": {
+ "description": "数据类型 0-短剧数据 2-广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendItemType"
+ }
+ ]
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "virtualCountLike": {
+ "description": "后台虚拟点赞数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRecommendListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PostCreateInfo": {
+ "type": "object",
+ "properties": {
+ "background": {
+ "description": "背景",
+ "type": "string"
+ },
+ "createType": {
+ "description": "创作者类型 0=普通 1=官方",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "作者描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "image": {
+ "description": "头像",
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "works": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.QueryActressListResp": {
+ "type": "object",
+ "properties": {
+ "actressInfoList": {
+ "description": "女优列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ },
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryActressWorkListResp": {
+ "type": "object",
+ "properties": {
+ "actressInfo": {
+ "description": "女优信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryFavoriteVideoListResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryPlayletChapterInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "chapterInfo": {
+ "description": "当前剧集详情",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ ]
+ },
+ "chapterList": {
+ "description": "剧集列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotPlayletList": {
+ "description": "热门短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该短剧",
+ "type": "boolean"
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "playletInfo": {
+ "description": "所属短剧详情",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryPlayletInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "chapterList": {
+ "description": "剧集列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterInfo"
+ }
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotPlayletList": {
+ "description": "热门短剧列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该短剧",
+ "type": "boolean"
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "playletInfo": {
+ "description": "短剧详情数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletDetailInfo"
+ }
+ ]
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryVideoInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "announcement": {
+ "description": "公告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AnnouncementInfo"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "config": {
+ "description": "配置信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotVideoList": {
+ "description": "热门视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoData"
+ }
+ },
+ "isFavorite": {
+ "description": "当前登录用户是否已收藏该视频",
+ "type": "boolean"
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该视频",
+ "type": "boolean"
+ },
+ "path": {
+ "description": "当前路径",
+ "type": "string"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "videoInfo": {
+ "description": "视频详情数据",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoDetailInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QueryVideoItemType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 2
+ ],
+ "x-enum-varnames": [
+ "QueryVideoItemTypeVideo",
+ "QueryVideoItemTypeAdvertise"
+ ]
+ },
+ "proto.QueryVideoListItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "advertiseId": {
+ "description": "广告ID,type=2时返回",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "link": {
+ "description": "广告跳转链接,type=2时返回",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID,type=2时返回",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "数据类型:0-视频数据、2-广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.QueryVideoItemType"
+ }
+ ]
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.QueryVideoResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "categoryInfo": {
+ "description": "当前分类信息(含 SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoCategoryInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.QuestionnaireSubmitReq": {
+ "type": "object",
+ "required": [
+ "description",
+ "deviceType",
+ "frequency",
+ "networkType",
+ "operator",
+ "province",
+ "questionType"
+ ],
+ "properties": {
+ "attachments": {
+ "description": "附件列表,最多3个(选填)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/questionnairemod.Attachment"
+ }
+ },
+ "city": {
+ "description": "所在城市(选填)",
+ "type": "string"
+ },
+ "contact": {
+ "description": "联系方式(选填)",
+ "type": "string"
+ },
+ "description": {
+ "description": "问题描述(必填)",
+ "type": "string"
+ },
+ "deviceModel": {
+ "description": "设备型号(选填)",
+ "type": "string"
+ },
+ "deviceType": {
+ "description": "设备类型:iPhone、Android、iPad、其他",
+ "type": "string"
+ },
+ "frequency": {
+ "description": "出现频率:每次都会、经常出现、偶尔出现、仅现一次",
+ "type": "string"
+ },
+ "networkType": {
+ "description": "网络类型:WI-FI、5G、4G、其他",
+ "type": "string"
+ },
+ "operator": {
+ "description": "运营商:电信、联通、移动、广电",
+ "type": "string"
+ },
+ "province": {
+ "description": "所在省份(必填)",
+ "type": "string"
+ },
+ "questionType": {
+ "description": "问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题",
+ "type": "string"
+ },
+ "userId": {
+ "description": "登录用户ID,0表示匿名(选填)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.RankResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "rankData": {
+ "$ref": "#/definitions/proto.HomeRankData"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.RichTextInfo": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.SearchResp": {
+ "type": "object",
+ "properties": {
+ "actress": {
+ "description": "女优资源",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ },
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "bottonTranslations": {
+ "description": "底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "completePath": {
+ "type": "string"
+ },
+ "config": {
+ "description": "配置文件",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "hotSearch": {
+ "description": "热搜",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HotSearchItem"
+ }
+ },
+ "inTitle": {
+ "description": "输入标题",
+ "type": "string"
+ },
+ "inType": {
+ "description": "输入类型 -1=全部 0=视频 1=女优 3=AV 6=短剧",
+ "type": "integer"
+ },
+ "media": {
+ "description": "视频资源",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfoData"
+ }
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "path": {
+ "type": "string"
+ },
+ "playlets": {
+ "description": "短剧资源",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletData"
+ }
+ },
+ "searchRecommend": {
+ "description": "搜索推荐",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "sorts": {
+ "description": "排序选项",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.SearchSortItem"
+ }
+ },
+ "totalSize": {
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.SearchSortItem": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "description": "值",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TagsInfoData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "images": {
+ "description": "背景",
+ "type": "string"
+ },
+ "name": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.TranslationCategory": {
+ "type": "object",
+ "properties": {
+ "categoryType": {
+ "description": "导航类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.TranslationType"
+ }
+ ]
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "translationList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationInfo"
+ }
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TranslationInfo": {
+ "type": "object",
+ "properties": {
+ "displayMode": {
+ "description": "展示方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.DisplayMode"
+ }
+ ]
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.VideoCategoryInfo": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ }
+ }
+ },
+ "proto.VideoData": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoDetailInfo": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "desc": {
+ "description": "作品简介",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "isFavorite": {
+ "description": "当前登录用户是否已收藏该视频",
+ "type": "boolean"
+ },
+ "isLike": {
+ "description": "当前登录用户是否已点赞该视频",
+ "type": "boolean"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频播放信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoInfo": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoInfoData": {
+ "type": "object",
+ "properties": {
+ "actressInfo": {
+ "description": "女优信息",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ActressInfoData"
+ }
+ },
+ "addedTime": {
+ "description": "帖子上架时间",
+ "type": "string"
+ },
+ "advitiseId": {
+ "description": "广告Id",
+ "type": "string"
+ },
+ "bango": {
+ "description": "番号,唯一",
+ "type": "string"
+ },
+ "collectionInfo": {
+ "description": "合集数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoInfoData"
+ }
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createInfo": {
+ "description": "作者信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PostCreateInfo"
+ }
+ ]
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "issuedAt": {
+ "description": "发行时间",
+ "type": "string"
+ },
+ "issuedBrand": {
+ "description": "发行商",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "link": {
+ "description": "跳转链接",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置id",
+ "type": "integer"
+ },
+ "previewVtt": {
+ "description": "雪碧图 vtt",
+ "type": "string"
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TagsInfoData"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoType"
+ }
+ ]
+ },
+ "updatedAt": {
+ "description": "页面更新时间",
+ "type": "string"
+ },
+ "video": {
+ "description": "Nodes []videoinformationmod.Node `json:\"nodes\"` // 节点",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "watchTimes": {
+ "description": "观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoRankResp": {
+ "type": "object",
+ "properties": {
+ "bottonTranslations": {
+ "description": "底部翻译",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ },
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ },
+ "translations": {
+ "description": "导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.VideoRecommendItemType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 2
+ ],
+ "x-enum-comments": {
+ "VideoRecommendItemTypeAdvertise": "广告数据",
+ "VideoRecommendItemTypeVideo": "视频数据"
+ },
+ "x-enum-varnames": [
+ "VideoRecommendItemTypeVideo",
+ "VideoRecommendItemTypeAdvertise"
+ ]
+ },
+ "proto.VideoRecommendListItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "advertiseId": {
+ "description": "广告ID,type=2时返回",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "likeNum": {
+ "description": "点赞(收藏)数(真假点赞(收藏)数之和)",
+ "type": "integer"
+ },
+ "link": {
+ "description": "广告跳转链接,type=2时返回",
+ "type": "string"
+ },
+ "location": {
+ "description": "广告位置ID,type=2时返回",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "数据类型 0-视频数据 2-广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoRecommendItemType"
+ }
+ ]
+ },
+ "watchNum": {
+ "description": "观看数(真假观看数之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoRecommendListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "VideoTypeAdvitise": "广告",
+ "VideoTypeCollection": "合集",
+ "VideoTypeVideo": "视频",
+ "VideoType_AV": "AV视频"
+ },
+ "x-enum-varnames": [
+ "VideoTypeVideo",
+ "VideoTypeCollection",
+ "VideoTypeAdvitise",
+ "VideoType_AV"
+ ]
+ },
+ "proxy.AdvitiseInfo": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "广告id",
+ "type": "string"
+ },
+ "images": {
+ "description": "视频banner",
+ "type": "string"
+ },
+ "link": {
+ "description": "链接",
+ "type": "string"
+ },
+ "location": {
+ "description": "位置id",
+ "type": "integer"
+ },
+ "title": {
+ "description": "广告标题",
+ "type": "string"
+ }
+ }
+ },
+ "pubdef.CommentObjectType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 4
+ ],
+ "x-enum-comments": {
+ "CommentObjectTypePlaylet": "短剧",
+ "CommentObjectTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "CommentObjectTypeVideo",
+ "CommentObjectTypePlaylet"
+ ]
+ },
+ "pubdef.DisplayMode": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "DisplayModeAvatarList": "头像列表",
+ "DisplayModeBigCover": "大封面列表(一大图两列)",
+ "DisplayModeCardCarousel": "卡片层叠轮播",
+ "DisplayModeDefault": "默认(上图下标题多列)",
+ "DisplayModeImageText": "图文列表(左图右标题多列)"
+ },
+ "x-enum-varnames": [
+ "DisplayModeDefault",
+ "DisplayModeCardCarousel",
+ "DisplayModeAvatarList",
+ "DisplayModeImageText",
+ "DisplayModeBigCover"
+ ]
+ },
+ "pubdef.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeMobile": "手机登陆",
+ "LoginTypeQrCode": "二维码登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeMobile",
+ "LoginTypeQrCode",
+ "LoginTypeAccount"
+ ]
+ },
+ "pubdef.SeoValue": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "网站SEO标题",
+ "type": "string"
+ }
+ }
+ },
+ "pubdef.TranslationType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "TranslationTypeBottom": "底部导航",
+ "TranslationTypeSub": "二级导航",
+ "TranslationTypeUrl": "URL模式"
+ },
+ "x-enum-varnames": [
+ "TranslationTypeUrl",
+ "TranslationTypeSub",
+ "TranslationTypeBottom"
+ ]
+ },
+ "pubdef.UserStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "UserStatusDisable": "封禁",
+ "UserStatusNormal": "正常",
+ "UserStatusSlient": "禁言"
+ },
+ "x-enum-varnames": [
+ "UserStatusNormal",
+ "UserStatusDisable",
+ "UserStatusSlient"
+ ]
+ },
+ "questionnairemod.Attachment": {
+ "type": "object",
+ "properties": {
+ "fileType": {
+ "description": "附件类型(img / video)",
+ "type": "string"
+ },
+ "url": {
+ "description": "附件URL地址",
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.ImageUploadData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sourceNameFs": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.ImageUploadResp": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "$ref": "#/definitions/uploadser.ImageUploadData"
+ },
+ "hash": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "time": {
+ "type": "string"
+ },
+ "tip": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.VideoBodyData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "sourceNameFs": {
+ "type": "string"
+ },
+ "taskId": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.VideoBodyResp": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "$ref": "#/definitions/uploadser.VideoBodyData"
+ },
+ "hash": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "time": {
+ "type": "string"
+ },
+ "tip": {
+ "type": "string"
+ }
+ }
+ },
+ "uploadser.VideoOkResp": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "tip": {
+ "type": "string"
+ }
+ }
+ },
+ "videoinformationmod.VideoInfo": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "fileId": {
+ "description": "媒资库ID",
+ "type": "string"
+ },
+ "h265Url": {
+ "description": "H.265 m3u8 播放地址,空字符串表示暂无该编码源",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "H.264 m3u8 播放地址(始终存在)",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "ApiKeyAuth": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ },
+ "BasicAuth": {
+ "type": "basic"
+ },
+ "OAuth2AccessCode": {
+ "type": "oauth2",
+ "flow": "accessCode",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information"
+ }
+ },
+ "OAuth2Application": {
+ "type": "oauth2",
+ "flow": "application",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Implicit": {
+ "type": "oauth2",
+ "flow": "implicit",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Password": {
+ "type": "oauth2",
+ "flow": "password",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "read": "Grants read access",
+ "write": "Grants write access"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/docs/swagger.yaml b/app/docs/swagger.yaml
new file mode 100644
index 0000000..b865749
--- /dev/null
+++ b/app/docs/swagger.yaml
@@ -0,0 +1,4417 @@
+basePath: /
+definitions:
+ actress.Meas:
+ properties:
+ blood:
+ description: 血型
+ type: string
+ bust:
+ description: 胸围 英寸
+ type: integer
+ cup:
+ description: 罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I
+ type: integer
+ dia:
+ description: 直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)
+ type: integer
+ gender:
+ description: 性别 0:女 1:男 2:其他
+ type: integer
+ height:
+ description: 身高 厘米
+ type: integer
+ hip:
+ description: 臀围 英寸
+ type: integer
+ len:
+ description: 长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)
+ type: integer
+ waist:
+ description: 腰围 英寸
+ type: integer
+ weight:
+ description: 体重 公斤
+ type: integer
+ type: object
+ authapi.guestLoginReq:
+ properties:
+ devID:
+ description: 设备 ID(首选)
+ type: string
+ deviceId:
+ description: 设备 ID 第二版本(备用)
+ type: string
+ mac:
+ description: 设备 MAC 地址(备用)
+ type: string
+ type: object
+ authapi.loginReq:
+ properties:
+ account:
+ description: 账号
+ type: string
+ password:
+ description: 明文密码
+ type: string
+ required:
+ - account
+ - password
+ type: object
+ authapi.registerReq:
+ properties:
+ accountNo:
+ description: 账号
+ type: string
+ agentInviteCode:
+ description: 邀请人的邀请码(可选)
+ type: string
+ password:
+ description: 明文密码
+ type: string
+ required:
+ - accountNo
+ - password
+ type: object
+ authapi.updateProfileReq:
+ properties:
+ accountNo:
+ description: 账号(必填,用于定位用户)
+ type: string
+ avatar:
+ description: 新头像 URL(可选)
+ type: string
+ newPassword:
+ description: 新密码明文(修改密码时必填)
+ type: string
+ nikeName:
+ description: 昵称
+ type: string
+ oldPassword:
+ description: 旧密码明文(修改密码时必填)
+ type: string
+ required:
+ - accountNo
+ type: object
+ authser.LoginResp:
+ properties:
+ accountNo:
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ inviteCode:
+ description: 邀请码
+ type: string
+ isNewUser:
+ description: 是否本次请求新创建的用户
+ type: boolean
+ isRegister:
+ description: 是否已完成账号注册
+ type: boolean
+ loginType:
+ allOf:
+ - $ref: '#/definitions/pubdef.LoginType'
+ description: 登录方式 0=设备 3=账号
+ nickName:
+ type: string
+ recharge:
+ description: 当前金币个数
+ type: integer
+ status:
+ $ref: '#/definitions/pubdef.UserStatus'
+ token:
+ type: string
+ userId:
+ type: integer
+ type: object
+ authser.UserInfoResp:
+ properties:
+ accountNo:
+ type: string
+ avatar:
+ type: string
+ inviteCode:
+ type: string
+ isRegister:
+ type: boolean
+ loginType:
+ $ref: '#/definitions/pubdef.LoginType'
+ nickName:
+ type: string
+ recharge:
+ type: integer
+ status:
+ $ref: '#/definitions/pubdef.UserStatus'
+ userId:
+ type: integer
+ type: object
+ contenttagmod.ContentTagType:
+ enum:
+ - 1
+ - 5
+ type: integer
+ x-enum-comments:
+ ContentTagTypePlaylet: 短剧
+ ContentTagTypeVideo: 视频
+ x-enum-varnames:
+ - ContentTagTypeVideo
+ - ContentTagTypePlaylet
+ playletchaptermod.PayType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ PayTypeCoin: 金币
+ PayTypeFree: 免费
+ PayTypeVip: VIP
+ x-enum-varnames:
+ - PayTypeFree
+ - PayTypeCoin
+ - PayTypeVip
+ playletchaptermod.WatermarkStatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ type: integer
+ x-enum-comments:
+ WatermarkStatusDone: 已完成
+ WatermarkStatusFailed: 处理失败
+ WatermarkStatusPending: 未处理(待 mediafile/sync)
+ WatermarkStatusProcessing: 处理中(已提交加水印任务,待回写)
+ WatermarkStatusRegistered: 已登记(原片已登记,待 task/add)
+ x-enum-varnames:
+ - WatermarkStatusPending
+ - WatermarkStatusRegistered
+ - WatermarkStatusProcessing
+ - WatermarkStatusDone
+ - WatermarkStatusFailed
+ playletmod.ContentType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ type: integer
+ x-enum-comments:
+ ContentTypeAlbum: 普通短剧/影集(对应 media_type=6)
+ ContentTypeAnime: 动漫(对应 media_type=1)
+ ContentTypeComic: 漫画短剧(对应 media_type=4)
+ ContentTypeManga: 漫画(对应 media_type=2)
+ ContentTypeTheater: 短剧(对应 media_type=5)
+ x-enum-varnames:
+ - ContentTypeTheater
+ - ContentTypeAlbum
+ - ContentTypeComic
+ - ContentTypeAnime
+ - ContentTypeManga
+ playletmod.PayType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ PayTypeCoin: 金币
+ PayTypeFree: 免费
+ PayTypeVip: VIP
+ x-enum-varnames:
+ - PayTypeFree
+ - PayTypeCoin
+ - PayTypeVip
+ playletmod.Status:
+ enum:
+ - 0
+ - 1
+ type: integer
+ x-enum-comments:
+ StatusDown: 下架
+ StatusUp: 上架
+ x-enum-varnames:
+ - StatusDown
+ - StatusUp
+ proto.ActressInfo:
+ properties:
+ alias:
+ description: 别名
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ hotValue:
+ description: 热度值
+ type: integer
+ id:
+ description: Id
+ type: integer
+ slug:
+ description: slug
+ type: string
+ works:
+ description: 作品数
+ type: integer
+ type: object
+ proto.ActressInfoData:
+ properties:
+ alias:
+ description: 别名
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ bornDate:
+ description: 出生日期
+ type: string
+ comments:
+ description: 评论条数
+ type: integer
+ cover:
+ description: 封面大图
+ type: string
+ debutDate:
+ description: 出道日期
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ hotValue:
+ description: 热度值
+ type: integer
+ id:
+ description: Id
+ type: integer
+ meas:
+ allOf:
+ - $ref: '#/definitions/actress.Meas'
+ description: 三围
+ measValue:
+ description: 三围值
+ type: string
+ office:
+ description: 事务所
+ type: string
+ region:
+ description: 地区/国家
+ type: string
+ score:
+ description: 评分
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ showTimes:
+ description: 展示次数
+ type: integer
+ slug:
+ description: slug
+ type: string
+ watchTimes:
+ description: 观看次数
+ type: integer
+ works:
+ description: 作品数
+ type: integer
+ type: object
+ proto.AdvitiseData:
+ properties:
+ AList:
+ description: 固定12广告位
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ BList:
+ description: 固定13广告位
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ detailTopList:
+ description: 详情页顶部广告
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ detailbannerList:
+ description: 详情banner
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ dialogList:
+ description: 弹窗广告
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ floatingList:
+ description: 悬浮小方块
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ friendList:
+ description: 友链
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ homeList:
+ description: 首页列表
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ launchScreenList:
+ description: 启动页广告
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ mustAppList:
+ description: 必备精品
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ nineGridList:
+ description: 9宫格
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ postList:
+ description: 列表穿插广告(视频、短剧、女优列表使用)
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ titleAppList:
+ description: 标题广告(APP名称)
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ videoList:
+ description: 视频列表
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ type: object
+ proto.AnnouncementInfo:
+ properties:
+ appAddr:
+ description: 吃瓜app (免费吃瓜 每日更新)
+ type: string
+ foreverUrlAddr:
+ description: 永久海外地址(翻墙访问 永不迷路)
+ type: string
+ newUrlAddr:
+ description: 新国内地址
+ type: string
+ newUrlAddr1:
+ description: 备用地址1
+ type: string
+ newUrlAddr2:
+ description: 备用地址1
+ type: string
+ pdfAddr:
+ description: 回家的路PDF
+ type: string
+ translationAddr:
+ description: 应用导航
+ type: string
+ videoInfo:
+ description: 热门信息
+ items:
+ $ref: '#/definitions/proto.VideoInfoData'
+ type: array
+ type: object
+ proto.AppCategoryInfo:
+ properties:
+ appList:
+ description: 应用列表
+ items:
+ $ref: '#/definitions/proto.AppInfoData'
+ type: array
+ code:
+ description: 分类编码(特殊值:12=广告位A 13=广告位B)
+ type: integer
+ id:
+ description: 分类ID
+ type: integer
+ intro:
+ description: 分类简介
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ title:
+ description: 分类标题
+ type: string
+ type: object
+ proto.AppContentTagItem:
+ properties:
+ bindCount:
+ description: 当前查询类型绑定数量,不传type时为各类型绑定数量合计
+ type: integer
+ id:
+ description: 标签ID
+ type: integer
+ name:
+ description: 标签名称
+ type: string
+ playletCount:
+ description: 绑定短剧数量
+ type: integer
+ rank:
+ description: 排序
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ types:
+ description: 支持类型 1-视频 5-短剧
+ items:
+ $ref: '#/definitions/contenttagmod.ContentTagType'
+ type: array
+ videoCount:
+ description: 绑定视频数量
+ type: integer
+ type: object
+ proto.AppContentTagListResp:
+ properties:
+ list:
+ description: 统一标签列表
+ items:
+ $ref: '#/definitions/proto.AppContentTagItem'
+ type: array
+ type: object
+ proto.AppInfoData:
+ properties:
+ advitiseId:
+ description: 广告ID(广告条目时返回)
+ type: string
+ categoryId:
+ description: 所属分类ID
+ type: integer
+ description:
+ description: 详细描述
+ type: string
+ id:
+ description: 应用ID
+ type: integer
+ intro:
+ description: 简介
+ type: string
+ location:
+ description: 广告位置ID
+ type: integer
+ logo:
+ description: Logo 图片地址
+ type: string
+ name:
+ description: 应用名称
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ url:
+ description: 跳转链接(已含渠道码)
+ type: string
+ type: object
+ proto.AppListResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ category:
+ allOf:
+ - $ref: '#/definitions/proto.AppCategoryInfo'
+ description: 当前分类(含应用列表)
+ categoryList:
+ description: 应用分类列表(含各分类下的应用列表)
+ items:
+ $ref: '#/definitions/proto.AppCategoryInfo'
+ type: array
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ path:
+ description: 当前路径
+ type: string
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.AppRichTextItemResp:
+ properties:
+ content:
+ description: 内容
+ type: string
+ desc:
+ description: 标题(app)
+ type: string
+ id:
+ description: 富文本ID
+ type: integer
+ title:
+ description: 标题
+ type: string
+ type: object
+ proto.AppRichTextListResp:
+ properties:
+ list:
+ description: 富文本内容列表
+ items:
+ $ref: '#/definitions/proto.AppRichTextItemResp'
+ type: array
+ type: object
+ proto.AppTranslationInfo:
+ properties:
+ categoryList:
+ items:
+ $ref: '#/definitions/proto.TranslationCategory'
+ type: array
+ type: object
+ proto.CommentInfo:
+ properties:
+ avatar:
+ description: 用户头像
+ type: string
+ commentCount:
+ description: 该评论回复数量
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ description: 评论ID
+ type: integer
+ level:
+ description: 评论级别
+ type: integer
+ objectId:
+ description: 对象ID
+ type: integer
+ parentsId:
+ description: 上级评论ID
+ type: integer
+ replyUserName:
+ description: 回复名称
+ type: string
+ subComment:
+ description: 子评论
+ items:
+ $ref: '#/definitions/proto.CommentInfo'
+ type: array
+ text:
+ description: 评论内容
+ type: string
+ userName:
+ description: 用户名称
+ type: string
+ type: object
+ proto.CommentListResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.CommentSendReq:
+ properties:
+ objectId:
+ description: 对象ID
+ type: integer
+ objectType:
+ allOf:
+ - $ref: '#/definitions/pubdef.CommentObjectType'
+ description: 对象类型 0=视频 4=短剧
+ parentsId:
+ description: 上级评论ID
+ type: integer
+ text:
+ description: 评论内容
+ type: string
+ type: object
+ proto.ConfigInfo:
+ properties:
+ appAddr:
+ description: APP下载地址(落地页)
+ type: string
+ channelCode:
+ description: 渠道码
+ type: string
+ dhDescription:
+ description: 导航SEO描述
+ type: string
+ dhKeyworld:
+ description: 导航SEO关键字
+ items:
+ type: string
+ type: array
+ dhNewUrlAddr1:
+ description: 导航页-线路1
+ type: string
+ dhNewUrlAddr2:
+ description: 导航页-线路2
+ type: string
+ dhNewUrlAddr3:
+ description: 导航页-线路3
+ type: string
+ dhSiteName:
+ description: 导航SEO网站名称
+ type: string
+ dhTitle:
+ description: 导航SEO标题
+ type: string
+ enableH265:
+ description: 是否允许客户端使用 H265 地址播放
+ type: boolean
+ foreverUrlAddr:
+ description: 永久地址
+ type: string
+ freeVPN:
+ description: 免费vpn链接
+ type: string
+ gitAddr:
+ description: git地址
+ type: string
+ gmailAddr:
+ description: 邮箱地址
+ type: string
+ imgAppUsCdn:
+ description: 图片CDN(海外app使用)
+ type: string
+ imgCdn:
+ description: 图片CDN
+ type: string
+ m3u8DayRateLimitMax:
+ description: 每 IP 每天最多不同正片数
+ type: integer
+ mediaAppUsCdn:
+ description: 视频CDN(海外app使用)
+ type: string
+ newUrlAddr:
+ description: 最新地址
+ type: string
+ newUrlAddr1:
+ description: 最新地址(备用地址1)
+ type: string
+ newUrlAddr2:
+ description: 最新地址(备用地址2)
+ type: string
+ pdfAddr:
+ description: 回家的路PDF
+ type: string
+ potaoGroup:
+ description: 土豆群
+ type: string
+ qq:
+ description: qq群
+ type: string
+ reportSdkConfig:
+ description: 数据上报SDK域名配置
+ type: string
+ telegram:
+ description: 商务tg
+ type: string
+ telegramAdd:
+ description: 加入我们tg
+ type: string
+ tgGroup:
+ description: tg群
+ type: string
+ translationAddr:
+ description: 应用导航
+ type: string
+ tsCdn:
+ description: 视频CDN
+ type: string
+ twitterAddr:
+ description: 推特地址
+ type: string
+ useIngAddress:
+ description: 所有使用中的域名
+ items:
+ type: string
+ type: array
+ webAddr:
+ description: 网站SEO网站地址
+ type: string
+ webDescription:
+ description: 网站SEO描述
+ type: string
+ webKeyworld:
+ description: 网站SEO关键字
+ items:
+ type: string
+ type: array
+ webSiteName:
+ description: 网站SEO网站名称
+ type: string
+ webTitle:
+ description: 网站SEO标题
+ type: string
+ weiChart:
+ description: 微信群
+ type: string
+ type: object
+ proto.ContentData:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签id
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.DHHtmlData:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ completePath:
+ type: string
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置文件
+ hotNavs:
+ description: 热门导航
+ items:
+ $ref: '#/definitions/proto.HotNavItem'
+ type: array
+ hotSearch:
+ description: 热搜
+ items:
+ $ref: '#/definitions/proto.HotSearchItem'
+ type: array
+ path:
+ type: string
+ richTextInfo:
+ allOf:
+ - $ref: '#/definitions/proto.RichTextInfo'
+ description: 富文本
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.DataResp:
+ properties:
+ actressInfoList:
+ description: 女优信息
+ items:
+ $ref: '#/definitions/proto.ActressInfo'
+ type: array
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ homeBannerList:
+ description: 首页banner信息列表
+ items:
+ $ref: '#/definitions/proto.HomeBannerInfo'
+ type: array
+ path:
+ description: 当前路径
+ type: string
+ playletData:
+ allOf:
+ - $ref: '#/definitions/proto.HomePlayletDataResp'
+ description: 首页短剧数据
+ videoData:
+ allOf:
+ - $ref: '#/definitions/proto.HomeVideoDataResp'
+ description: 首页视频数据
+ type: object
+ proto.FavoriteVideoListItem:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签id
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.FavoriteVideoReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ isFavorite:
+ description: 是否收藏,true-收藏、false-取消收藏
+ type: boolean
+ required:
+ - id
+ - isFavorite
+ type: object
+ proto.FavoriteVideoResp:
+ properties:
+ id:
+ description: ID
+ type: integer
+ isFavorite:
+ description: 是否收藏,true-收藏、false-取消收藏
+ type: boolean
+ type: object
+ proto.FooterBrand:
+ properties:
+ content:
+ description: 品牌标语
+ type: string
+ title:
+ description: 品牌标题
+ type: string
+ type: object
+ proto.FooterContact:
+ properties:
+ name:
+ description: 联系方式名称
+ type: string
+ value:
+ description: 联系方式值
+ type: string
+ type: object
+ proto.FooterInfo:
+ properties:
+ brand:
+ allOf:
+ - $ref: '#/definitions/proto.FooterBrand'
+ description: 品牌信息
+ contactList:
+ description: 联系方式
+ items:
+ $ref: '#/definitions/proto.FooterContact'
+ type: array
+ navList:
+ description: 底部导航
+ items:
+ $ref: '#/definitions/proto.TranslationCategory'
+ type: array
+ type: object
+ proto.FooterInfoResp:
+ properties:
+ footerInfo:
+ allOf:
+ - $ref: '#/definitions/proto.FooterInfo'
+ description: 底部信息
+ type: object
+ proto.HomeBannerInfo:
+ properties:
+ id:
+ description: ID
+ type: integer
+ image:
+ description: 图片,已拼接图片域名
+ type: string
+ name:
+ description: 名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ status:
+ description: 状态 true-启用 false-停用
+ type: boolean
+ url:
+ description: URL
+ type: string
+ verticalImage:
+ description: 竖版图片,已拼接图片域名
+ type: string
+ type: object
+ proto.HomeBaseResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.HomeHtmlData:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ completePath:
+ description: 请求路径
+ type: string
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置文件
+ hotSearch:
+ description: 热搜
+ items:
+ $ref: '#/definitions/proto.HotSearchItem'
+ type: array
+ path:
+ type: string
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.HomePlayletDataResp:
+ properties:
+ hotList:
+ description: 最热短剧列表
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ newestList:
+ description: 最新短剧列表
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ recommendList:
+ description: 推荐短剧列表
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ type: object
+ proto.HomeRankData:
+ properties:
+ month:
+ description: 月榜
+ items:
+ $ref: '#/definitions/proto.ContentData'
+ type: array
+ total:
+ description: 总榜
+ items:
+ $ref: '#/definitions/proto.ContentData'
+ type: array
+ week:
+ description: 周榜
+ items:
+ $ref: '#/definitions/proto.ContentData'
+ type: array
+ year:
+ description: 年榜
+ items:
+ $ref: '#/definitions/proto.ContentData'
+ type: array
+ type: object
+ proto.HomeTagsResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ playletTagsList:
+ description: 短剧标签
+ items:
+ $ref: '#/definitions/proto.TagsInfoData'
+ type: array
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ videoTagsList:
+ description: 视频标签
+ items:
+ $ref: '#/definitions/proto.TagsInfoData'
+ type: array
+ type: object
+ proto.HomeVideoDataResp:
+ properties:
+ hotList:
+ description: 最热视频列表
+ items:
+ $ref: '#/definitions/proto.VideoInfo'
+ type: array
+ newestList:
+ description: 最新视频列表
+ items:
+ $ref: '#/definitions/proto.VideoInfo'
+ type: array
+ recommendList:
+ description: 推荐视频列表
+ items:
+ $ref: '#/definitions/proto.VideoInfo'
+ type: array
+ type: object
+ proto.HotNavItem:
+ properties:
+ icon:
+ description: 图标
+ type: string
+ id:
+ description: ID
+ type: integer
+ intro:
+ description: 介绍
+ type: string
+ title:
+ description: 标题
+ type: string
+ url:
+ description: 链接
+ type: string
+ type: object
+ proto.HotSearchItem:
+ properties:
+ hotValue:
+ description: 热度值
+ type: integer
+ id:
+ description: ID
+ type: integer
+ title:
+ description: 标题
+ type: string
+ type: object
+ proto.LawAndTrustHtmlData:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ completePath:
+ type: string
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置文件
+ hotSearch:
+ description: 热搜
+ items:
+ $ref: '#/definitions/proto.HotSearchItem'
+ type: array
+ path:
+ type: string
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.LikePlayletReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞、false-取消点赞
+ type: boolean
+ required:
+ - id
+ - isLike
+ type: object
+ proto.LikePlayletResp:
+ properties:
+ id:
+ description: ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞、false-取消点赞
+ type: boolean
+ likeNum:
+ description: 点赞数(真实点赞数加后台虚拟点赞数)
+ type: integer
+ type: object
+ proto.LikeVideoReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞、false-取消点赞
+ type: boolean
+ required:
+ - id
+ - isLike
+ type: object
+ proto.LikeVideoResp:
+ properties:
+ id:
+ description: ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞、false-取消点赞
+ type: boolean
+ likeNum:
+ description: 点赞数(真假点赞数之和)
+ type: integer
+ type: object
+ proto.PlayletChapterInfo:
+ properties:
+ countBrowse:
+ description: 浏览数
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 喜欢数
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ currentEpisode:
+ description: 第几集
+ type: integer
+ hash:
+ description: 媒体hash
+ type: string
+ height:
+ description: 高
+ type: integer
+ id:
+ description: 剧集ID
+ type: integer
+ md5:
+ description: 媒体md5
+ type: string
+ mediaContentId:
+ description: 媒资中心章节ID
+ type: integer
+ mediaSize:
+ description: 资源大小
+ type: integer
+ mediaTime:
+ description: 影片长度(秒)
+ type: integer
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletchaptermod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ playletId:
+ description: 短剧ID
+ type: integer
+ ratio:
+ description: 宽高比
+ type: number
+ singleCoins:
+ description: 单集金币
+ type: integer
+ title:
+ description: 章节名
+ type: string
+ updatedAt:
+ description: 修改时间
+ type: string
+ videoUrl:
+ description: 视频播放地址,优先返回水印地址
+ type: string
+ watermarkStatus:
+ allOf:
+ - $ref: '#/definitions/playletchaptermod.WatermarkStatus'
+ description: 水印状态 0-未处理 1-已登记 2-处理中 3-已完成
+ width:
+ description: 宽
+ type: integer
+ type: object
+ proto.PlayletData:
+ properties:
+ anchor:
+ description: 作者或演员
+ type: string
+ contentType:
+ allOf:
+ - $ref: '#/definitions/playletmod.ContentType'
+ description: 内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画
+ countBrowse:
+ description: 累计点击量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 真实点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ displayCountLike:
+ description: 展示点赞数,真实点赞数加后台虚拟点赞数
+ type: integer
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧id
+ type: integer
+ isNew:
+ description: 是否上新 本周内上架为true
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ mediaCreatedAt:
+ description: 媒资中心创建时间
+ type: string
+ mediaUpdatedAt:
+ description: 媒资中心更新时间
+ type: string
+ onlineAt:
+ description: 上架时间
+ type: string
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ preview:
+ description: 预告,已拼接图片域名
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ singleCoins:
+ description: 单集金币,支付方式为金币时生效
+ type: integer
+ slug:
+ description: slug
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 状态 0-下架 1-上架
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ virtualCountLike:
+ description: 后台虚拟点赞数
+ type: integer
+ type: object
+ proto.PlayletDetailInfo:
+ properties:
+ anchor:
+ description: 作者或演员
+ type: string
+ contentType:
+ allOf:
+ - $ref: '#/definitions/playletmod.ContentType'
+ description: 内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画
+ countBrowse:
+ description: 累计点击量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countComment:
+ description: 评论数量
+ type: integer
+ countLike:
+ description: 真实点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ displayCountLike:
+ description: 展示点赞数,真实点赞数加后台虚拟点赞数
+ type: integer
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧id
+ type: integer
+ isLike:
+ description: 当前登录用户是否已点赞该短剧
+ type: boolean
+ isNew:
+ description: 是否上新 本周内上架为true
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ mediaCreatedAt:
+ description: 媒资中心创建时间
+ type: string
+ mediaUpdatedAt:
+ description: 媒资中心更新时间
+ type: string
+ onlineAt:
+ description: 上架时间
+ type: string
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ preview:
+ description: 预告,已拼接图片域名
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ singleCoins:
+ description: 单集金币,支付方式为金币时生效
+ type: integer
+ slug:
+ description: slug
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 状态 0-下架 1-上架
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ virtualCountLike:
+ description: 后台虚拟点赞数
+ type: integer
+ type: object
+ proto.PlayletRankData:
+ properties:
+ anchor:
+ description: 作者或演员
+ type: string
+ contentType:
+ allOf:
+ - $ref: '#/definitions/playletmod.ContentType'
+ description: 内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画
+ countBrowse:
+ description: 累计点击量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 真实点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ displayCountLike:
+ description: 展示点赞数,真实点赞数加后台虚拟点赞数
+ type: integer
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧id
+ type: integer
+ isNew:
+ description: 是否上新 本周内上架为true
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ mediaCreatedAt:
+ description: 媒资中心创建时间
+ type: string
+ mediaUpdatedAt:
+ description: 媒资中心更新时间
+ type: string
+ onlineAt:
+ description: 上架时间
+ type: string
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ preview:
+ description: 预告,已拼接图片域名
+ type: string
+ rankScore:
+ description: 榜单分数,点赞榜为累计展示点赞数,周/月/年榜为对应周期观看数
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ singleCoins:
+ description: 单集金币,支付方式为金币时生效
+ type: integer
+ slug:
+ description: slug
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 状态 0-下架 1-上架
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ virtualCountLike:
+ description: 后台虚拟点赞数
+ type: integer
+ type: object
+ proto.PlayletRankResp:
+ properties:
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.PlayletRecommendItemType:
+ enum:
+ - 0
+ - 2
+ type: integer
+ x-enum-comments:
+ PlayletRecommendItemTypeAdvertise: 广告数据
+ PlayletRecommendItemTypePlaylet: 短剧数据
+ x-enum-varnames:
+ - PlayletRecommendItemTypePlaylet
+ - PlayletRecommendItemTypeAdvertise
+ proto.PlayletRecommendListItem:
+ properties:
+ advertiseId:
+ description: 广告ID,type=2时返回
+ type: string
+ anchor:
+ description: 作者或演员
+ type: string
+ contentType:
+ allOf:
+ - $ref: '#/definitions/playletmod.ContentType'
+ description: 内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画
+ countBrowse:
+ description: 累计点击量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 真实点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ displayCountLike:
+ description: 展示点赞数,真实点赞数加后台虚拟点赞数
+ type: integer
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧id
+ type: integer
+ isNew:
+ description: 是否上新 本周内上架为true
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ link:
+ description: 广告跳转链接,type=2时返回
+ type: string
+ location:
+ description: 广告位置ID,type=2时返回
+ type: integer
+ mediaCreatedAt:
+ description: 媒资中心创建时间
+ type: string
+ mediaUpdatedAt:
+ description: 媒资中心更新时间
+ type: string
+ onlineAt:
+ description: 上架时间
+ type: string
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ preview:
+ description: 预告,已拼接图片域名
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ singleCoins:
+ description: 单集金币,支付方式为金币时生效
+ type: integer
+ slug:
+ description: slug
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 状态 0-下架 1-上架
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ type:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletRecommendItemType'
+ description: 数据类型 0-短剧数据 2-广告
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ virtualCountLike:
+ description: 后台虚拟点赞数
+ type: integer
+ type: object
+ proto.PlayletRecommendListResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.PostCreateInfo:
+ properties:
+ background:
+ description: 背景
+ type: string
+ createType:
+ description: 创作者类型 0=普通 1=官方
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 作者描述
+ type: string
+ id:
+ type: integer
+ image:
+ description: 头像
+ type: string
+ name:
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ works:
+ type: integer
+ type: object
+ proto.QueryActressListResp:
+ properties:
+ actressInfoList:
+ description: 女优列表
+ items:
+ $ref: '#/definitions/proto.ActressInfoData'
+ type: array
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ path:
+ description: 当前路径
+ type: string
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.QueryActressWorkListResp:
+ properties:
+ actressInfo:
+ allOf:
+ - $ref: '#/definitions/proto.ActressInfoData'
+ description: 女优信息
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.QueryFavoriteVideoListResp:
+ properties:
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.QueryPlayletChapterInfoResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ chapterInfo:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletChapterInfo'
+ description: 当前剧集详情
+ chapterList:
+ description: 剧集列表
+ items:
+ $ref: '#/definitions/proto.PlayletChapterInfo'
+ type: array
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ hotPlayletList:
+ description: 热门短剧列表
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ isLike:
+ description: 当前登录用户是否已点赞该短剧
+ type: boolean
+ path:
+ description: 当前路径
+ type: string
+ playletInfo:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletData'
+ description: 所属短剧详情
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.QueryPlayletInfoResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ chapterList:
+ description: 剧集列表
+ items:
+ $ref: '#/definitions/proto.PlayletChapterInfo'
+ type: array
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ hotPlayletList:
+ description: 热门短剧列表
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ isLike:
+ description: 当前登录用户是否已点赞该短剧
+ type: boolean
+ path:
+ description: 当前路径
+ type: string
+ playletInfo:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletDetailInfo'
+ description: 短剧详情数据
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.QueryVideoInfoResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ announcement:
+ allOf:
+ - $ref: '#/definitions/proto.AnnouncementInfo'
+ description: 公告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置信息
+ hotVideoList:
+ description: 热门视频列表
+ items:
+ $ref: '#/definitions/proto.VideoData'
+ type: array
+ isFavorite:
+ description: 当前登录用户是否已收藏该视频
+ type: boolean
+ isLike:
+ description: 当前登录用户是否已点赞该视频
+ type: boolean
+ path:
+ description: 当前路径
+ type: string
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ videoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.VideoDetailInfo'
+ description: 视频详情数据
+ type: object
+ proto.QueryVideoItemType:
+ enum:
+ - 0
+ - 2
+ type: integer
+ x-enum-varnames:
+ - QueryVideoItemTypeVideo
+ - QueryVideoItemTypeAdvertise
+ proto.QueryVideoListItem:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ advertiseId:
+ description: 广告ID,type=2时返回
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ link:
+ description: 广告跳转链接,type=2时返回
+ type: string
+ location:
+ description: 广告位置ID,type=2时返回
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签id
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ type:
+ allOf:
+ - $ref: '#/definitions/proto.QueryVideoItemType'
+ description: 数据类型:0-视频数据、2-广告
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.QueryVideoResp:
+ properties:
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ categoryInfo:
+ allOf:
+ - $ref: '#/definitions/proto.VideoCategoryInfo'
+ description: 当前分类信息(含 SEO)
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.QuestionnaireSubmitReq:
+ properties:
+ attachments:
+ description: 附件列表,最多3个(选填)
+ items:
+ $ref: '#/definitions/questionnairemod.Attachment'
+ type: array
+ city:
+ description: 所在城市(选填)
+ type: string
+ contact:
+ description: 联系方式(选填)
+ type: string
+ description:
+ description: 问题描述(必填)
+ type: string
+ deviceModel:
+ description: 设备型号(选填)
+ type: string
+ deviceType:
+ description: 设备类型:iPhone、Android、iPad、其他
+ type: string
+ frequency:
+ description: 出现频率:每次都会、经常出现、偶尔出现、仅现一次
+ type: string
+ networkType:
+ description: 网络类型:WI-FI、5G、4G、其他
+ type: string
+ operator:
+ description: 运营商:电信、联通、移动、广电
+ type: string
+ province:
+ description: 所在省份(必填)
+ type: string
+ questionType:
+ description: 问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题
+ type: string
+ userId:
+ description: 登录用户ID,0表示匿名(选填)
+ type: integer
+ required:
+ - description
+ - deviceType
+ - frequency
+ - networkType
+ - operator
+ - province
+ - questionType
+ type: object
+ proto.RankResp:
+ properties:
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ rankData:
+ $ref: '#/definitions/proto.HomeRankData'
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.RichTextInfo:
+ properties:
+ content:
+ description: 内容
+ type: string
+ desc:
+ description: 标题(app)
+ type: string
+ id:
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ title:
+ description: 标题
+ type: string
+ type: object
+ proto.SearchResp:
+ properties:
+ actress:
+ description: 女优资源
+ items:
+ $ref: '#/definitions/proto.ActressInfoData'
+ type: array
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部导航
+ completePath:
+ type: string
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 配置文件
+ hotSearch:
+ description: 热搜
+ items:
+ $ref: '#/definitions/proto.HotSearchItem'
+ type: array
+ inTitle:
+ description: 输入标题
+ type: string
+ inType:
+ description: 输入类型 -1=全部 0=视频 1=女优 3=AV 6=短剧
+ type: integer
+ media:
+ description: 视频资源
+ items:
+ $ref: '#/definitions/proto.VideoInfoData'
+ type: array
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ path:
+ type: string
+ playlets:
+ description: 短剧资源
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ searchRecommend:
+ description: 搜索推荐
+ items:
+ type: string
+ type: array
+ sorts:
+ description: 排序选项
+ items:
+ $ref: '#/definitions/proto.SearchSortItem'
+ type: array
+ totalSize:
+ type: integer
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.SearchSortItem:
+ properties:
+ key:
+ description: 值
+ type: string
+ title:
+ description: 标题
+ type: string
+ type: object
+ proto.TagsInfoData:
+ properties:
+ id:
+ type: integer
+ images:
+ description: 背景
+ type: string
+ name:
+ description: 帖子标题
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ works:
+ description: 作品数量
+ type: integer
+ type: object
+ proto.TranslationCategory:
+ properties:
+ categoryType:
+ allOf:
+ - $ref: '#/definitions/pubdef.TranslationType'
+ description: 导航类型
+ icon:
+ description: Icon
+ type: string
+ id:
+ type: integer
+ name:
+ description: 名称
+ type: string
+ translationList:
+ items:
+ $ref: '#/definitions/proto.TranslationInfo'
+ type: array
+ urlValue:
+ description: URl路径
+ type: string
+ type: object
+ proto.TranslationInfo:
+ properties:
+ displayMode:
+ allOf:
+ - $ref: '#/definitions/pubdef.DisplayMode'
+ description: 展示方式
+ id:
+ description: ID
+ type: integer
+ isFriendAds:
+ description: 是否是友链广告位
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ urlValue:
+ description: URl路径
+ type: string
+ type: object
+ proto.VideoCategoryInfo:
+ properties:
+ desc:
+ description: 描述
+ type: string
+ id:
+ type: integer
+ name:
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ title:
+ description: APP显示
+ type: string
+ type: object
+ proto.VideoData:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签id
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.VideoDetailInfo:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ desc:
+ description: 作品简介
+ type: string
+ id:
+ description: id
+ type: integer
+ isFavorite:
+ description: 当前登录用户是否已收藏该视频
+ type: boolean
+ isLike:
+ description: 当前登录用户是否已点赞该视频
+ type: boolean
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签
+ items:
+ $ref: '#/definitions/proto.TagsInfoData'
+ type: array
+ title:
+ description: 标题
+ type: string
+ video:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.VideoInfo'
+ description: 视频播放信息
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.VideoInfo:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签id
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.VideoInfoData:
+ properties:
+ actressInfo:
+ description: 女优信息
+ items:
+ $ref: '#/definitions/proto.ActressInfoData'
+ type: array
+ addedTime:
+ description: 帖子上架时间
+ type: string
+ advitiseId:
+ description: 广告Id
+ type: string
+ bango:
+ description: 番号,唯一
+ type: string
+ collectionInfo:
+ description: 合集数据
+ items:
+ $ref: '#/definitions/proto.VideoInfoData'
+ type: array
+ coverImg:
+ description: 封面
+ type: string
+ createInfo:
+ allOf:
+ - $ref: '#/definitions/proto.PostCreateInfo'
+ description: 作者信息
+ desc:
+ description: 详细描述
+ type: string
+ id:
+ type: integer
+ isHot:
+ description: 是否热门
+ type: boolean
+ issuedAt:
+ description: 发行时间
+ type: string
+ issuedBrand:
+ description: 发行商
+ type: string
+ keyworld:
+ description: 关键字
+ items:
+ type: string
+ type: array
+ link:
+ description: 跳转链接
+ type: string
+ location:
+ description: 广告位置id
+ type: integer
+ previewVtt:
+ description: 雪碧图 vtt
+ type: string
+ showTimes:
+ description: 展示次数
+ type: integer
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签
+ items:
+ $ref: '#/definitions/proto.TagsInfoData'
+ type: array
+ title:
+ description: 帖子标题
+ type: string
+ type:
+ allOf:
+ - $ref: '#/definitions/proto.VideoType'
+ description: 类型
+ updatedAt:
+ description: 页面更新时间
+ type: string
+ video:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.VideoInfo'
+ description: Nodes []videoinformationmod.Node `json:"nodes"` //
+ 节点
+ watchTimes:
+ description: 观看次数
+ type: integer
+ type: object
+ proto.VideoRankResp:
+ properties:
+ bottonTranslations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 底部翻译
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ translations:
+ allOf:
+ - $ref: '#/definitions/proto.AppTranslationInfo'
+ description: 导航
+ type: object
+ proto.VideoRecommendItemType:
+ enum:
+ - 0
+ - 2
+ type: integer
+ x-enum-comments:
+ VideoRecommendItemTypeAdvertise: 广告数据
+ VideoRecommendItemTypeVideo: 视频数据
+ x-enum-varnames:
+ - VideoRecommendItemTypeVideo
+ - VideoRecommendItemTypeAdvertise
+ proto.VideoRecommendListItem:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ advertiseId:
+ description: 广告ID,type=2时返回
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ likeNum:
+ description: 点赞(收藏)数(真假点赞(收藏)数之和)
+ type: integer
+ link:
+ description: 广告跳转链接,type=2时返回
+ type: string
+ location:
+ description: 广告位置ID,type=2时返回
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ tags:
+ description: 标签id
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ type:
+ allOf:
+ - $ref: '#/definitions/proto.VideoRecommendItemType'
+ description: 数据类型 0-视频数据 2-广告
+ watchNum:
+ description: 观看数(真假观看数之和)
+ type: integer
+ type: object
+ proto.VideoRecommendListResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.VideoType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ VideoType_AV: AV视频
+ VideoTypeAdvitise: 广告
+ VideoTypeCollection: 合集
+ VideoTypeVideo: 视频
+ x-enum-varnames:
+ - VideoTypeVideo
+ - VideoTypeCollection
+ - VideoTypeAdvitise
+ - VideoType_AV
+ proxy.AdvitiseInfo:
+ properties:
+ id:
+ description: 广告id
+ type: string
+ images:
+ description: 视频banner
+ type: string
+ link:
+ description: 链接
+ type: string
+ location:
+ description: 位置id
+ type: integer
+ title:
+ description: 广告标题
+ type: string
+ type: object
+ pubdef.CommentObjectType:
+ enum:
+ - 0
+ - 4
+ type: integer
+ x-enum-comments:
+ CommentObjectTypePlaylet: 短剧
+ CommentObjectTypeVideo: 视频
+ x-enum-varnames:
+ - CommentObjectTypeVideo
+ - CommentObjectTypePlaylet
+ pubdef.DisplayMode:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ type: integer
+ x-enum-comments:
+ DisplayModeAvatarList: 头像列表
+ DisplayModeBigCover: 大封面列表(一大图两列)
+ DisplayModeCardCarousel: 卡片层叠轮播
+ DisplayModeDefault: 默认(上图下标题多列)
+ DisplayModeImageText: 图文列表(左图右标题多列)
+ x-enum-varnames:
+ - DisplayModeDefault
+ - DisplayModeCardCarousel
+ - DisplayModeAvatarList
+ - DisplayModeImageText
+ - DisplayModeBigCover
+ pubdef.LoginType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ LoginTypeAccount: 账号登陆
+ LoginTypeDevice: 设备登陆
+ LoginTypeMobile: 手机登陆
+ LoginTypeQrCode: 二维码登陆
+ x-enum-varnames:
+ - LoginTypeDevice
+ - LoginTypeMobile
+ - LoginTypeQrCode
+ - LoginTypeAccount
+ pubdef.SeoValue:
+ properties:
+ description:
+ description: 网站SEO描述
+ type: string
+ keyworld:
+ description: 网站SEO关键字
+ items:
+ type: string
+ type: array
+ title:
+ description: 网站SEO标题
+ type: string
+ type: object
+ pubdef.TranslationType:
+ enum:
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ TranslationTypeBottom: 底部导航
+ TranslationTypeSub: 二级导航
+ TranslationTypeUrl: URL模式
+ x-enum-varnames:
+ - TranslationTypeUrl
+ - TranslationTypeSub
+ - TranslationTypeBottom
+ pubdef.UserStatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ UserStatusDisable: 封禁
+ UserStatusNormal: 正常
+ UserStatusSlient: 禁言
+ x-enum-varnames:
+ - UserStatusNormal
+ - UserStatusDisable
+ - UserStatusSlient
+ questionnairemod.Attachment:
+ properties:
+ fileType:
+ description: 附件类型(img / video)
+ type: string
+ url:
+ description: 附件URL地址
+ type: string
+ type: object
+ uploadser.ImageUploadData:
+ properties:
+ id:
+ type: string
+ sourceNameFs:
+ type: string
+ type: object
+ uploadser.ImageUploadResp:
+ properties:
+ code:
+ type: integer
+ data:
+ $ref: '#/definitions/uploadser.ImageUploadData'
+ hash:
+ type: boolean
+ msg:
+ type: string
+ time:
+ type: string
+ tip:
+ type: string
+ type: object
+ uploadser.VideoBodyData:
+ properties:
+ id:
+ type: string
+ sourceNameFs:
+ type: string
+ taskId:
+ type: string
+ type: object
+ uploadser.VideoBodyResp:
+ properties:
+ code:
+ type: integer
+ data:
+ $ref: '#/definitions/uploadser.VideoBodyData'
+ hash:
+ type: boolean
+ msg:
+ type: string
+ time:
+ type: string
+ tip:
+ type: string
+ type: object
+ uploadser.VideoOkResp:
+ properties:
+ code:
+ type: integer
+ msg:
+ type: string
+ tip:
+ type: string
+ type: object
+ videoinformationmod.VideoInfo:
+ properties:
+ cover:
+ description: 视频封面
+ type: string
+ fileId:
+ description: 媒资库ID
+ type: string
+ h265Url:
+ description: H.265 m3u8 播放地址,空字符串表示暂无该编码源
+ type: string
+ height:
+ description: 分辨率高(视频才会存在)
+ type: integer
+ preMp4FileNameFs:
+ description: 预览MP4文件
+ type: string
+ taskId:
+ description: 视频ID(视频才会存在)
+ type: string
+ unencryptedPreMp4:
+ description: 不加密的预览MP4
+ type: string
+ url:
+ description: H.264 m3u8 播放地址(始终存在)
+ type: string
+ videoTime:
+ description: 影片长度
+ type: integer
+ width:
+ description: 分辨率宽(视频才会存在)
+ type: integer
+ type: object
+host: 视频站-api.lcncctn.com
+info:
+ contact: {}
+ description: 这是视频站APP接口服务.
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+ title: 视频站APP接口
+ version: "1.0"
+paths:
+ /api/app/actress/list:
+ get:
+ consumes:
+ - application/json
+ description: 返回导航、广告和按热度值降序排列的女优列表
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.QueryActressListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 女优榜单列表 -- by 王鸣
+ tags:
+ - 女优相关接口
+ /api/app/actress/work/list:
+ get:
+ consumes:
+ - application/json
+ description: 查询指定女优详情和作品列表,支持上架时间/观看量/收藏量升降序,每间隔8条作品数据插入一个广告
+ parameters:
+ - description: 上架时间升序/降序(1-升序 2降序)
+ in: query
+ name: addedTimeSort
+ type: integer
+ - description: 女优ID
+ in: query
+ name: id
+ type: integer
+ - description: 按收藏(点赞)量升序/降序(1-升序 2降序)
+ in: query
+ name: likesSort
+ type: integer
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 女优slug,非空时优先按slug解析女优ID
+ in: query
+ name: slug
+ type: string
+ - description: 按观看量升序/降序(1-升序 2降序)
+ in: query
+ name: watchTimesSort
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.QueryActressWorkListResp'
+ - properties:
+ list:
+ items:
+ allOf:
+ - $ref: '#/definitions/proto.QueryVideoListItem'
+ - properties:
+ VideoInfo:
+ $ref: '#/definitions/proto.VideoInfo'
+ type: object
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 指定女优详情和作品列表 -- by 王鸣
+ tags:
+ - 女优相关接口
+ /api/app/applist/list:
+ get:
+ consumes:
+ - application/json
+ description: 返回应用分类列表,每个分类下含应用信息列表;categoryId=0 返回第一个分类
+ parameters:
+ - description: 当前分类ID,0表示默认第一个分类
+ in: query
+ name: categoryId
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.AppListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询应用列表
+ tags:
+ - 应用列表
+ /api/app/auth/getInfo:
+ get:
+ consumes:
+ - application/json
+ description: 根据当前登录 token 查询用户基础信息
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/authser.UserInfoResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 获取当前登录用户信息
+ tags:
+ - 用户认证
+ /api/app/auth/login:
+ post:
+ consumes:
+ - application/json
+ description: 账号均可作为登录凭证
+ parameters:
+ - description: 登录参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/authapi.loginReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 登录成功,返回 token 及用户基本信息
+ schema:
+ $ref: '#/definitions/authser.LoginResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 登录
+ tags:
+ - 用户认证
+ /api/app/auth/profile:
+ post:
+ consumes:
+ - application/json
+ description: 账号必填;avatar、oldPassword+newPassword 均为可选,至少提供一项。修改密码时旧密码和新密码必须同时提供。
+ parameters:
+ - description: 修改资料参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/authapi.updateProfileReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 修改用户资料(头像 / 密码,可任意组合)
+ tags:
+ - 用户认证
+ /api/app/auth/register:
+ post:
+ consumes:
+ - application/json
+ description: 已登录游客传入账号密码完成升级;未登录用户直接注册新账号。
+ parameters:
+ - description: 注册参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/authapi.registerReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 注册成功,返回 token 及用户基本信息
+ schema:
+ $ref: '#/definitions/authser.LoginResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 注册
+ tags:
+ - 用户认证
+ /api/app/comment/list:
+ get:
+ consumes:
+ - application/json
+ description: 按对象查询树形评论列表
+ parameters:
+ - description: 对象ID
+ in: query
+ name: objectId
+ type: integer
+ - description: 对象类型 0=视频 4=短剧
+ enum:
+ - 0
+ - 4
+ in: query
+ name: objectType
+ type: integer
+ x-enum-comments:
+ CommentObjectTypePlaylet: 短剧
+ CommentObjectTypeVideo: 视频
+ x-enum-varnames:
+ - CommentObjectTypeVideo
+ - CommentObjectTypePlaylet
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.CommentListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommentInfo'
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 评论列表 -- by 王鸣
+ tags:
+ - 评论APP
+ /api/app/comment/send:
+ post:
+ consumes:
+ - application/json
+ description: 发布评论
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.CommentSendReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 发布评论 -- by 王鸣
+ tags:
+ - 评论APP
+ /api/app/config/footer/info:
+ get:
+ consumes:
+ - application/json
+ description: 返回APP底部品牌、底部导航和联系方式
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.FooterInfoResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询APP底部信息 -- by 王鸣
+ tags:
+ - app配置相关接口
+ /api/app/config/richtext/list:
+ get:
+ consumes:
+ - application/json
+ description: 返回启用状态的富文本内容,支持按ids或titles过滤
+ parameters:
+ - description: 富文本ID,多个用英文逗号分隔
+ in: query
+ name: ids
+ type: string
+ - description: 富文本标题,多个用英文逗号分隔
+ in: query
+ name: titles
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.AppRichTextListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询APP富文本内容列表 -- by 王鸣
+ tags:
+ - app配置相关接口
+ /api/app/content/tag/list:
+ get:
+ consumes:
+ - application/json
+ description: 返回启用且有绑定内容数量的统一标签,contentTagType非必填,按绑定内容数量倒序排序
+ parameters:
+ - description: 内容类型 1-视频 5-短剧,不传返回全部有绑定数量的标签
+ enum:
+ - 1
+ - 5
+ in: query
+ name: contentTagType
+ type: integer
+ x-enum-comments:
+ ContentTagTypePlaylet: 短剧
+ ContentTagTypeVideo: 视频
+ x-enum-varnames:
+ - ContentTagTypeVideo
+ - ContentTagTypePlaylet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.AppContentTagListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询APP统一标签列表 -- by 王鸣
+ tags:
+ - app配置相关接口
+ /api/app/home:
+ get:
+ consumes:
+ - application/json
+ description: 首页分类数据
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.DataResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 首页 --by 旺鸣
+ tags:
+ - 首页
+ /api/app/home/base:
+ get:
+ consumes:
+ - application/json
+ description: 只返回翻译、配置、底部翻译、广告和公告
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeBaseResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 首页公共数据 --by 旺鸣
+ tags:
+ - 首页
+ /api/app/home/tags:
+ get:
+ consumes:
+ - application/json
+ description: 返回视频、短剧的标签信息,以及基础导航、配置、广告和公告
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeTagsResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 首页标签数据 --by 旺鸣
+ tags:
+ - 首页
+ /api/app/login/guest:
+ post:
+ consumes:
+ - application/json
+ description: 按设备 ID 查找已有游客账号,找不到则自动注册。若请求携带有效 token 则优先复用该用户。
+ parameters:
+ - description: 设备信息
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/authapi.guestLoginReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 登录成功
+ schema:
+ $ref: '#/definitions/authser.LoginResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 游客登录
+ tags:
+ - 用户认证
+ /api/app/navigation/header:
+ get:
+ consumes:
+ - application/json
+ description: 查询导航类型为URL模式和二级导航的头部导航配置
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.AppTranslationInfo'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询头部导航配置 -- by 王鸣
+ tags:
+ - app导航相关接口
+ /api/app/playlet/chapter/info:
+ get:
+ consumes:
+ - application/json
+ description: 根据剧集ID查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态
+ parameters:
+ - description: 剧集ID
+ in: query
+ name: id
+ required: true
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.QueryPlayletChapterInfoResp'
+ - properties:
+ chapterInfo:
+ $ref: '#/definitions/proto.PlayletChapterInfo'
+ chapterList:
+ items:
+ $ref: '#/definitions/proto.PlayletChapterInfo'
+ type: array
+ hotPlayletList:
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ playletInfo:
+ $ref: '#/definitions/proto.PlayletData'
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端短剧剧集详情 -- by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/info:
+ get:
+ consumes:
+ - application/json
+ description: 查询短剧详情、剧集列表、热门短剧列表和点赞状态
+ parameters:
+ - description: 短剧ID
+ in: query
+ name: id
+ type: integer
+ - description: 短剧slug,非空时优先按slug解析短剧ID
+ in: query
+ name: slug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.QueryPlayletInfoResp'
+ - properties:
+ chapterList:
+ items:
+ $ref: '#/definitions/proto.PlayletChapterInfo'
+ type: array
+ hotPlayletList:
+ items:
+ $ref: '#/definitions/proto.PlayletData'
+ type: array
+ playletInfo:
+ $ref: '#/definitions/proto.PlayletDetailInfo'
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 短剧详情 -- by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/like:
+ post:
+ consumes:
+ - application/json
+ description: 当前登录用户点赞或取消点赞指定短剧,isLike=true表示点赞,isLike=false表示取消点赞
+ parameters:
+ - description: 点赞或取消点赞请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.LikePlayletReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.LikePlayletResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 点赞或取消点赞短剧 -- by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/rank/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜
+ in: query
+ name: rankPeriod
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletRankResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.PlayletRankData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 短剧排行榜分页列表 -- by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/recommend/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告
+ parameters:
+ - description: 短剧分类ID,不传查询全部分类
+ in: query
+ name: categoryId
+ type: integer
+ - description: 分类slug,非空时优先按slug解析分类ID
+ in: query
+ name: categorySlug
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 排序方式 1-推荐 2-最新 3-最热
+ in: query
+ name: sortList
+ type: integer
+ - description: 标签ID,不传查询全部标签
+ in: query
+ name: tagId
+ type: integer
+ - description: 标签slug,非空时优先按slug解析标签ID
+ in: query
+ name: tagSlug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletRecommendListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.PlayletRecommendListItem'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端推荐短剧列表 -- by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/tag/{id}:
+ get:
+ consumes:
+ - application/json
+ description: 根据统一标签ID分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告
+ parameters:
+ - description: 标签ID
+ in: path
+ name: id
+ required: true
+ type: integer
+ - description: 页码,默认1
+ in: query
+ name: pageNum
+ type: integer
+ - description: 每页数量,默认20,最大100
+ in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletRecommendListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.PlayletRecommendListItem'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端根据标签ID查询短剧列表 -- by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/questionnaire/submit:
+ post:
+ consumes:
+ - application/json
+ description: 用户提交问题反馈问卷
+ parameters:
+ - description: 提交参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.QuestionnaireSubmitReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 提交问卷
+ tags:
+ - 问卷调查APP
+ /api/app/richtext:
+ get:
+ description: 从缓存中查询启用状态的富文本信息
+ parameters:
+ - description: 富文本内部标题
+ in: query
+ name: title
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.RichTextInfo'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 按 title 查询富文本
+ tags:
+ - 富文本
+ /api/app/upload/image:
+ post:
+ consumes:
+ - multipart/form-data
+ description: 将图片转发至存储服务 /media/web/autopublish/image
+ parameters:
+ - description: 图片文件
+ in: formData
+ name: file
+ required: true
+ type: file
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/uploadser.ImageUploadResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ summary: 图片上传
+ tags:
+ - 上传
+ /api/app/upload/videoBody:
+ post:
+ consumes:
+ - multipart/form-data
+ description: 将视频分片转发至存储服务 /media/web/autopublish/videoBody
+ parameters:
+ - description: 视频任务ID
+ in: formData
+ name: taskId
+ required: true
+ type: string
+ - description: 当前分片序号,从0开始
+ in: formData
+ name: pos
+ required: true
+ type: string
+ - description: 上传用户名,为空默认 questionnaires
+ in: formData
+ name: uploadUserName
+ type: string
+ - description: 分片文件数据
+ in: formData
+ name: file
+ required: true
+ type: file
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/uploadser.VideoBodyResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ summary: 视频分片上传
+ tags:
+ - 上传
+ /api/app/upload/videoOk:
+ post:
+ consumes:
+ - application/json
+ description: 所有分片上传完毕后调用,转发至存储服务 /media/web/autopublish/videoOk
+ parameters:
+ - description: '{ \'
+ in: body
+ name: param
+ required: true
+ schema:
+ type: object
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/uploadser.VideoOkResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ summary: 视频分片上传完成通知
+ tags:
+ - 上传
+ /api/app/video/favorite:
+ post:
+ consumes:
+ - application/json
+ description: 当前登录用户收藏或取消收藏指定视频,isFavorite=true表示收藏,isFavorite=false表示取消收藏
+ parameters:
+ - description: 收藏或取消收藏请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.FavoriteVideoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.FavoriteVideoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 收藏或取消收藏视频 --by 旺鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/favorite/list:
+ get:
+ consumes:
+ - application/json
+ description: 当前登录用户收藏视频分页列表,按收藏时间倒序排序
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.QueryFavoriteVideoListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.FavoriteVideoListItem'
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 当前登录用户收藏视频分页列表 --by 旺鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/info:
+ get:
+ consumes:
+ - application/json
+ description: 查询视频详情、热门视频列表、收藏状态和点赞状态
+ parameters:
+ - in: query
+ name: id
+ type: integer
+ - description: 视频slug,非空时优先按slug解析视频ID
+ in: query
+ name: slug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.QueryVideoInfoResp'
+ - properties:
+ hotVideoList:
+ items:
+ $ref: '#/definitions/proto.VideoData'
+ type: array
+ videoInfo:
+ $ref: '#/definitions/proto.VideoDetailInfo'
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 视频详情 -- by 王鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/like:
+ post:
+ consumes:
+ - application/json
+ description: 当前登录用户点赞或取消点赞指定视频,isLike=true表示点赞,isLike=false表示取消点赞
+ parameters:
+ - description: 点赞或取消点赞请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.LikeVideoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.LikeVideoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 点赞或取消点赞视频 -- by 王鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/more:
+ get:
+ consumes:
+ - application/json
+ description: 根据分类id查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
+ parameters:
+ - description: 视频分类id
+ in: query
+ name: categoryId
+ type: integer
+ - description: 分类slug,非空时优先按slug解析分类ID
+ in: query
+ name: categorySlug
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.QueryVideoResp'
+ - properties:
+ list:
+ items:
+ allOf:
+ - $ref: '#/definitions/proto.QueryVideoListItem'
+ - properties:
+ VideoInfo:
+ $ref: '#/definitions/proto.VideoInfo'
+ type: object
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 根据分类id查询视频列表 --by 王鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/rank:
+ get:
+ consumes:
+ - application/json
+ description: 视频收藏排行榜
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.RankResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 视频收藏排行榜
+ tags:
+ - 视频相关接口
+ /api/app/video/rank/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询视频排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜
+ in: query
+ name: rankPeriod
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.VideoRankResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.ContentData'
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 视频排行榜分页列表 -- by 王鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/recommend/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
+ parameters:
+ - description: 分类id,不传查询全部分类id
+ in: query
+ name: categoryId
+ type: integer
+ - description: 分类slug,非空时优先按slug解析分类ID
+ in: query
+ name: categorySlug
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 排序方式 1-推荐 2-最新 3-最热
+ in: query
+ name: sortList
+ type: integer
+ - description: 标签ID,不传查询全部标签
+ in: query
+ name: tagId
+ type: integer
+ - description: 标签slug,非空时优先按slug解析标签ID
+ in: query
+ name: tagSlug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.VideoRecommendListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.VideoRecommendListItem'
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: app端推荐视频列表 -- by 王鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/tag/{id}:
+ get:
+ consumes:
+ - application/json
+ description: 根据统一标签ID分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
+ parameters:
+ - description: 标签ID
+ in: path
+ name: id
+ required: true
+ type: integer
+ - description: 页码,默认1
+ in: query
+ name: pageNum
+ type: integer
+ - description: 每页数量,默认20,最大100
+ in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.VideoRecommendListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.VideoRecommendListItem'
+ type: array
+ type: object
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: app端根据标签ID查询视频列表 -- by 王鸣
+ tags:
+ - 视频相关接口
+ /businessCooperation:
+ get:
+ consumes:
+ - application/json
+ description: 导航
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 导航
+ tags:
+ - 导航
+ /consulting:
+ get:
+ consumes:
+ - application/json
+ description: 招募
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 招募
+ tags:
+ - 招募
+ /consultinginfo/:id:
+ get:
+ consumes:
+ - application/json
+ description: 招募详情
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 招募详情
+ tags:
+ - 招募详情
+ /dh:
+ get:
+ consumes:
+ - application/json
+ description: 导航
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.DHHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 导航
+ tags:
+ - 导航
+ /dmca:
+ get:
+ consumes:
+ - application/json
+ description: dmca
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.LawAndTrustHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: dmca
+ tags:
+ - dmca
+ /privacyPolicy:
+ get:
+ consumes:
+ - application/json
+ description: 隐私策略
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.LawAndTrustHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 隐私策略
+ tags:
+ - 隐私策略
+ /search:
+ get:
+ consumes:
+ - application/json
+ description: '搜索,支持查询参数:title=关键词&page=1&type=0&sort=default(不传type=全部;type:
+ 0=视频 1=女优 3=AV 6=短剧;sort: default/hot/new)'
+ parameters:
+ - description: 搜索关键词
+ in: query
+ name: title
+ type: string
+ - description: 页码,默认1
+ in: query
+ name: page
+ type: integer
+ - description: 媒体类型,不传=全部,0=视频 1=女优 3=AV 6=短剧
+ in: query
+ name: type
+ type: integer
+ - description: 排序方式 default/hot/new,默认default
+ in: query
+ name: sort
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.SearchResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 搜索
+ tags:
+ - 搜索
+ /servicesProject:
+ get:
+ consumes:
+ - application/json
+ description: 导航
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 导航
+ tags:
+ - 导航
+ /statement:
+ get:
+ consumes:
+ - application/json
+ description: 合规声明
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.LawAndTrustHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 合规声明
+ tags:
+ - 合规声明
+ /useragreement:
+ get:
+ consumes:
+ - application/json
+ description: 用户协议
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.LawAndTrustHtmlData'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 用户协议
+ tags:
+ - 用户协议
+schemes:
+- http
+- https
+securityDefinitions:
+ ApiKeyAuth:
+ in: header
+ name: Authorization
+ type: apiKey
+ BasicAuth:
+ type: basic
+ OAuth2AccessCode:
+ authorizationUrl: https://example.com/oauth/authorize
+ flow: accessCode
+ scopes:
+ admin: Grants read and write access to administrative information
+ tokenUrl: https://example.com/oauth/token
+ type: oauth2
+ OAuth2Application:
+ flow: application
+ scopes:
+ admin: Grants read and write access to administrative information
+ write: Grants write access
+ tokenUrl: https://example.com/oauth/token
+ type: oauth2
+ OAuth2Implicit:
+ authorizationUrl: https://example.com/oauth/authorize
+ flow: implicit
+ scopes:
+ admin: Grants read and write access to administrative information
+ write: Grants write access
+ type: oauth2
+ OAuth2Password:
+ flow: password
+ scopes:
+ admin: Grants read and write access to administrative information
+ read: Grants read access
+ write: Grants write access
+ tokenUrl: https://example.com/oauth/token
+ type: oauth2
+swagger: "2.0"
diff --git a/app/main.go b/app/main.go
new file mode 100644
index 0000000..c77ead9
--- /dev/null
+++ b/app/main.go
@@ -0,0 +1,55 @@
+//go:generate swag init --parseDependency --parseDepth 4
+//go:generate mtool replicate-module -m contest
+package main
+
+import (
+ "os"
+ "videoweb/app/server"
+)
+
+// @title 视频站APP接口
+// @version 1.0
+// @description 这是视频站APP接口服务.
+// @termsOfService
+
+// @contact.name
+// @contact.url
+// @contact.email
+
+// @license.name Apache 2.0
+// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
+
+// @host 视频站-api.lcncctn.com
+// @BasePath /
+// @Schemes http https
+
+// @securityDefinitions.basic BasicAuth
+
+// @securityDefinitions.apikey ApiKeyAuth
+// @in header
+// @name Authorization
+
+// @securitydefinitions.oauth2.application OAuth2Application
+// @tokenUrl https://example.com/oauth/token
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.implicit OAuth2Implicit
+// @authorizationUrl https://example.com/oauth/authorize
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.password OAuth2Password
+// @tokenUrl https://example.com/oauth/token
+// @scope.read Grants read access
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
+// @tokenUrl https://example.com/oauth/token
+// @authorizationUrl https://example.com/oauth/authorize
+// @scope.admin Grants read and write access to administrative information
+
+func main() {
+ server.Run(os.Args)
+}
diff --git a/app/middleware/auth/http.go b/app/middleware/auth/http.go
new file mode 100644
index 0000000..4378329
--- /dev/null
+++ b/app/middleware/auth/http.go
@@ -0,0 +1,140 @@
+package auth
+
+import (
+ "net/http"
+ "strings"
+ "videoweb/app/appg"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// gin 验证中间件
+
+var (
+ // 白名单,不需要验证token的api
+ whiteList = []string{
+ "/swagger",
+ "/api/app/ping/check",
+ "/api/app/ping/config",
+ "/api/app/health/ping",
+ "/api/app/login/guest",
+ "/api/app/auth/register",
+ "/api/app/auth/login",
+ "/api/app/user/qrcode/info",
+ "/api/app/upload/images",
+ "/api/app/home",
+ "/api/app/richtext",
+ "/api/app/content/tag/list",
+ "/api/app/config/footer/info",
+ "/api/app/config/richtext/list",
+ "/api/app/navigation/header",
+ "/api/app/comment/list",
+ "/api/app/video/more",
+ "/api/app/video/list",
+ "/api/app/video/category/list",
+ "/api/app/video/info",
+ "/api/app/video/recommend/list",
+ "/api/app/video/guess/list",
+ "/api/app/video/rank/list",
+ "/api/app/playlet/recommend/list",
+ "/api/app/playlet/guess/list",
+ "/api/app/playlet/home",
+ "/api/app/playlet/home-config/list",
+ "/api/app/playlet/topic/detail",
+ "/api/app/playlet/theme/list",
+ "/api/app/playlet/list",
+ "/api/app/playlet/content/list",
+ "/api/app/playlet/tag/",
+ "/api/app/playlet/info",
+ "/api/app/playlet/chapter/info",
+ "/api/app/playlet/rank/list",
+ "/api/app/actress",
+ "/api/app/upload/videoBody",
+ "/api/app/upload/videoOk",
+ "/api/app/upload/image",
+ "/search",
+ "/api/app/applist",
+ "/h5/m3u8",
+ "/enkey",
+ "/newenkey",
+ "/lsjenkey",
+ "/dh",
+ "/businessCooperation/",
+ "/servicesProject/",
+ "/consulting/",
+ "/consultinginfo/",
+ }
+)
+
+var (
+ tokener = utils.NewTokenRedis()
+)
+
+func Auth(ctx *gin.Context) {
+ dotoken := func() srvdef.CodeType {
+ token := ctx.Request.Header.Get("Authorization")
+ // 去掉Bearer,如果有
+ token = utils.StripBearerPrefixFromTokenString(token)
+
+ if token == "" {
+ for _, w := range whiteList {
+ if strings.Contains(ctx.Request.URL.Path, w) {
+ return srvdef.Success
+ }
+ }
+
+ log.Error("Auth:token is empty", log.Any("sysType", ctx.GetString(consts.CtxSysType)))
+ return srvdef.InvalidToken
+ }
+
+ userId, newToken, err := utils.ParseUserToken(token, appg.Conf.JwtKey)
+ if err != nil {
+ for _, w := range whiteList {
+ if strings.Contains(ctx.Request.URL.Path, w) {
+ return srvdef.Success
+ }
+ }
+
+ log.Error("Auth:ParseUserToken", log.Any("token", token), log.E(err))
+ return srvdef.InvalidToken
+ }
+
+ has, err := tokener.Exists(userId, token)
+ if err != nil {
+ log.Error("Auth:Exists", log.Any("userId", userId), log.Any("token", token), log.E(err))
+ return srvdef.InvalidToken
+ }
+ if !has {
+ log.Error("Auth:Exists:token not exists", log.Any("userId", userId), log.Any("token", token), log.E(err))
+ return srvdef.InvalidToken
+ }
+
+ if len(newToken) > 0 {
+ err = tokener.Save(userId, newToken)
+ if err != nil {
+ log.Error("Auth:Save", log.Any("userId", userId), log.Any("token", newToken), log.E(err))
+ return srvdef.StoreLevel
+ }
+ ctx.Writer.Header().Set("Refresh-Authorization", newToken)
+ }
+
+ ctx.Set(consts.CtxUserID, userId)
+ ctx.Set(consts.CtxToken, token)
+
+ return srvdef.Success
+ }
+
+ errType := dotoken()
+ if errType != srvdef.Success {
+ // log.Info("Auth bad request", log.Any("Path", ctx.Request.URL.Path), log.Any("ip", ctx.ClientIP()))
+ ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
+ "code": errType,
+ "msg": errType.Msg(),
+ })
+ }
+
+}
diff --git a/app/middleware/cors/cors.go b/app/middleware/cors/cors.go
new file mode 100644
index 0000000..e1fc226
--- /dev/null
+++ b/app/middleware/cors/cors.go
@@ -0,0 +1,135 @@
+package cors
+
+import (
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+type Options struct {
+ Origin *[]string //传入空指针,表示允许"*", 传入空数组表示禁止跨域
+ Methods []string //Methods
+ AllowedHeaders []string //AllowHeaders
+ ExposedHeaders []string //exposeHeader
+ Credentials bool //cookie
+ MaxAge int64 //缓存MaxAge
+ PreflightContinue bool //在遇到Options的时候,继续,而不是返回请求
+ OptionsSuccessStatus int //Options时,返回的状态码,默认204
+}
+
+func configureOrigin(o *Options, c *gin.Context) map[string]string {
+ reqOrigin := c.GetHeader("Origin")
+ headers := make(map[string]string)
+ if o.Origin == nil {
+ headers["Access-Control-Allow-Origin"] = "*"
+ } else {
+ isAllowed := false
+ for _, allowed := range *o.Origin {
+ if allowed == reqOrigin {
+ isAllowed = true
+ break
+ }
+ }
+ if isAllowed {
+ headers["Access-Control-Allow-Origin"] = reqOrigin
+ } else {
+ headers["Access-Control-Allow-Origin"] = strconv.FormatBool(false)
+ }
+ headers["Vary"] = "Origin"
+ }
+ return headers
+}
+
+func configureMethods(o *Options) map[string]string {
+ headers := make(map[string]string)
+ if len(o.Methods) > 0 {
+ headers["Access-Control-Allow-Methods"] = strings.ToUpper(strings.Join(o.Methods, ","))
+ }
+ return headers
+}
+
+func configureCredentials(o *Options) map[string]string {
+ headers := make(map[string]string)
+ if o.Credentials {
+ headers["Access-Control-Allow-Credentials"] = strconv.FormatBool(true)
+ }
+ return headers
+}
+
+func configureAllowedHeaders(o *Options, c *gin.Context) map[string]string {
+ headers := make(map[string]string)
+ if len(o.AllowedHeaders) > 0 {
+ headers["Access-Control-Allow-Headers"] = strings.Join(o.AllowedHeaders, ",")
+ }
+ return headers
+}
+
+func configureExposedHeaders(o *Options) map[string]string {
+ var headers = make(map[string]string)
+ if len(o.ExposedHeaders) > 0 {
+ headers["Access-Control-Expose-Headers"] = strings.Join(o.ExposedHeaders, ",")
+ }
+ return headers
+}
+
+func configureMaxAge(o *Options) map[string]string {
+ var headers = make(map[string]string)
+ if o.MaxAge >= 0 {
+ headers["Access-Control-Max-Age"] = strconv.FormatInt(o.MaxAge, 10)
+ }
+ return headers
+}
+
+func mergeMap(m ...map[string]string) map[string]string {
+ r := make(map[string]string)
+ for _, t := range m {
+ for k, v := range t {
+ r[k] = v
+ }
+ }
+ return r
+}
+
+func applyHeader(c *gin.Context, h map[string]string) {
+ for k, v := range h {
+ c.Header(k, v)
+ }
+}
+
+// Cors 跨域处理
+func Cors(o *Options) gin.HandlerFunc {
+ if o.OptionsSuccessStatus == 0 {
+ o.OptionsSuccessStatus = http.StatusNoContent
+ }
+ return func(c *gin.Context) {
+ method := c.Request.Method
+ //c.Header("Access-Control-Allow-Origin", "*")
+ //c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,Content-Length,Etag,Content-Range,Accept-Ranges,User-Agent,Range,Bucket,Content-Disposition,Signature,X-Forwarded-For,X-Real-Ip")
+ if method == "OPTIONS" {
+ h := mergeMap(
+ configureOrigin(o, c),
+ configureCredentials(o),
+ configureMethods(o),
+ configureAllowedHeaders(o, c),
+ configureMaxAge(o),
+ configureExposedHeaders(o),
+ )
+ applyHeader(c, h)
+ if o.PreflightContinue {
+ c.Next()
+ } else {
+ c.Header("Content-Length", "0")
+ c.AbortWithStatus(o.OptionsSuccessStatus)
+ }
+ } else {
+ h := mergeMap(
+ configureOrigin(o, c),
+ configureCredentials(o),
+ configureExposedHeaders(o),
+ )
+ applyHeader(c, h)
+ c.Next()
+ }
+ }
+}
diff --git a/app/middleware/ipblacklist/ipblacklist.go b/app/middleware/ipblacklist/ipblacklist.go
new file mode 100644
index 0000000..2cbbcdf
--- /dev/null
+++ b/app/middleware/ipblacklist/ipblacklist.go
@@ -0,0 +1,16 @@
+package ipblacklist
+
+import (
+ "errors"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "videoweb/app/service/configser"
+)
+
+func IpBlackListMiddleware(ctx *gin.Context) {
+ ip := ctx.ClientIP()
+ if configser.CheckIpBlack(ip) {
+ ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
+ return
+ }
+}
\ No newline at end of file
diff --git a/app/middleware/ipstat/ipstat.go b/app/middleware/ipstat/ipstat.go
new file mode 100644
index 0000000..247d4fe
--- /dev/null
+++ b/app/middleware/ipstat/ipstat.go
@@ -0,0 +1,94 @@
+package ipstat
+
+import (
+ "net"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ "github.com/gin-gonic/gin"
+)
+
+// IpStatMiddleware 记录访客真实 IP,按天存入 Redis SET 实现去重计数。
+// key 格式:ipstat:{appId}:{yyyymmdd},TTL 49 小时(保证昨日数据次日仍可读)。
+func IpStatMiddleware(ctx *gin.Context) {
+ ctx.Next()
+
+ normalized := normalizeIP(ipaddr.ResolveClientIP(ctx))
+ if normalized == "" {
+ return
+ }
+
+ date := time.Now().In(utils.LocBJ).Format("20060102")
+ key := redisconst.IpStatKey(pubdef.AppId, date)
+
+ isNew, err := redisSAddNX(key, normalized)
+ if err != nil {
+ log.Warn("IpStatMiddleware SAdd failed", log.Any("key", key), log.E(err))
+ return
+ }
+ if isNew {
+ // 仅新增成员时刷新 TTL,避免每次请求都执行 EXPIRE
+ if err := redis.Expire(key, redisconst.IpStatExpire); err != nil {
+ log.Warn("IpStatMiddleware Expire failed", log.Any("key", key), log.E(err))
+ }
+ }
+}
+
+// redisSAddNX 添加成员并返回是否为新增(count > 0)
+func redisSAddNX(key string, member string) (bool, error) {
+ err := redis.SAdd(key, member)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
+// normalizeIP 将原始 IP 标准化:
+// - 去掉 IPv4-mapped IPv6 前缀(::ffff:)
+// - 过滤私有/回环/链路本地地址
+// - 非法或私有 IP 返回空字符串
+func normalizeIP(raw string) string {
+ ip := net.ParseIP(raw)
+ if ip == nil {
+ return ""
+ }
+ if v4 := ip.To4(); v4 != nil {
+ ip = v4
+ }
+ if isPrivateIP(ip) {
+ return ""
+ }
+ return ip.String()
+}
+
+var privateRanges = []net.IPNet{
+ parseCIDR("10.0.0.0/8"),
+ parseCIDR("172.16.0.0/12"),
+ parseCIDR("192.168.0.0/16"),
+ parseCIDR("127.0.0.0/8"),
+ parseCIDR("::1/128"),
+ parseCIDR("fe80::/10"),
+ parseCIDR("fc00::/7"),
+}
+
+func parseCIDR(s string) net.IPNet {
+ _, network, err := net.ParseCIDR(s)
+ if err != nil {
+ panic(err)
+ }
+ return *network
+}
+
+func isPrivateIP(ip net.IP) bool {
+ for _, r := range privateRanges {
+ if r.Contains(ip) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/app/middleware/monitor/define.go b/app/middleware/monitor/define.go
new file mode 100644
index 0000000..482f318
--- /dev/null
+++ b/app/middleware/monitor/define.go
@@ -0,0 +1,69 @@
+package monitor
+
+import "time"
+
+type APIStatis struct {
+ //路径
+ Path string `json:"path"`
+ //消耗最大时间rawdata
+ MaxTimeRawData string `json:"maxTimeRawData"`
+ //调用次数
+ CallNum int64 `json:"callNum"`
+ //最小耗时
+ MinTime int64 `json:"minTime"`
+ //最大耗时
+ MaxTime int64 `json:"maxTime"`
+ //总耗时
+ TotalTime int64 `json:"totalTime"`
+ //每次调用时间
+ TimePerCall int64 `json:"timePerCall"`
+ //每秒调用次数
+ CallPerSec int64 `json:"callPerSec"`
+ //开始时间
+ Start time.Time `json:"start"`
+ //是否发送过报警
+ Send int `json:"send"`
+}
+
+// APIStatis 接口统计
+// 每天统计数
+type LogicStatis struct {
+ //API总调用次数
+ ApiCallNum int64
+ //API调用总耗时
+ ApiTotalTime int64
+ //开始时间
+ Start time.Time
+ //接口访问次数
+ ApisStatis map[string]*APIStatis
+}
+
+type SPType string
+
+const (
+ SP_ApiCall SPType = "apicall"
+ SP_GetStatis SPType = "getStatis"
+)
+
+// StatisReq 统计请求数据格式
+type AppStatisReq struct {
+ Time time.Time `json:"time"`
+ //获取的api数量
+ ApiNum int64 `json:"apiNum"`
+}
+
+type StatisParam struct {
+ //调用类型
+ Type SPType
+ Param1 string
+ Param2 int64
+ Param3 string
+}
+
+var (
+ hourStatises map[string]*LogicStatis
+ muniteStatises map[string]*LogicStatis
+ //api调用参数
+ apiCallChan chan *StatisParam
+ //退出
+)
diff --git a/app/middleware/monitor/monitor.go b/app/middleware/monitor/monitor.go
new file mode 100644
index 0000000..353f75c
--- /dev/null
+++ b/app/middleware/monitor/monitor.go
@@ -0,0 +1,262 @@
+package monitor
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "net/http"
+ "runtime"
+ "sort"
+ "time"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/tgbot"
+ "videoweb/pub/redis"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Check 运行监控检查
+func Check(ctx *gin.Context) {
+ //查看是否有限制登陆
+ ip := ctx.ClientIP()
+ ifKey := redisconst.IpForbitKey(ip)
+ retValue, _ := redis.Get(ifKey)
+ if retValue != nil {
+ //log.Info("Test Check", log.Any("ip", ip), log.Any("retValue", *retValue))
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
+ "code": srvdef.ErrIpForbid,
+ "msg": srvdef.ErrIpForbid.Msg(),
+ })
+ return
+ }
+
+ // Start timer
+ start := time.Now()
+ path := ctx.Request.URL.Path
+ raw := ctx.Request.URL.RawQuery
+ rawData, err := ctx.GetRawData()
+ if err != nil {
+ log.Info("Test get raw data error", log.E(err))
+ }
+ // 将取出来的执行条件放回ctx中
+ ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData))
+ //recover
+ func() {
+ defer func() {
+ if r := recover(); r != nil {
+ buf := make([]byte, 2048)
+ length := runtime.Stack(buf, false)
+ info := string(buf[:length])
+ log.Error("catch a unkown excetion", log.Any("excetion :", info))
+ //发送tg消息
+ msg := "monitor check crash,the stack:\n" + info
+ tgbot.SendToProgram("API接口处理宕了", msg)
+ }
+ }()
+ // Process request
+ ctx.Next()
+ }()
+ // Stop timer
+ stopTime := time.Now()
+ latency := stopTime.Sub(start).Milliseconds()
+ if raw != "" {
+ path = path + "?" + raw
+ }
+ //添加记录
+ //log.Info("Test", log.Any("rawData", rawData))
+ rawString := string(rawData)
+ addApiCall(path, int64(latency), rawString)
+ //如果时间大于1秒发送tg消息
+ if latency > 4000 {
+ userId := ctx.GetInt64(consts.CtxUserID)
+ msg := ""
+ msg = msg + fmt.Sprintf("path:%s", path) + "\n"
+ msg = msg + fmt.Sprintf("userId:%d", userId) + "\n"
+ msg = msg + fmt.Sprintf("userIp:%s", ip) + "\n"
+ msg = msg + fmt.Sprintf("rawData:%s", string(rawData)) + "\n"
+ msg = msg + fmt.Sprintf("time:%d", latency) + "\n"
+ tgbot.SendToProgram("API接口调用超时", msg)
+ }
+ //log.Info("monitor:Check", log.Any("latency", latency), log.Any("path", path))
+}
+
+func init() {
+ hourStatises = make(map[string]*LogicStatis)
+ muniteStatises = make(map[string]*LogicStatis)
+ apiCallChan = make(chan *StatisParam, 1024)
+ godef.Go(loop)
+ log.Info("(monitor:init) init success.")
+}
+
+func loop() {
+ log.Info("(monitor:loop) start...")
+ defer log.Info("monitor:loop) end")
+ var param *StatisParam
+ for {
+ select {
+ case param = <-apiCallChan:
+ onStatis(param)
+ default:
+ time.Sleep(time.Millisecond * 10)
+ }
+ }
+}
+
+func onStatis(param *StatisParam) {
+ if param == nil {
+ return
+ }
+ now := time.Now()
+ hourStr := now.Format("2006-01-02 15")
+ hourStatis, hourOk := hourStatises[hourStr]
+ if !hourOk {
+ hourStatis = &LogicStatis{
+ ApisStatis: make(map[string]*APIStatis),
+ }
+ hourStatises[hourStr] = hourStatis
+ }
+ muniteStr := now.Format("2006-01-02 15:04")
+ minuteStatis, minuteOk := muniteStatises[muniteStr]
+ if !minuteOk {
+ minuteStatis = &LogicStatis{
+ ApisStatis: make(map[string]*APIStatis),
+ }
+ muniteStatises[muniteStr] = minuteStatis
+ }
+ onStatisApiCall(hourStr, param, hourStatis, 10000000)
+ onStatisApiCall(muniteStr, param, minuteStatis, 20000)
+ //如果新的一小时,发送上一小时数据
+ if !hourOk {
+ //清空分钟数据
+ muniteStatises = make(map[string]*LogicStatis)
+ lastHour := now.Add(-time.Hour).Format("2006-01-02 15")
+ hourStatis, hourOk = hourStatises[lastHour]
+ if hourOk {
+ msg := fmt.Sprintf("统计时间:%s\n", lastHour)
+ msg += formatStatis(hourStatis)
+ tgbot.SendToProgram("API接口调用统计", msg)
+ }
+ }
+}
+
+func formatStatis(hourStatis *LogicStatis) string {
+ apiStatises := make([]*APIStatis, 0)
+ for _, static := range hourStatis.ApisStatis {
+ apiStatises = append(apiStatises, static)
+ }
+ sort.Slice(apiStatises, func(i, j int) bool {
+ return apiStatises[i].CallNum > apiStatises[j].CallNum
+ })
+ //发送数据
+ msg := ""
+ maxlen := len(apiStatises)
+ if maxlen > 10 {
+ maxlen = 10
+ }
+ for i := 0; i < maxlen; i++ {
+ apiStatis := apiStatises[i]
+ msg += formatApiStatis(apiStatis)
+ }
+ timePerCall := int64(0)
+ callPerSec := int64(0)
+ if hourStatis.ApiCallNum > 0 {
+ timePerCall = hourStatis.ApiTotalTime / hourStatis.ApiCallNum
+ diffSecdonds := int64(time.Now().Sub(hourStatis.Start).Seconds())
+ if diffSecdonds > 0 {
+ callPerSec = hourStatis.ApiCallNum / diffSecdonds
+ }
+ }
+ msg += fmt.Sprintf("ApiCallNum:%d \n", hourStatis.ApiCallNum)
+ msg += fmt.Sprintf("ApiTotalTime:%d \n", hourStatis.ApiTotalTime)
+ msg += fmt.Sprintf("ApiTimePerCall:%d \n", timePerCall)
+ msg += fmt.Sprintf("ApiCallPerSec:%d \n", callPerSec)
+ return msg
+}
+
+func formatApiStatis(apiStatis *APIStatis) string {
+ msg := ""
+ if apiStatis.CallNum > 0 {
+ apiStatis.TimePerCall = apiStatis.TotalTime / apiStatis.CallNum
+ diffSeconds := int64(time.Now().Sub(apiStatis.Start).Seconds())
+ if diffSeconds > 0 {
+ apiStatis.CallPerSec = apiStatis.CallNum / diffSeconds
+ }
+ }
+
+ statisStr := fmt.Sprintf(`path %s:
+MaxRawData:%s
+CallNum:%d;MinTime:%d;MaxTime:%d;TotalTime:%d;TimePerCall:%d;CallPerSec:%d`,
+ apiStatis.Path,
+ apiStatis.MaxTimeRawData,
+ apiStatis.CallNum,
+ apiStatis.MinTime,
+ apiStatis.MaxTime,
+ apiStatis.TotalTime,
+ apiStatis.TimePerCall,
+ apiStatis.CallPerSec)
+ msg += statisStr
+ msg += "\n---------------------------------------------------\n"
+ return msg
+}
+
+// api统计
+func onStatisApiCall(dateStr string, param *StatisParam,
+ logicStatis *LogicStatis, maxCallNum int64) {
+ logicStatis.ApiCallNum++
+ logicStatis.ApiTotalTime += param.Param2
+ apiStatis, ok := logicStatis.ApisStatis[param.Param1]
+ if !ok {
+ apiStatis = &APIStatis{
+ Path: param.Param1,
+ MaxTimeRawData: param.Param3,
+ CallNum: 0,
+ MinTime: math.MaxInt64,
+ MaxTime: 0,
+ TotalTime: 0,
+ Start: time.Now(),
+ Send: 0,
+ }
+ logicStatis.ApisStatis[param.Param1] = apiStatis
+ }
+ apiStatis.CallNum++
+ apiStatis.TotalTime += param.Param2
+ if param.Param2 > apiStatis.MaxTime {
+ apiStatis.MaxTime = param.Param2
+ apiStatis.MaxTimeRawData = param.Param3
+ }
+ if param.Param2 < apiStatis.MinTime {
+ apiStatis.MinTime = param.Param2
+ }
+ if apiStatis.CallNum > maxCallNum && apiStatis.Send < 4 {
+ apiStatis.Send++
+ msg := fmt.Sprintf("接口%s每分钟调用超过最大次数%d\n", apiStatis.Path, maxCallNum)
+ msg += formatApiStatis(apiStatis)
+ tgbot.SendToProgram("API调用警告", msg)
+ }
+}
+
+func addApiCall(path string, latency int64, rawData string) {
+ param := &StatisParam{
+ Type: SP_ApiCall,
+ Param1: path,
+ Param2: latency,
+ Param3: rawData,
+ }
+ pushParam(param)
+}
+
+func pushParam(param *StatisParam) {
+ //no block
+ select {
+ case apiCallChan <- param:
+ default:
+ info := "(monitor:pushParam) failed,chan is full."
+ log.Info(info)
+ tgbot.SendToProgram("monitor pushParam failed", info)
+ }
+}
diff --git a/app/middleware/reptilerecord/reptilerecord.go b/app/middleware/reptilerecord/reptilerecord.go
new file mode 100644
index 0000000..4585c7e
--- /dev/null
+++ b/app/middleware/reptilerecord/reptilerecord.go
@@ -0,0 +1,48 @@
+package reptilerecord
+
+import (
+ "github.com/gin-gonic/gin"
+ "strings"
+ "videoweb/pub/common/log"
+)
+
+const ctxKeyIsBot = "isBot"
+
+var (
+ botPatterns = map[string]string{
+ "Googlebot": "Googlebot",
+ "Bingbot": "Bingbot",
+ "Slurp": "Slurp",
+ "DuckDuckBot": "DuckDuckBot",
+ "Baiduspider": "Baiduspider",
+ "YandexBot": "YandexBot",
+ "Sogou": "Sogou",
+ "Exabot": "Exabot",
+ "facebot": "facebot",
+ "facebookexternalhit": "facebookexternalhit",
+ }
+)
+
+func ReptileRecord(c *gin.Context) {
+ ua := c.Request.UserAgent()
+ botName := identifyBot(ua)
+ if botName != "" {
+ c.Set(ctxKeyIsBot, true)
+ log.Info("爬虫访问:", log.Any("botName", botName), log.Any("ua", ua), log.Any("path", c.Request.URL.Path), log.Any("ip", c.ClientIP()))
+ }
+}
+
+// IsBot 返回当前请求是否来自已知搜索引擎爬虫
+func IsBot(c *gin.Context) bool {
+ v, exists := c.Get(ctxKeyIsBot)
+ return exists && v.(bool)
+}
+
+func identifyBot(userAgent string) string {
+ for name, pattern := range botPatterns {
+ if strings.Contains(userAgent, pattern) {
+ return name
+ }
+ }
+ return ""
+}
diff --git a/app/middleware/templatecache/cache.go b/app/middleware/templatecache/cache.go
new file mode 100644
index 0000000..e21a0a2
--- /dev/null
+++ b/app/middleware/templatecache/cache.go
@@ -0,0 +1,116 @@
+package templatecache
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/patrickmn/go-cache"
+)
+
+const (
+ maxCacheItems = 2000
+ maxCacheBytes = 64 << 20
+)
+
+var (
+ Cache *cache.Cache
+ cacheBytes int64
+)
+
+// 章节详情、资讯详情等 ID 空间过大,禁止进进程缓存。
+var highCardinalityContains = []string{
+ "/chapter/info",
+ "/consultinginfo/",
+}
+
+func init() {
+ Cache = cache.New(5*time.Minute, 5*time.Minute)
+ Cache.OnEvicted(func(_ string, v interface{}) {
+ if b, ok := v.([]byte); ok {
+ atomic.AddInt64(&cacheBytes, -int64(len(b)))
+ }
+ })
+}
+
+func shouldBypassCache(path string) bool {
+ if path == "" {
+ return false
+ }
+ if i := strings.IndexByte(path, '?'); i >= 0 {
+ path = path[:i]
+ }
+ for _, part := range highCardinalityContains {
+ if strings.Contains(path, part) {
+ return true
+ }
+ }
+ return false
+}
+
+func cacheBudgetAllows(n int) bool {
+ if n <= 0 {
+ return false
+ }
+ if Cache.ItemCount() >= maxCacheItems {
+ return false
+ }
+ return atomic.LoadInt64(&cacheBytes)+int64(n) <= maxCacheBytes
+}
+
+type templateResponseWriter struct {
+ gin.ResponseWriter
+ body *bytes.Buffer
+}
+
+func (w *templateResponseWriter) Write(b []byte) (int, error) {
+ w.body.Write(b)
+ return w.ResponseWriter.Write(b)
+}
+
+func WithCache(duration time.Duration) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ if ctx.Request.Method != "GET" {
+ ctx.Next()
+ return
+ }
+ if shouldBypassCache(ctx.Request.URL.Path) {
+ ctx.Next()
+ return
+ }
+
+ cacheKey := ctx.Request.URL.String()
+ if cacheData, exists := Cache.Get(cacheKey); exists {
+ if item, ok := cacheData.([]byte); ok && len(item) > 0 {
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", item)
+ ctx.Abort()
+ return
+ }
+ }
+
+ writer := &templateResponseWriter{
+ body: bytes.NewBuffer(nil),
+ ResponseWriter: ctx.Writer,
+ }
+ ctx.Writer = writer
+ ctx.Next()
+
+ if ctx.Writer.Status() != 200 {
+ return
+ }
+ raw := writer.body.Bytes()
+ if !cacheBudgetAllows(len(raw)) {
+ return
+ }
+ copied := append([]byte(nil), raw...)
+ atomic.AddInt64(&cacheBytes, int64(len(copied)))
+ cacheDuration := duration
+ if cacheDuration == 0 {
+ cacheDuration = time.Minute
+ }
+ Cache.Set(cacheKey, copied, cacheDuration)
+ }
+}
diff --git a/app/middleware/validator/default-validator.go b/app/middleware/validator/default-validator.go
new file mode 100644
index 0000000..daf78ee
--- /dev/null
+++ b/app/middleware/validator/default-validator.go
@@ -0,0 +1,81 @@
+package validator
+
+import (
+ "reflect"
+ "strings"
+ "sync"
+
+ "github.com/gin-gonic/gin/binding"
+ "github.com/go-playground/locales/en"
+ ut "github.com/go-playground/universal-translator"
+ "gopkg.in/go-playground/validator.v9"
+ en_translations "gopkg.in/go-playground/validator.v9/translations/en"
+)
+
+var (
+ ValidatorUni *ut.UniversalTranslator
+)
+
+type defaultValidator struct {
+ once sync.Once
+ validate *validator.Validate
+}
+
+var v binding.StructValidator = &defaultValidator{}
+
+func init() {
+ v.Engine()
+}
+
+func (v *defaultValidator) ValidateStruct(obj interface{}) error {
+
+ if kindOfData(obj) == reflect.Struct {
+
+ v.lazyinit()
+
+ if err := v.validate.Struct(obj); err != nil {
+ return error(err)
+ }
+ }
+
+ return nil
+}
+
+func (v *defaultValidator) Engine() interface{} {
+ v.lazyinit()
+ return v.validate
+}
+
+func (v *defaultValidator) lazyinit() {
+ v.once.Do(func() {
+ v.validate = validator.New()
+
+ // 依旧使用bingding tag兼容v8
+ v.validate.SetTagName("binding")
+
+ // 错误信息转换
+ en := en.New()
+ ValidatorUni = ut.New(en, en)
+ trans, _ := ValidatorUni.GetTranslator("en")
+ en_translations.RegisterDefaultTranslations(v.validate, trans)
+
+ v.validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
+ name := strings.SplitN(fld.Tag.Get("comment"), ",", 2)[0]
+ if name == "-" {
+ return ""
+ }
+ return name
+ })
+ })
+}
+
+func kindOfData(data interface{}) reflect.Kind {
+
+ value := reflect.ValueOf(data)
+ valueType := value.Kind()
+
+ if valueType == reflect.Ptr {
+ valueType = value.Elem().Kind()
+ }
+ return valueType
+}
diff --git a/app/middleware/validator/v9.go b/app/middleware/validator/v9.go
new file mode 100644
index 0000000..4e2e38e
--- /dev/null
+++ b/app/middleware/validator/v9.go
@@ -0,0 +1,11 @@
+package validator
+
+import (
+ "github.com/gin-gonic/gin/binding"
+)
+
+// 替换gin 默认v8验证器
+// 改为v9
+func init() {
+ binding.Validator = new(defaultValidator)
+}
diff --git a/app/middleware/verfyparam/verfyparam.go b/app/middleware/verfyparam/verfyparam.go
new file mode 100644
index 0000000..7370c8a
--- /dev/null
+++ b/app/middleware/verfyparam/verfyparam.go
@@ -0,0 +1,23 @@
+package verfyparam
+
+import (
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+var (
+ // 白名单,不需要验证token的api
+ whitelist = map[string]bool{}
+)
+
+func VerifyReplayAttackRequest() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ for url, ok := range whitelist {
+ if ok && strings.HasPrefix(ctx.Request.URL.Path, url) {
+ return
+ }
+ }
+ ctx.Next()
+ }
+}
diff --git a/app/middleware/visit/visit.go b/app/middleware/visit/visit.go
new file mode 100644
index 0000000..f1f3701
--- /dev/null
+++ b/app/middleware/visit/visit.go
@@ -0,0 +1,12 @@
+package visit
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// 用户访问中间件
+// 记录用户在线量
+// 按5分钟纬度记录到redis中
+func Visit(ctx *gin.Context) {
+
+}
diff --git a/app/router/actress.go b/app/router/actress.go
new file mode 100644
index 0000000..231e8c4
--- /dev/null
+++ b/app/router/actress.go
@@ -0,0 +1,12 @@
+package router
+
+import (
+ "videoweb/app/api/actressapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func actressRouter(r *gin.Engine) {
+ r.GET("/api/app/actress/list", actressapi.ActressList)
+ r.GET("/api/app/actress/work/list", actressapi.QueryActressWorkList)
+}
diff --git a/app/router/appinfo.go b/app/router/appinfo.go
new file mode 100644
index 0000000..28f2d49
--- /dev/null
+++ b/app/router/appinfo.go
@@ -0,0 +1,11 @@
+package router
+
+import (
+ "videoweb/app/api/appinfoapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func appInfoRouter(r *gin.Engine) {
+ r.GET("/api/app/applist/list", appinfoapi.QueryAppList)
+}
diff --git a/app/router/auth.go b/app/router/auth.go
new file mode 100644
index 0000000..672e93a
--- /dev/null
+++ b/app/router/auth.go
@@ -0,0 +1,24 @@
+package router
+
+import (
+ "videoweb/app/api/authapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func authRouter(r *gin.Engine) {
+ // 游客登录(无需 token)
+ login := r.Group("/api/app/login")
+ {
+ login.POST("/guest", authapi.GuestLogin)
+ }
+
+ // 账号注册 / 登录 / 改资料
+ auth := r.Group("/api/app/auth")
+ {
+ auth.POST("/register", authapi.Register)
+ auth.POST("/login", authapi.Login)
+ auth.GET("/getInfo", authapi.GetInfo)
+ auth.POST("/profile", authapi.UpdateProfile)
+ }
+}
diff --git a/app/router/comment.go b/app/router/comment.go
new file mode 100644
index 0000000..b567eae
--- /dev/null
+++ b/app/router/comment.go
@@ -0,0 +1,13 @@
+package router
+
+import (
+ "videoweb/app/api/commentapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func commentRouter(r *gin.Engine) {
+ r.GET("/api/app/comment/list", commentapi.List)
+ r.POST("/api/app/comment/send", commentapi.SendComment)
+ r.POST("/api/app/comment/like/set", commentapi.SetLike)
+}
diff --git a/app/router/config.go b/app/router/config.go
new file mode 100644
index 0000000..610efd8
--- /dev/null
+++ b/app/router/config.go
@@ -0,0 +1,13 @@
+package router
+
+import (
+ configapi "videoweb/app/api/config"
+
+ "github.com/gin-gonic/gin"
+)
+
+func configRouter(r *gin.Engine) {
+ r.GET("/api/app/config/footer/info", configapi.FooterInfo)
+ r.GET("/api/app/config/richtext/list", configapi.RichTextList)
+ r.GET("/api/app/content/tag/list", configapi.ContentTagList)
+}
diff --git a/app/router/dh.go b/app/router/dh.go
new file mode 100644
index 0000000..b9dad81
--- /dev/null
+++ b/app/router/dh.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "time"
+ "videoweb/app/api/dhapi"
+ "videoweb/app/middleware/templatecache"
+
+ "github.com/gin-gonic/gin"
+)
+
+func dhRouter(r *gin.Engine) {
+ r.GET("/dh/", templatecache.WithCache(time.Minute*1), dhapi.DHApi)
+ r.GET("/businessCooperation/", templatecache.WithCache(time.Minute*1), dhapi.BusinessCooperationApi)
+ r.GET("/servicesProject/", templatecache.WithCache(time.Minute*1), dhapi.ServicesProjectApi)
+ r.GET("/consulting/", templatecache.WithCache(time.Minute*1), dhapi.ConsultingApi)
+ r.GET("/consultinginfo/:id/", templatecache.WithCache(time.Minute*1), dhapi.ConsultingInfoApi)
+}
diff --git a/app/router/gin.go b/app/router/gin.go
new file mode 100644
index 0000000..78a29b9
--- /dev/null
+++ b/app/router/gin.go
@@ -0,0 +1,124 @@
+package router
+
+import (
+ "log"
+ "net/http"
+ "strings"
+ "videoweb/app/appg"
+ "videoweb/app/middleware/auth"
+ "videoweb/app/middleware/cors"
+ "videoweb/app/middleware/ipblacklist"
+ "videoweb/app/middleware/ipstat"
+ "videoweb/app/middleware/reptilerecord"
+ "videoweb/app/service/displaycount"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/useragent"
+
+ "github.com/DeanThompson/ginpprof"
+ "github.com/gin-gonic/gin"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+
+ _ "videoweb/app/docs"
+)
+
+func Init() (engine *gin.Engine) {
+ // 设置启动模式
+ switch appg.Conf.RunMod {
+ case "dev":
+ gin.SetMode(gin.DebugMode)
+ case "test":
+ gin.SetMode(gin.TestMode)
+ case "prod":
+ gin.SetMode(gin.ReleaseMode)
+ default:
+ gin.SetMode(gin.DebugMode)
+ }
+ // 初始化gin引擎
+ engine = gin.Default()
+ engine.HandleMethodNotAllowed = true
+ engine.MaxMultipartMemory = 60 << 20
+
+ // 从配置文件读取可信代理列表(字符串,多个 IP 用逗号分隔)
+ // 配置了则只信任这些 IP 的 X-Forwarded-For,未配置则不信任任何代理(直接用 TCP 连接 IP)
+ // 配置示例(app.yaml)
+ // trustedProxies: "10.0.0.1"
+ // trustedProxies: "10.0.0.1,10.0.0.2"
+ if appg.Conf.TrustedProxies != "" {
+ proxies := strings.Split(appg.Conf.TrustedProxies, ",")
+ for i, p := range proxies {
+ proxies[i] = strings.TrimSpace(p)
+ }
+ _ = engine.SetTrustedProxies(proxies)
+ log.Printf("SetTrustedProxies: %v", proxies)
+ } else {
+ _ = engine.SetTrustedProxies([]string{})
+ log.Printf("SetTrustedProxies: none (direct TCP IP mode)")
+ }
+
+ common.InitGinSecret(appg.Conf.ResKey, consts.RunMod(appg.Conf.RunMod))
+ common.SetResponseDataTransform(displaycount.Transform)
+ // 注册中间件
+ registeMiddleware(engine)
+ // 注册路由
+ registeRouter(engine)
+
+ if appg.Conf.RunMod != "prod" {
+ setGinSwagger(engine)
+ }
+ // 是否开启监控服务
+ if appg.Conf.Pprof {
+ ginpprof.Wrap(engine)
+ }
+ return
+}
+
+func registeMiddleware(engine *gin.Engine) {
+ engine.Use(
+ // gin.Recovery(),
+ cors.Cors(&cors.Options{
+ Origin: nil, // "*"
+ Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete},
+ AllowedHeaders: []string{"Content-Length", "Content-Type", "Authorization", "X-User-Agent", "device", "api_version", "x-api-key", useragent.SDKCommonHeader},
+ ExposedHeaders: []string{"Content-Disposition", "Refresh-Authorization"},
+ OptionsSuccessStatus: http.StatusOK,
+ }),
+ // 必须是此顺序, happens before原则
+ //monitor.Check,
+ ipblacklist.IpBlackListMiddleware,
+ reptilerecord.ReptileRecord,
+ ipstat.IpStatMiddleware,
+ useragent.UserAgent,
+ auth.Auth,
+ // calllimit.Check,
+ // visit.Visit,
+ )
+}
+
+// 注册路由
+func registeRouter(r *gin.Engine) {
+ homeRouter(r)
+ videoRouter(r)
+ commentRouter(r)
+ searchRouter(r)
+ dhRouter(r)
+ lawAndTrustRouter(r)
+ actressRouter(r)
+ configRouter(r)
+ navigationRouter(r)
+ playletRouter(r)
+ authRouter(r)
+ uploadRouter(r)
+ questionnaireRouter(r)
+ appInfoRouter(r)
+ healthRouter(r)
+ richTextRouter(r)
+}
+
+// 设置swag 接口信息
+func setGinSwagger(router *gin.Engine) {
+ // 使用相对路径,无需依赖 Host 配置,本地/测试环境均可直接访问
+ url := ginSwagger.URL("/swagger/doc.json")
+ router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
+}
diff --git a/app/router/health.go b/app/router/health.go
new file mode 100644
index 0000000..63140df
--- /dev/null
+++ b/app/router/health.go
@@ -0,0 +1,11 @@
+package router
+
+import (
+ "videoweb/app/api/healthapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func healthRouter(r *gin.Engine) {
+ r.POST("/api/app/health/ping", healthapi.HealthCheck)
+}
diff --git a/app/router/home.go b/app/router/home.go
new file mode 100644
index 0000000..99a0594
--- /dev/null
+++ b/app/router/home.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "time"
+ "videoweb/app/api/home"
+ "videoweb/app/middleware/templatecache"
+
+ "github.com/gin-gonic/gin"
+)
+
+func homeRouter(r *gin.Engine) {
+ r.GET("/api/app/home", home.Home)
+ r.GET("/api/app/home/base", templatecache.WithCache(time.Minute*1), home.HomeBase)
+ r.GET("/api/app/home/tags", templatecache.WithCache(time.Minute*1), home.HomeTags)
+}
diff --git a/app/router/lawandtrust.go b/app/router/lawandtrust.go
new file mode 100644
index 0000000..0fc6282
--- /dev/null
+++ b/app/router/lawandtrust.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "time"
+ "videoweb/app/api/lawandtrust"
+ "videoweb/app/middleware/templatecache"
+
+ "github.com/gin-gonic/gin"
+)
+
+func lawAndTrustRouter(r *gin.Engine) {
+ r.GET("/useragreement/", templatecache.WithCache(time.Minute*1), lawandtrust.UserAgreementApi)
+ r.GET("/privacyPolicy/", templatecache.WithCache(time.Minute*1), lawandtrust.PrivacyPolicyApi)
+ r.GET("/dmca/", templatecache.WithCache(time.Minute*1), lawandtrust.DmcaApi)
+ r.GET("/statement/", templatecache.WithCache(time.Minute*1), lawandtrust.StatementApi)
+}
diff --git a/app/router/navigation.go b/app/router/navigation.go
new file mode 100644
index 0000000..15cdb75
--- /dev/null
+++ b/app/router/navigation.go
@@ -0,0 +1,11 @@
+package router
+
+import (
+ "videoweb/app/api/navigationapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func navigationRouter(r *gin.Engine) {
+ r.GET("/api/app/navigation/header", navigationapi.HeaderNavigation)
+}
diff --git a/app/router/playlet.go b/app/router/playlet.go
new file mode 100644
index 0000000..313010d
--- /dev/null
+++ b/app/router/playlet.go
@@ -0,0 +1,29 @@
+package router
+
+import (
+ "videoweb/app/api/playletapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func playletRouter(r *gin.Engine) {
+ r.GET("/api/app/playlet/home", playletapi.HomePlaylet)
+ r.GET("/api/app/playlet/home-config/list", playletapi.HomeConfigList)
+ r.GET("/api/app/playlet/topic/detail", playletapi.TopicDetail)
+ r.GET("/api/app/playlet/theme/list", playletapi.ThemeList)
+ r.GET("/api/app/playlet/list", playletapi.List)
+ r.GET("/api/app/playlet/content/list", playletapi.ContentList)
+ r.GET("/api/app/playlet/recommend/list", playletapi.RecommendList)
+ r.GET("/api/app/playlet/guess/list", playletapi.GuessList)
+ r.GET("/api/app/playlet/tag/:id", playletapi.TagList)
+ r.GET("/api/app/playlet/info", playletapi.QueryPlayletInfo)
+ r.GET("/api/app/playlet/chapter/info", playletapi.QueryPlayletChapterInfo)
+ r.POST("/api/app/playlet/like", playletapi.LikePlaylet)
+ r.GET("/api/app/playlet/like/list", playletapi.QueryLikePlayletList)
+ r.POST("/api/app/playlet/favorite", playletapi.FavoritePlaylet)
+ r.GET("/api/app/playlet/favorite/list", playletapi.QueryFavoritePlayletList)
+ r.GET("/api/app/playlet/history/list", playletapi.QueryPlayletHistoryList)
+ r.POST("/api/app/playlet/history", playletapi.ReportPlayletHistory)
+ r.DELETE("/api/app/playlet/history", playletapi.ClearPlayletHistory)
+ r.GET("/api/app/playlet/rank/list", playletapi.QueryPlayletRankList)
+}
diff --git a/app/router/questionnaire.go b/app/router/questionnaire.go
new file mode 100644
index 0000000..f248913
--- /dev/null
+++ b/app/router/questionnaire.go
@@ -0,0 +1,12 @@
+package router
+
+import (
+ "videoweb/app/api/questionnaireapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func questionnaireRouter(r *gin.Engine) {
+ r.GET("/questionnaire", questionnaireapi.Page)
+ r.POST("/api/app/questionnaire/submit", questionnaireapi.Submit)
+}
diff --git a/app/router/richtext.go b/app/router/richtext.go
new file mode 100644
index 0000000..ba46cd1
--- /dev/null
+++ b/app/router/richtext.go
@@ -0,0 +1,11 @@
+package router
+
+import (
+ "videoweb/app/api/richtextapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func richTextRouter(router *gin.Engine) {
+ router.GET("/api/app/richtext", richtextapi.GetByTitle)
+}
diff --git a/app/router/search.go b/app/router/search.go
new file mode 100644
index 0000000..959fa1f
--- /dev/null
+++ b/app/router/search.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "time"
+ "videoweb/app/api/searchapi"
+ "videoweb/app/middleware/templatecache"
+
+ "github.com/gin-gonic/gin"
+)
+
+func searchRouter(r *gin.Engine) {
+
+ r.GET("/search/", templatecache.WithCache(time.Minute*1), searchapi.SearchApi)
+
+}
diff --git a/app/router/upload.go b/app/router/upload.go
new file mode 100644
index 0000000..2107c89
--- /dev/null
+++ b/app/router/upload.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "videoweb/app/api/uploadapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func uploadRouter(r *gin.Engine) {
+ group := r.Group("/api/app/upload")
+ {
+ group.POST("/videoBody", uploadapi.VideoBody)
+ group.POST("/videoOk", uploadapi.VideoOk)
+ group.POST("/image", uploadapi.UploadImage)
+ }
+}
diff --git a/app/router/video.go b/app/router/video.go
new file mode 100644
index 0000000..5aa8e4c
--- /dev/null
+++ b/app/router/video.go
@@ -0,0 +1,36 @@
+package router
+
+import (
+ "videoweb/app/api/videoapi"
+ "videoweb/app/service/videoser"
+
+ "github.com/gin-gonic/gin"
+
+ _ "videoweb/app/docs"
+)
+
+func videoRouter(r *gin.Engine) {
+
+ r.GET("/api/app/video/list", videoapi.List)
+ r.GET("/api/app/video/category/list", videoapi.CategoryList)
+ r.GET("/api/app/video/recommend/list", videoapi.RecommendList)
+ r.GET("/api/app/video/guess/list", videoapi.GuessList)
+ r.GET("/api/app/video/tag/:id", videoapi.TagList)
+ r.GET("/api/app/video/more", videoapi.QueryVideoMore)
+ r.GET("/api/app/video/info", videoapi.QueryVideoInfo)
+ r.POST("/api/app/video/favorite", videoapi.FavoriteVideo)
+ r.POST("/api/app/video/like", videoapi.LikeVideo)
+ r.GET("/api/app/video/like/list", videoapi.QueryLikeVideoList)
+ r.GET("/api/app/video/favorite/list", videoapi.QueryFavoriteVideoList)
+ r.GET("/api/app/video/history/list", videoapi.QueryVideoHistoryList)
+ r.POST("/api/app/video/history", videoapi.ReportVideoHistory)
+ r.DELETE("/api/app/video/history", videoapi.ClearVideoHistory)
+ r.GET("/api/app/video/rank", videoapi.QueryVideoRank)
+ r.GET("/api/app/video/rank/list", videoapi.QueryVideoRankList)
+ r.GET("/h5/m3u8/*key", videoser.GetM3U8Ex)
+ r.GET("/enkey", videoser.Enkey)
+ r.GET("/newenkey", videoser.NewEnkey)
+ r.GET("/lsjenkey", videoser.LsjEnkey)
+ r.GET("/vtt/*key", videoser.GetVTT)
+
+}
diff --git a/app/server/server.go b/app/server/server.go
new file mode 100644
index 0000000..9e6cca0
--- /dev/null
+++ b/app/server/server.go
@@ -0,0 +1,214 @@
+package server
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/app/router"
+ "videoweb/app/service/appser"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/richtextser"
+ "videoweb/app/service/translationser"
+ ipstatsync "videoweb/app/skd/ipstat"
+ seoskd "videoweb/app/skd/seo"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvversion"
+ "videoweb/pub/common/tgbot"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models"
+ "videoweb/pub/mongos"
+ "videoweb/pub/redis"
+ "videoweb/pub/storage/proxy"
+
+ "gopkg.in/yaml.v2"
+)
+
+func init() {
+ time.Local, _ = time.LoadLocation("Asia/Shanghai")
+}
+func Run(args []string) {
+ showVersion()
+ if err := initConfig(); err != nil {
+ log.Error("init config failed", log.E(err))
+ os.Exit(1)
+ }
+
+ if err := initMongoDB(); err != nil {
+ log.Error("init mongodb failed", log.E(err))
+ os.Exit(1)
+ }
+
+ //初始化tg机器人
+ if appg.Conf.TgToken != "" {
+ // tgbot.InitTgBot(appg.Conf.TgToken, "[汁乎App]")
+ //tgbot.SendToProgram("Test", "Test")
+ }
+ detectorV6, err := ipaddr.NewXDBDetectorV6("conf/ip2region_v6.xdb")
+ if err != nil {
+ fmt.Printf("创建检测器失败: %v\n", err)
+ fmt.Println("正在尝试下载数据库文件...")
+ }
+ appg.DetectorV6 = detectorV6
+
+ detectorV4, err := ipaddr.NewXDBDetectorV4("conf/ip2region_v4.xdb")
+ if err != nil {
+ fmt.Printf("创建检测器失败: %v\n", err)
+ fmt.Println("正在尝试下载数据库文件...")
+ }
+ appg.DetectorV4 = detectorV4
+ proxy.InitBucketVideoAddress(appg.Conf.BucketVideoAddress, appg.Conf.BucketVideoAddressV2)
+ mongos.StartLid()
+ initRedis()
+ initData()
+ ipstatsync.StartSync()
+ dataReport.Init(dataReport.Config{
+ ApiUrl: appg.Conf.DataReport.Url,
+ RunMod: appg.Conf.RunMod,
+ })
+ if err := initElasticSearch(); err != nil {
+ log.Error("init ES failed", log.E(err))
+ os.Exit(1)
+ }
+
+ initHttpServer()
+ seoskd.Start()
+ //tgbot.SendToProgram("启动", "app启动完毕")
+ shutdownHandler()
+}
+
+func initConfig() error {
+
+ buf, err := ioutil.ReadFile("conf/app.yaml")
+ if err != nil {
+ return err
+ }
+ if err = yaml.Unmarshal(buf, &appg.Conf); err != nil {
+ return err
+ }
+ nameBuf, err := ioutil.ReadFile("conf/names.yaml")
+ if err != nil {
+ return err
+ }
+ if err = yaml.Unmarshal(nameBuf, &appg.Name); err != nil {
+ return err
+ }
+
+ // pictureBuf, err := ioutil.ReadFile("conf/picture.yaml")
+ // if err != nil {
+ // return err
+ // }
+
+ // if err = yaml.Unmarshal(pictureBuf, &appg.Picture); err != nil {
+ // return err
+ // }
+
+ return nil
+}
+
+func initMongoDB() error {
+ shopDb := mongos.NewManager(appg.Conf.Mongodb.Addr)
+ if err := shopDb.Init(); err != nil {
+ return err
+ }
+ mongos.Shop = shopDb
+
+ ycShopDb := mongos.NewManager(appg.Conf.Mongodb.LogAddr)
+ if err := ycShopDb.Init(); err != nil {
+ return err
+ }
+ mongos.ShopLog = ycShopDb
+ return nil
+}
+
+func initElasticSearch() error {
+ es, err := esmanager.NewManager(appg.Conf.Es.Addr,
+ appg.Conf.Es.User, appg.Conf.Es.Pass)
+ if err != nil {
+ log.Info("new es failed", log.Any("err", err))
+ return err
+ }
+ if err = es.ClientConnect(); err != nil {
+ log.Info("connect es failed", log.Any("err", err))
+ return err
+ }
+ esmanager.Instance = es
+ log.Info("es:Init Ok!")
+ return nil
+}
+
+// 数据
+func initData() {
+ models.Init()
+ // service.Start()
+ appg.InitCache()
+ translationser.Start()
+ configser.Start()
+ richtextser.Start()
+ appser.Start()
+}
+
+func initRedis() error {
+ if err := redis.Init(redis.Options{
+ Addr: appg.Conf.Redis.Addr,
+ Psw: appg.Conf.Redis.Passwd,
+ Db: appg.Conf.Redis.Db,
+ }); err != nil {
+ log.Error("redis connect err", log.E(err))
+ return err
+ }
+ return nil
+}
+
+func showVersion() {
+ log.Info(fmt.Sprintf("sevice version:%s", srvversion.SERVICE_VERSION))
+ log.Info(fmt.Sprintf("git version:%s", srvversion.GIT_VERSION))
+ log.Info(fmt.Sprintf("build date:%s", srvversion.BUILD_DATE))
+ log.Info(fmt.Sprintf("build ip:%s", srvversion.BUILD_IP))
+}
+
+func initHttpServer() {
+ addr := fmt.Sprintf(":%s", appg.Conf.ListenAddr)
+ srv := &http.Server{
+ Addr: addr,
+ Handler: router.Init(),
+ }
+ go func() {
+ // service connections
+ log.Info("服务已启动", log.Any("地址", addr))
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatal("listen:", log.E(err))
+ }
+ }()
+}
+
+func shutdownHandler() {
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Kill)
+ <-quit
+ log.Warn("Shutdown... server")
+
+ done := make(chan int)
+ go func() {
+ seoskd.Stop()
+ richtextser.Stop()
+ configser.Stop()
+ mongos.Shop.DisConnect()
+ mongos.ShopLog.DisConnect()
+ redis.Disconnect()
+ tgbot.ReleaseTgBot()
+ close(done)
+ }()
+ select {
+ case <-time.After(5 * time.Second):
+ log.Warn("shutdown timeout....")
+ case <-done:
+ log.Info("graceful shutdown")
+ }
+}
diff --git a/app/service/actressser/actress.go b/app/service/actressser/actress.go
new file mode 100644
index 0000000..27db68c
--- /dev/null
+++ b/app/service/actressser/actress.go
@@ -0,0 +1,333 @@
+package actressser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/slugser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const homeActressPageSize int64 = 9
+const videoHomeActressPageSize int64 = 16
+
+const (
+ actressWorkDefaultPageSize int64 = 10
+ actressWorkMaxPageSize int64 = 100
+ actressWorkAdInterval int64 = 8
+)
+
+// ActressListData 查询女优榜单列表,按热度值降序返回全部可用数据
+func ActressListData(ctx context.Context, path string) (*proto.QueryActressListResp, *srvdef.SrvErr) {
+ list := configser.GetActressWorks()
+ if len(list) == 0 {
+ var err *srvdef.SrvErr
+ list, err = queryActressListDataFromDB(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+ resp := &proto.QueryActressListResp{
+ Path: path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Advitise: configser.GetAdvitise(),
+ ActressInfoList: list,
+ }
+ configser.RewriteImageCdn(ctx, resp)
+ return resp, nil
+}
+
+// HomeActressData 查询首页女优数据,按热度值降序取前 9 条
+func HomeActressData(ctx context.Context) ([]proto.ActressInfo, *srvdef.SrvErr) {
+ return homeActressDataByCdn(ctx, homeActressPageSize, configser.GetConfigInfo().ImgCdn)
+}
+
+// VideoHomeActressData 查询视频首页热门女优数据,按热度值降序取前 16 条
+func VideoHomeActressData(ctx context.Context) ([]proto.ActressInfo, *srvdef.SrvErr) {
+ return homeActressDataByCdn(ctx, videoHomeActressPageSize, configser.ImageCdnFromCtx(ctx))
+}
+
+// HomeActressDataByLimit 按热度值降序查询指定数量的女优数据
+func HomeActressDataByLimit(ctx context.Context, limit int64) ([]proto.ActressInfo, *srvdef.SrvErr) {
+ return homeActressDataByCdn(ctx, limit, configser.ImageCdnFromCtx(ctx))
+}
+
+func homeActressDataByCdn(ctx context.Context, limit int64, imgCdn string) ([]proto.ActressInfo, *srvdef.SrvErr) {
+ if limit <= 0 {
+ limit = homeActressPageSize
+ }
+ list, err := actress.FindManyActresss(ctx, bson.M{
+ actress.FN.Actress.Status: true,
+ }, homeActressFindOptions(limit))
+ if err != nil {
+ log.Warn("查询首页女优数据失败", log.E(err))
+ return nil, err
+ }
+ data := make([]proto.ActressInfo, 0, len(list))
+ for _, item := range list {
+ data = append(data, homeActressInfo(item, imgCdn))
+ }
+ return data, nil
+}
+
+// queryActressListDataFromDB 回源查询女优榜单,使用字段投影减少 MongoDB 返回体积
+func queryActressListDataFromDB(ctx context.Context) ([]proto.ActressInfoData, *srvdef.SrvErr) {
+ config := configser.GetConfigInfo()
+ list, err := actress.FindManyActresss(ctx, bson.M{
+ actress.FN.Actress.Status: true,
+ }, options.Find().
+ SetSort(bson.D{{Key: actress.FN.Actress.HotValue, Value: -1}, {Key: actress.FN.Actress.Id, Value: -1}}).
+ SetProjection(bson.M{
+ actress.FN.Actress.Id: 1,
+ actress.FN.Actress.Alias: 1,
+ actress.FN.Actress.Slug: 1,
+ actress.FN.Actress.Avatar: 1,
+ actress.FN.Actress.Cover: 1,
+ actress.FN.Actress.Desc: 1,
+ actress.FN.Actress.Office: 1,
+ actress.FN.Actress.DebutDate: 1,
+ actress.FN.Actress.BornDate: 1,
+ actress.FN.Actress.Region: 1,
+ actress.FN.Actress.Score: 1,
+ actress.FN.Actress.Works: 1,
+ actress.FN.Actress.HotValue: 1,
+ actress.FN.Actress.SeoInfo: 1,
+ actress.FN.Actress.Meas.Gender: 1,
+ actress.FN.Actress.Meas.Height: 1,
+ actress.FN.Actress.Meas.Weight: 1,
+ actress.FN.Actress.Meas.Bust: 1,
+ actress.FN.Actress.Meas.Waist: 1,
+ actress.FN.Actress.Meas.Hip: 1,
+ actress.FN.Actress.Meas.Cup: 1,
+ actress.FN.Actress.Meas.Len: 1,
+ actress.FN.Actress.Meas.Dia: 1,
+ actress.FN.Actress.Meas.Blood: 1,
+ actress.FN.Actress.Real.WatchTimes: 1,
+ actress.FN.Actress.Real.ShowTimes: 1,
+ actress.FN.Actress.Real.Comments: 1,
+ }))
+ if err != nil {
+ log.Warn("查询女优榜单数据失败", log.E(err))
+ return nil, err
+ }
+ data := make([]proto.ActressInfoData, 0, len(list))
+ for _, item := range list {
+ data = append(data, proto.ActressInfoToData(*item, config.ImgCdn))
+ }
+ return data, nil
+}
+
+// homeActressFindOptions 限定首页女优查询数量、排序和字段投影,减少 MongoDB 返回数据量
+func homeActressFindOptions(limit int64) *options.FindOptions {
+ return options.Find().
+ SetLimit(limit).
+ SetSort(bson.D{{Key: actress.FN.Actress.HotValue, Value: -1}, {Key: actress.FN.Actress.Id, Value: -1}}).
+ SetProjection(bson.M{
+ actress.FN.Actress.Id: 1,
+ actress.FN.Actress.Alias: 1,
+ actress.FN.Actress.Slug: 1,
+ actress.FN.Actress.Avatar: 1,
+ actress.FN.Actress.Works: 1,
+ actress.FN.Actress.HotValue: 1,
+ })
+}
+
+// homeActressInfo 将女优模型转换为首页轻量女优结构
+func homeActressInfo(item *actress.Actress, imgCdn string) proto.ActressInfo {
+ return proto.ActressInfo{
+ Id: item.Id,
+ Alias: item.Alias,
+ Slug: item.Slug,
+ Avatar: netutil.MergeURL(imgCdn, item.Avatar),
+ Works: item.Works,
+ HotValue: item.HotValue,
+ }
+}
+
+// QueryActressWorkList 查询指定女优详情和作品混排列表
+func QueryActressWorkList(ctx context.Context, req *proto.QueryActressWorkListReq) (*proto.QueryActressWorkListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ actressId, err := slugser.ActressID(ctx, req.Id, req.Slug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.Id = actressId
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, actressWorkDefaultPageSize, actressWorkMaxPageSize)
+ config := configser.GetRequestConfig(ctx)
+ actressInfo := configser.GetActressInfo(req.Id)
+ if actressInfo == nil {
+ log.Warn("女优详情不存在", log.Any("actressId", req.Id))
+ return nil, srvdef.New(srvdef.AtressNotExist)
+ }
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.ActressIds: bson.M{"$in": []int64{req.Id}},
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ log.Warn("统计女优作品总数失败", log.Any("actressId", req.Id), log.E(err))
+ return nil, err
+ }
+ ads := configser.GetAdvitise().PostList
+ adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), actressWorkAdInterval)
+ mixedTotalCount := totalCount + adsTotalCount
+ resp := &proto.QueryActressWorkListResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: mixedTotalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.QueryVideoListItem, 0),
+ },
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ ActressInfo: proto.ActressInfoToData(*actressInfo, config.ImgCdn),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ start := (pageNum - 1) * pageSize
+ if start >= mixedTotalCount {
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedTotalCount {
+ end = mixedTotalCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, actressWorkAdInterval)
+ list := videoinformationmod.VideoInformationList{}
+ if contentLimit > 0 {
+ list, err = videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
+ SetSort(actressWorkSort(req)).
+ SetProjection(actressWorkProjection()).
+ SetSkip(contentStart).
+ SetLimit(contentLimit))
+ if err != nil {
+ log.Warn("查询女优作品列表失败", log.Any("actressId", req.Id), log.E(err))
+ return nil, err
+ }
+ }
+ mediaList := buildActressWorkMixedListWithoutTotal(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
+ resp.List = mediaList
+ return resp, nil
+}
+
+// actressWorkSort 观看量和收藏量按真实数据 real.watchTimes、real.likes 排序,不使用 fake 数据
+func actressWorkSort(req *proto.QueryActressWorkListReq) bson.D {
+ sort := make(bson.D, 0, 4)
+ if dir := actressWorkSortDir(req.AddedTimeSort); dir != 0 {
+ sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: dir})
+ }
+ if dir := actressWorkSortDir(req.WatchTimesSort); dir != 0 {
+ sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: dir})
+ }
+ if dir := actressWorkSortDir(req.LikesSort); dir != 0 {
+ sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: dir})
+ }
+ if len(sort) > 0 {
+ sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1})
+ return sort
+ }
+ return bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.VideoCount, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+}
+
+// actressWorkProjection 限定作品列表卡片字段,避免读取详情页大字段和视频节点。
+func actressWorkProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ }
+}
+
+// actressWorkSortDir 将接口排序值转换为 MongoDB 排序方向
+func actressWorkSortDir(sortType int64) int {
+ switch sortType {
+ case 1:
+ return 1
+ case 2:
+ return -1
+ default:
+ return 0
+ }
+}
+
+// buildActressWorkMixedListWithoutTotal 根据无总数混排分页窗口组装当前页列表
+func buildActressWorkMixedListWithoutTotal(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string, start, end, contentStart, adsTotal int64) []*proto.QueryVideoListItem {
+ resp := make([]*proto.QueryVideoListItem, 0, end-start)
+ contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
+ for i, item := range list {
+ contentByIndex[contentStart+int64(i)] = item
+ }
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, actressWorkAdInterval)
+ if adIndex >= 0 && adIndex < adsTotal {
+ resp = append(resp, actressWorkAdToListItem(ads[adIndex], cdn))
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, actressWorkAdInterval)
+ if item := contentByIndex[contentIndex]; item != nil {
+ resp = append(resp, actressWorkVideoToListItem(item, cdn))
+ }
+ }
+ return resp
+}
+
+// actressWorkVideoToListItem 将视频模型转换为女优作品列表中的视频项
+func actressWorkVideoToListItem(item *videoinformationmod.VideoInformation, cdn string) *proto.QueryVideoListItem {
+ tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
+ return &proto.QueryVideoListItem{
+ Type: proto.QueryVideoItemTypeVideo,
+ VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
+ Id: item.Id,
+ Title: item.Title,
+ Slug: item.Slug,
+ AddedTime: item.AddedTime,
+ CoverImg: netutil.MergeURL(cdn, item.CoverImg),
+ Tags: item.Tags,
+ TagsName: tagNames,
+ TagSlugs: tagSlugs,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ }},
+ }
+}
+
+// actressWorkAdToListItem 将列表广告转换为女优作品列表中的广告项
+func actressWorkAdToListItem(ad proxy.AdvitiseInfo, cdn string) *proto.QueryVideoListItem {
+ return &proto.QueryVideoListItem{
+ Type: proto.QueryVideoItemTypeAdvertise,
+ AdvertiseId: ad.Id,
+ Link: ad.Link,
+ Location: ad.Location,
+ VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
+ Title: ad.Title,
+ CoverImg: netutil.MergeURL(cdn, ad.Images),
+ }},
+ }
+}
diff --git a/app/service/appser/appinfo.go b/app/service/appser/appinfo.go
new file mode 100644
index 0000000..48ef02f
--- /dev/null
+++ b/app/service/appser/appinfo.go
@@ -0,0 +1,17 @@
+package appser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/srvdef"
+)
+
+// AppCategoryData 查询应用分类列表及当前分类数据
+func AppCategoryData(ctx context.Context, categoryId int64) ([]proto.AppCategoryInfo, proto.AppCategoryInfo, *srvdef.SrvErr) {
+ list := GetAppListCache()
+ category := GetCategoryCache(categoryId)
+ configser.RewriteImageCdn(ctx, &list)
+ configser.RewriteImageCdn(ctx, &category)
+ return list, category, nil
+}
diff --git a/app/service/appser/cache.go b/app/service/appser/cache.go
new file mode 100644
index 0000000..e88788e
--- /dev/null
+++ b/app/service/appser/cache.go
@@ -0,0 +1,195 @@
+package appser
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/url"
+ "strings"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/http"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/robfig/cron/v3"
+)
+
+const appListAPIPath = "/media/web/appinfo/applist"
+
+var jsonHeader = map[string]string{"Content-Type": "application/json"}
+
+var (
+ appListCron *cron.Cron
+ appListCache struct {
+ lock sync.RWMutex
+ appList []proto.AppCategoryInfo
+ categoryMaps map[int64]proto.AppCategoryInfo
+ }
+)
+
+type remoteAppListResp struct {
+ Code int `json:"code"`
+ Data struct {
+ List []proto.AppCategoryInfo `json:"list"`
+ } `json:"data"`
+}
+
+// Start 启动定时刷新应用列表缓存(每10分钟一次)
+func Start() {
+ appListCron = cron.New(cron.WithSeconds())
+ UpdateAppListCache(context.Background())
+ _, _ = appListCron.AddFunc("0 */10 * * * ?", func() {
+ UpdateAppListCache(context.Background())
+ })
+ appListCron.Start()
+}
+
+// GetAppListCache 返回全部应用分类列表(带各分类下应用)
+func GetAppListCache() []proto.AppCategoryInfo {
+ appListCache.lock.RLock()
+ defer appListCache.lock.RUnlock()
+ if len(appListCache.appList) == 0 {
+ return make([]proto.AppCategoryInfo, 0)
+ }
+ result := make([]proto.AppCategoryInfo, len(appListCache.appList))
+ copy(result, appListCache.appList)
+ return result
+}
+
+// GetCategoryCache 按分类ID返回单个分类(含应用列表);categoryId=0 返回第一个分类
+func GetCategoryCache(categoryId int64) proto.AppCategoryInfo {
+ appListCache.lock.RLock()
+ defer appListCache.lock.RUnlock()
+ if categoryId == 0 && len(appListCache.appList) > 0 {
+ return appListCache.appList[0]
+ }
+ if v, ok := appListCache.categoryMaps[categoryId]; ok {
+ return v
+ }
+ return proto.AppCategoryInfo{AppList: make([]proto.AppInfoData, 0)}
+}
+
+// UpdateAppListCache 从远端拉取应用列表并刷新内存缓存
+func UpdateAppListCache(ctx context.Context) {
+ domain := proxy.DomainAddressRepair("", proxy.BucketVideoAddressV2)
+ apiURL := strings.TrimSuffix(domain, "/") + appListAPIPath
+
+ reqBody, _ := json.Marshal(map[string]string{"dc": "dc"})
+ resp, err := http.Post(apiURL, jsonHeader, bytes.NewReader(reqBody))
+ if err != nil {
+ log.Warn("UpdateAppListCache http.Post failed", log.Any("url", apiURL), log.E(err))
+ return
+ }
+
+ var remote remoteAppListResp
+ if err = json.Unmarshal(resp.Data, &remote); err != nil {
+ log.Warn("UpdateAppListCache unmarshal failed", log.Any("url", apiURL), log.E(err))
+ return
+ }
+ if remote.Code != 200 {
+ log.Warn("UpdateAppListCache bad code", log.Any("url", apiURL), log.Any("code", remote.Code))
+ return
+ }
+
+ config := configser.GetConfigInfo()
+ advitise := configser.GetAdvitise()
+ channelCode := config.ChannelCode
+
+ categoryMaps := make(map[int64]proto.AppCategoryInfo)
+ var appList []proto.AppCategoryInfo
+
+ for _, category := range remote.Data.List {
+ var apps []proto.AppInfoData
+ for _, app := range category.AppList {
+ // 过滤掉本应用自身
+ if strings.HasPrefix(app.Name, pubdef.AppName) {
+ continue
+ }
+ apps = append(apps, proto.AppInfoData{
+ Id: app.Id,
+ CategoryId: app.CategoryId,
+ Name: app.Name,
+ Intro: app.Intro,
+ Description: app.Description,
+ Logo: netutil.MergeURL(config.ImgCdn, app.Logo),
+ Url: appendChannelCode(app.Url, channelCode),
+ Sort: app.Sort,
+ })
+ }
+ // 广告位 A(code=12)注入图标广告
+ if category.Code == 12 {
+ for _, ad := range advitise.AList {
+ apps = append(apps, proto.AppInfoData{
+ AdvitiseId: ad.Id,
+ CategoryId: category.Id,
+ Name: ad.Title,
+ Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
+ Url: ad.Link,
+ Location: ad.Location,
+ })
+ }
+ }
+ // 广告位 B(code=13)注入图标文字广告
+ if category.Code == 13 {
+ for _, ad := range advitise.BList {
+ apps = append(apps, proto.AppInfoData{
+ AdvitiseId: ad.Id,
+ CategoryId: category.Id,
+ Name: ad.Title,
+ Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
+ Url: ad.Link,
+ Location: ad.Location,
+ })
+ }
+ }
+ // 空分类不展示
+ if len(apps) == 0 {
+ continue
+ }
+ category.AppList = apps
+ categoryMaps[category.Id] = proto.AppCategoryInfo{
+ Id: category.Id,
+ Title: category.Title,
+ Intro: category.Intro,
+ Sort: category.Sort,
+ Code: category.Code,
+ }
+ appList = append(appList, category)
+ }
+
+ appListCache.lock.Lock()
+ appListCache.appList = appList
+ appListCache.categoryMaps = categoryMaps
+ appListCache.lock.Unlock()
+}
+
+// appendChannelCode 为应用 URL 拼接渠道码参数 dc=xxx;已含 dc 参数则不再拼接
+func appendChannelCode(rawURL, channelCode string) string {
+ if len(channelCode) == 0 {
+ channelCode = pubdef.DefaultChannelCode
+ }
+
+ rawURL = strings.TrimSpace(rawURL)
+ channelCode = strings.TrimSpace(channelCode)
+ if rawURL == "" || channelCode == "" {
+ return rawURL
+ }
+
+ parsed, err := url.Parse(rawURL)
+ if err != nil {
+ return rawURL
+ }
+
+ query := parsed.Query()
+ if query.Has("dc") {
+ return parsed.String()
+ }
+
+ query.Set("dc", channelCode)
+ parsed.RawQuery = query.Encode()
+ return parsed.String()
+}
diff --git a/app/service/authser/auth.go b/app/service/authser/auth.go
new file mode 100644
index 0000000..c8eab1e
--- /dev/null
+++ b/app/service/authser/auth.go
@@ -0,0 +1,587 @@
+package authser
+
+import (
+ "context"
+ "math/rand"
+ "strconv"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "golang.org/x/crypto/bcrypt"
+)
+
+var tokener = utils.NewTokenRedis()
+
+var defaultAvatars = []string{
+ "v3/image/bo/ty/1016/girl1.jpeg",
+ "v3/image/re/9y/1016/girl2.jpeg",
+ "v3/image/5f/k4/1016/girl3.jpeg",
+ "v3/image/ej/ss/1016/girl4.jpeg",
+ "v3/image/yq/ys/1016/girl5.jpeg",
+ "v3/image/9e/d3/1016/girl6.jpeg",
+}
+
+func randomAvatar() string {
+ return defaultAvatars[time.Now().UnixNano()%int64(len(defaultAvatars))]
+}
+
+// generateUniqueInviteCode 生成 8 位唯一邀请码(含大小写字母),最多重试 10 次
+func generateUniqueInviteCode(ctx context.Context) string {
+ for i := 0; i < 10; i++ {
+ code := utils.GetRandomLetters(8)
+ exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.InviteCode: code})
+ if exist == nil {
+ return code
+ }
+ log.Warn("authser:generateUniqueInviteCode collision, retrying", log.Any("code", code))
+ }
+ // 极小概率走到这里:用更长的随机串兜底
+ return utils.GetRandomLetters(12)
+}
+
+// LoginResp 登录/注册成功后的响应数据
+type LoginResp struct {
+ Token string `json:"token"`
+ UserId int64 `json:"userId"`
+ AccountNo string `json:"accountNo"`
+ NickName string `json:"nickName"`
+ Status pubdef.UserStatus `json:"status"`
+ Avatar string `json:"avatar"` // 头像
+ InviteCode string `json:"inviteCode"` // 邀请码
+ LoginType pubdef.LoginType `json:"loginType"` // 登录方式 0=设备 3=账号
+ IsRegister bool `json:"isRegister"` // 是否已完成账号注册
+ IsNewUser bool `json:"isNewUser"` // 是否本次请求新创建的用户
+ Recharge int64 `json:"recharge"` // 当前金币个数
+}
+
+// UserInfoResp 当前登录用户信息
+type UserInfoResp struct {
+ UserId int64 `json:"userId"`
+ AccountNo string `json:"accountNo"`
+ NickName string `json:"nickName"`
+ Status pubdef.UserStatus `json:"status"`
+ Avatar string `json:"avatar"`
+ InviteCode string `json:"inviteCode"`
+ LoginType pubdef.LoginType `json:"loginType"`
+ IsRegister bool `json:"isRegister"`
+ Recharge int64 `json:"recharge"`
+}
+
+// UAInfo 从 UA 中间件提取的设备/系统信息
+type UAInfo struct {
+ Device string // 设备型号(DevType,如 "iPhone")
+ SysType pubdef.SysType // 系统类型(ios / android / h5 等)
+ AppVer string // App 版本号
+ IP string // 客户端 IP
+}
+
+// GuestLoginReq 游客登录请求
+type GuestLoginReq struct {
+ DevID string // 设备 ID(首选)
+ Mac string // 设备 MAC 地址(备用)
+ DeviceId string // 设备 ID 第二版本(备用)
+ UA UAInfo // 设备/系统信息
+}
+
+// hashPassword 使用 bcrypt 对明文密码进行哈希
+func hashPassword(plain string) (string, error) {
+ hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
+ if err != nil {
+ return "", err
+ }
+ return string(hash), nil
+}
+
+// checkPassword 验证明文密码与 bcrypt 哈希是否匹配
+func checkPassword(plain, hash string) bool {
+ return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
+}
+
+// generateAndSaveToken 生成 JWT 并存入 Redis
+func generateAndSaveToken(userId int64) (string, *srvdef.SrvErr) {
+ token, err := utils.GenerateUserToken(utils.UserClaims{
+ UserID: userId,
+ Expire: consts.UserJwtTokenExpire,
+ }, appg.Conf.JwtKey)
+ if err != nil {
+ return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
+ }
+ if err = tokener.Save(userId, token); err != nil {
+ return "", srvdef.New(srvdef.StoreLevel, "token存储失败", "")
+ }
+ return token, nil
+}
+
+// GuestLogin 游客登录:按设备 ID 查找已有用户,找不到则自动注册
+// 若请求携带有效 token(currentUserId != 0),优先复用该账号
+func GuestLogin(ctx context.Context, req *GuestLoginReq, currentUserId int64) (*LoginResp, *srvdef.SrvErr) {
+ var user *usermod.User
+
+ // 优先用 token 对应的已有用户
+ if currentUserId > 0 {
+ u, _ := usermod.FindOneUserById(ctx, currentUserId)
+ if u != nil {
+ user = u
+ }
+ }
+
+ // 按设备 ID 查找
+ if user == nil {
+ user, _ = findUserByDevice(ctx, req.DevID, req.Mac, req.DeviceId)
+ }
+
+ isNewUser := false
+ if user == nil {
+ // 未找到 → 自动注册游客
+ var serr *srvdef.SrvErr
+ user, serr = registerGuest(ctx, req)
+ if serr != nil {
+ return nil, serr
+ }
+ isNewUser = true
+ } else {
+ // 已有用户:更新设备信息和 UA(静默,不影响主流程)
+ updateSet := bson.M{
+ usermod.FN.User.LastLoginAt: time.Now(),
+ usermod.FN.User.UpdatedAt: time.Now(),
+ }
+ if req.DevID != "" {
+ updateSet[usermod.FN.User.DevID] = req.DevID
+ }
+ if req.Mac != "" && req.Mac != "00:00:00:00:00:00" {
+ updateSet[usermod.FN.User.Mac] = req.Mac
+ }
+ if req.DeviceId != "" {
+ updateSet[usermod.FN.User.DevV2] = req.DeviceId
+ }
+ if req.UA.Device != "" {
+ updateSet[usermod.FN.User.Device] = req.UA.Device
+ }
+ if req.UA.SysType != "" {
+ updateSet[usermod.FN.User.SysType] = req.UA.SysType.String()
+ }
+ if req.UA.AppVer != "" {
+ updateSet[usermod.FN.User.AppVer] = req.UA.AppVer
+ }
+ if req.UA.IP != "" {
+ updateSet[usermod.FN.User.LastIp] = req.UA.IP
+ }
+ _ = usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: user.Id},
+ bson.M{"$set": updateSet},
+ )
+ }
+
+ if user.Status == pubdef.UserStatusDisable {
+ return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
+ }
+
+ token, serr := generateAndSaveToken(user.Id)
+ if serr != nil {
+ return nil, serr
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ return &LoginResp{
+ Token: token,
+ UserId: user.Id,
+ AccountNo: user.AccountNo,
+ NickName: user.NickName,
+ Status: user.Status,
+ Avatar: netutil.MergeURL(cdn, user.Avatar),
+ InviteCode: user.InviteCode,
+ LoginType: user.LoginType,
+ IsRegister: user.IsRegister,
+ IsNewUser: isNewUser,
+ Recharge: user.Recharge,
+ }, nil
+}
+
+// GetInfo 根据当前登录 token 解析出的用户 ID 查询用户信息
+func GetInfo(ctx context.Context, userId int64) (*UserInfoResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, srvdef.InvalidToken.Msg(), "")
+ }
+ user, serr := usermod.FindOneUserById(ctx, userId, options.FindOne().SetProjection(userInfoProjection()))
+ if serr != nil {
+ return nil, serr
+ }
+ if user == nil {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
+ }
+ if user.Status == pubdef.UserStatusDisable {
+ return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ return &UserInfoResp{
+ UserId: user.Id,
+ AccountNo: user.AccountNo,
+ NickName: user.NickName,
+ Status: user.Status,
+ Avatar: netutil.MergeURL(cdn, user.Avatar),
+ InviteCode: user.InviteCode,
+ LoginType: user.LoginType,
+ IsRegister: user.IsRegister,
+ Recharge: user.Recharge,
+ }, nil
+}
+
+func userInfoProjection() bson.M {
+ return bson.M{
+ usermod.FN.User.Id: 1,
+ usermod.FN.User.AccountNo: 1,
+ usermod.FN.User.NickName: 1,
+ usermod.FN.User.Status: 1,
+ usermod.FN.User.Avatar: 1,
+ usermod.FN.User.InviteCode: 1,
+ usermod.FN.User.LoginType: 1,
+ usermod.FN.User.IsRegister: 1,
+ usermod.FN.User.Recharge: 1,
+ }
+}
+
+// findUserByDevice 按设备 ID 三级回退查找用户(DevID → Mac → DeviceId)
+func findUserByDevice(ctx context.Context, devID, mac, deviceId string) (*usermod.User, *srvdef.SrvErr) {
+ invalidMac := "00:00:00:00:00:00"
+ if devID != "" {
+ u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: devID})
+ if u != nil {
+ return u, nil
+ }
+ }
+ if mac != "" && mac != invalidMac {
+ u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac})
+ if u != nil {
+ return u, nil
+ }
+ }
+ if deviceId != "" && deviceId != invalidMac {
+ u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId})
+ if u != nil {
+ return u, nil
+ }
+ }
+ return nil, nil
+}
+
+// registerGuest 自动创建游客用户
+func registerGuest(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) {
+ now := time.Now()
+ user := &usermod.User{
+ Id: mongos.Lid.User.Get(),
+ NickName: getName(),
+ Avatar: randomAvatar(),
+ InviteCode: generateUniqueInviteCode(ctx),
+ Status: pubdef.UserStatusNormal,
+ LoginType: pubdef.LoginTypeDevice,
+ IsRegister: false,
+ DevID: req.DevID,
+ Mac: req.Mac,
+ DevV2: req.DeviceId,
+ Device: req.UA.Device,
+ SysType: req.UA.SysType.String(),
+ AppVer: req.UA.AppVer,
+ OriVer: req.UA.AppVer,
+ RegIp: req.UA.IP,
+ LastIp: req.UA.IP,
+ RegisterTime: now,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if serr := usermod.InsertOneUser(ctx, user); serr != nil {
+ return nil, serr
+ }
+ return user, nil
+}
+
+// Register 账号注册
+// currentUserId != 0 时为游客升级模式:在已有游客账号上绑定账号密码
+// currentUserId == 0 时为直接注册新账号
+func Register(ctx context.Context, accountNo, password, agentInviteCode string, currentUserId int64, ua UAInfo) (*LoginResp, *srvdef.SrvErr) {
+ // 检查 accountNo 是否已被其他用户占用
+ exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo})
+ if exist != nil && exist.Id != currentUserId {
+ return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "")
+ }
+
+ hashed, err := hashPassword(password)
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, "密码处理失败", "")
+ }
+
+ now := time.Now()
+
+ // 游客升级:更新已有记录
+ if currentUserId > 0 {
+ user, _ := usermod.FindOneUserById(ctx, currentUserId)
+ if user == nil {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "用户不存在", "")
+ }
+ if user.IsRegister {
+ return nil, srvdef.New(srvdef.ErrDbInputExist, "已完成账号注册,无需重复操作", "")
+ }
+ setDoc := bson.M{
+ usermod.FN.User.AccountNo: accountNo,
+ usermod.FN.User.Password: hashed,
+ usermod.FN.User.IsRegister: true,
+ usermod.FN.User.LoginType: pubdef.LoginTypeAccount,
+ usermod.FN.User.UpdatedAt: now,
+ }
+ if agentInviteCode != "" && user.AgentInviteCode == "" {
+ setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode
+ }
+ if ua.Device != "" {
+ setDoc[usermod.FN.User.Device] = ua.Device
+ }
+ if ua.SysType != "" {
+ setDoc[usermod.FN.User.SysType] = ua.SysType.String()
+ }
+ if ua.AppVer != "" {
+ setDoc[usermod.FN.User.AppVer] = ua.AppVer
+ }
+ if ua.IP != "" {
+ setDoc[usermod.FN.User.LastIp] = ua.IP
+ }
+ if serr := usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: currentUserId},
+ bson.M{"$set": setDoc},
+ ); serr != nil {
+ return nil, serr
+ }
+ // 给邀请人 +1
+ if agentInviteCode != "" {
+ _ = usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.InviteCode: agentInviteCode},
+ bson.M{"$inc": bson.M{usermod.FN.User.Invites: 1},
+ "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()}},
+ )
+ }
+ token, serr := generateAndSaveToken(user.Id)
+ if serr != nil {
+ return nil, serr
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ return &LoginResp{
+ Token: token,
+ UserId: user.Id,
+ AccountNo: accountNo,
+ NickName: user.NickName,
+ Status: user.Status,
+ Avatar: netutil.MergeURL(cdn, user.Avatar),
+ InviteCode: user.InviteCode,
+ LoginType: pubdef.LoginTypeAccount,
+ IsRegister: true,
+ Recharge: user.Recharge,
+ }, nil
+ }
+
+ // 未登录:直接注册新账号
+ user := &usermod.User{
+ Id: mongos.Lid.User.Get(),
+ AccountNo: accountNo,
+ Password: hashed,
+ NickName: getName(),
+ Avatar: randomAvatar(),
+ InviteCode: generateUniqueInviteCode(ctx),
+ AgentInviteCode: agentInviteCode,
+ Status: pubdef.UserStatusNormal,
+ IsRegister: true,
+ LoginType: pubdef.LoginTypeAccount,
+ Device: ua.Device,
+ SysType: ua.SysType.String(),
+ AppVer: ua.AppVer,
+ OriVer: ua.AppVer,
+ RegIp: ua.IP,
+ LastIp: ua.IP,
+ RegisterTime: now,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if serr := usermod.InsertOneUser(ctx, user); serr != nil {
+ return nil, serr
+ }
+ if agentInviteCode != "" {
+ _ = usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.InviteCode: agentInviteCode},
+ bson.M{
+ "$inc": bson.M{usermod.FN.User.Invites: 1},
+ "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
+ },
+ )
+ }
+ token, serr := generateAndSaveToken(user.Id)
+ if serr != nil {
+ return nil, serr
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ return &LoginResp{
+ Token: token,
+ UserId: user.Id,
+ AccountNo: user.AccountNo,
+ NickName: user.NickName,
+ Status: user.Status,
+ Avatar: netutil.MergeURL(cdn, user.Avatar),
+ InviteCode: user.InviteCode,
+ LoginType: pubdef.LoginTypeAccount,
+ IsRegister: true,
+ }, nil
+}
+
+// Login 用户登录(账号均可)
+func Login(ctx context.Context, account, password string, ua UAInfo) (*LoginResp, *srvdef.SrvErr) {
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: account})
+ if user == nil {
+ return nil, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "")
+ }
+
+ if user.Status == pubdef.UserStatusDisable {
+ return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
+ }
+
+ if !checkPassword(password, user.Password) {
+ return nil, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
+ }
+
+ token, serr := generateAndSaveToken(user.Id)
+ if serr != nil {
+ return nil, serr
+ }
+
+ // 更新最后登录时间、IP 及 UA(fire-and-forget)
+ loginSet := bson.M{
+ usermod.FN.User.LastLoginAt: time.Now(),
+ usermod.FN.User.UpdatedAt: time.Now(),
+ }
+ if ua.IP != "" {
+ loginSet[usermod.FN.User.LastIp] = ua.IP
+ }
+ if ua.Device != "" {
+ loginSet[usermod.FN.User.Device] = ua.Device
+ }
+ if ua.SysType != "" {
+ loginSet[usermod.FN.User.SysType] = ua.SysType.String()
+ }
+ if ua.AppVer != "" {
+ loginSet[usermod.FN.User.AppVer] = ua.AppVer
+ }
+ _ = usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: user.Id},
+ bson.M{"$set": loginSet},
+ )
+ cdn := configser.ImageCdnFromCtx(ctx)
+ return &LoginResp{
+ Token: token,
+ UserId: user.Id,
+ AccountNo: user.AccountNo,
+ NickName: user.NickName,
+ Status: user.Status,
+ Avatar: netutil.MergeURL(cdn, user.Avatar),
+ InviteCode: user.InviteCode,
+ LoginType: user.LoginType,
+ IsRegister: user.IsRegister,
+ Recharge: user.Recharge,
+ }, nil
+}
+
+// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
+type UpdateProfileReq struct {
+ AccountNo string // 用户账号(用于定位用户,必传)
+ Avatar *string // 新头像 URL(可选)
+ NikeName *string // 昵称
+ OldPassword *string // 旧密码明文(修改密码时必传)
+ NewPassword *string // 新密码明文(修改密码时必传)
+}
+
+// UpdateProfile 修改用户资料:支持修改头像、密码,或两者同时修改
+func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr {
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: req.AccountNo})
+ if user == nil {
+ return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
+ }
+
+ setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()}
+
+ // 修改头像
+ if req.Avatar != nil && *req.Avatar != "" {
+ setDoc[usermod.FN.User.Avatar] = *req.Avatar
+ }
+ if req.NikeName != nil && *req.NikeName != "" {
+ setDoc[usermod.FN.User.NickName] = *req.NikeName
+ }
+
+ // 修改密码:两者必须同时提供
+ if req.OldPassword != nil || req.NewPassword != nil {
+ if req.OldPassword == nil || req.NewPassword == nil {
+ return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "")
+ }
+ if !checkPassword(*req.OldPassword, user.Password) {
+ return srvdef.New(srvdef.AccountErrPasswordError, "旧密码错误", "")
+ }
+ hashed, err := hashPassword(*req.NewPassword)
+ if err != nil {
+ return srvdef.New(srvdef.Failure, "密码处理失败", "")
+ }
+ setDoc[usermod.FN.User.Password] = hashed
+ }
+
+ // 没有任何需要更新的字段
+ if len(setDoc) == 1 {
+ return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "")
+ }
+
+ return usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: user.Id},
+ bson.M{"$set": setDoc},
+ )
+}
+
+func getName() string {
+ const limit = 300
+ totalCnt, err := redis.SCard(redis.NameSource)
+ if err != nil || totalCnt < limit {
+ if len(appg.Name.FamilyNames) == 0 || len(appg.Name.AfterNames) == 0 {
+ // fmt.Println(totalCnt, err, appg.Name.FamilyNames)
+ return "游客" + strconv.FormatInt(time.Now().Unix(), 10)
+ }
+ for i := 0; i < limit/100; i++ {
+ go func() {
+ var names []string
+ for j := 0; j < limit/3; j++ {
+ length := 2 + rand.Intn(2)
+ if length == 2 {
+ name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] +
+ appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))]
+ names = append(names, name)
+
+ }
+ if length == 3 {
+ name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] +
+ appg.Name.MiddleNames[utils.RandInt(0, len(appg.Name.MiddleNames))] +
+ appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))]
+ names = append(names, name)
+ }
+ }
+ _ = redis.SAdd(redis.NameSource, names)
+ }()
+ time.Sleep(100 * time.Millisecond)
+ }
+ _ = redis.Expire(redis.NameSource, redis.NameSourceExpire)
+ }
+
+ name, err := redis.SPop(redis.NameSource)
+ if name == "" || err != nil {
+ return "游客" + strconv.FormatInt(time.Now().Unix(), 10)
+ }
+ return name
+}
diff --git a/app/service/commentser/comment.go b/app/service/commentser/comment.go
new file mode 100644
index 0000000..13bb2c8
--- /dev/null
+++ b/app/service/commentser/comment.go
@@ -0,0 +1,506 @@
+package commentser
+
+import (
+ "context"
+ "encoding/json"
+ "strings"
+ "time"
+ "unicode/utf8"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/reportser"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ commentListLimit int64 = 200
+ subCommentListLimit int64 = 400
+ commentTreeDefaultPageSize int64 = 10
+ commentTreeMaxPageSize int64 = 100
+ commentTreeMaxDepth int = 5
+)
+
+type commentTreeNode struct {
+ info proto.CommentInfo
+ children []*commentTreeNode
+}
+
+// ListCommentTree 分页查询指定对象评论列表,并按父子关系组装树形结构
+func ListCommentTree(ctx context.Context, userId int64, req *proto.CommentListReq) (*proto.CommentListResp, *srvdef.SrvErr) {
+ if req.ObjectId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "对象ID不能为空")
+ }
+ if !pubdef.ValidCommentObjectType(req.ObjectType) {
+ return nil, srvdef.New(srvdef.ErrParamError, "评论类型有误")
+ }
+ if req.SortOrder != 0 && req.SortOrder != 1 && req.SortOrder != 2 {
+ return nil, srvdef.New(srvdef.ErrParamError, "排序方式有误")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, commentTreeDefaultPageSize, commentTreeMaxPageSize)
+ filter := bson.M{
+ commentmod.FN.Comment.ObjectId: req.ObjectId,
+ commentmod.FN.Comment.ObjectType: req.ObjectType,
+ commentmod.FN.Comment.ReviewStatus: 1,
+ commentmod.FN.Comment.Level: 1,
+ }
+ total, err := commentmod.CountComments(ctx, filter)
+ if err != nil {
+ log.Warn("ListCommentTree.CountComments", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ resp := &proto.CommentListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommentInfo, 0),
+ }}
+ if total == 0 {
+ return resp, nil
+ }
+ imgCdn := configser.ImageCdnFromCtx(ctx)
+ topList, err := commentmod.FindManyComment(ctx, filter, options.Find().
+ SetProjection(commentListProjection()).
+ SetSort(commentListSort(req.SortOrder)).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ log.Warn("ListCommentTree.FindManyComment", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ children, err := commentTreeChildren(ctx, req.ObjectId, req.ObjectType, collectCommentIds(topList))
+ if err != nil {
+ return nil, err
+ }
+ ids := append(collectCommentIds(topList), collectCommentIds(children)...)
+ likes, err := commentlikemod.FindByUserAndComments(ctx, userId, ids)
+ if err != nil {
+ return nil, err
+ }
+ liked := make(map[int64]bool, len(likes))
+ for _, like := range likes {
+ liked[like.CommentId] = true
+ }
+ resp.List = buildCommentTree(topList, children, imgCdn, liked)
+ return resp, nil
+}
+
+// commentListSort 热门按点赞数排序,默认按发布时间排序
+func commentListSort(sortOrder int64) bson.D {
+ if sortOrder == 1 {
+ return bson.D{{Key: commentmod.FN.Comment.CountLike, Value: -1}, {Key: commentmod.FN.Comment.Id, Value: -1}}
+ }
+ return bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}, {Key: commentmod.FN.Comment.Id, Value: -1}}
+}
+
+// GetCommentList 查询指定对象的首屏评论列表,并缓存短时间内重复读取
+func GetCommentList(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType) (*proto.GetCommentListResp, *srvdef.SrvErr) {
+ cacheKey := redisconst.CommentListCacheKey(objectId, objectType)
+ cacheData, cacheErr := redis.Get(cacheKey)
+ if cacheErr != nil {
+ log.Warn("获取缓存失败", log.Any("cacheKey", cacheKey), log.E(cacheErr))
+ }
+ if cacheData != nil {
+ var commentListResp *proto.GetCommentListResp
+ if err := json.Unmarshal([]byte(*cacheData), &commentListResp); err != nil {
+ log.Warn("解析缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
+ } else if commentListResp != nil {
+ configser.RewriteImageCdn(ctx, commentListResp)
+ return commentListResp, nil
+ }
+ }
+ data, err := commentList(ctx, objectId, objectType)
+ if err != nil {
+ return nil, err
+ }
+ bytes, marshalErr := json.Marshal(data)
+ if marshalErr != nil {
+ log.Warn("解析缓存失败", log.Any("cacheKey", cacheKey), log.E(marshalErr))
+ configser.RewriteImageCdn(ctx, data)
+ return data, nil
+ }
+ if setErr := redis.Set(cacheKey, string(bytes), redisconst.CommentListKeyExpire); setErr != nil {
+ log.Warn("设置缓存失败", log.Any("cacheKey", cacheKey), log.E(setErr))
+ }
+ configser.RewriteImageCdn(ctx, data)
+ return data, nil
+}
+
+// commentList 查询顶级评论,并批量补齐二级回复,避免逐条查询子评论
+func commentList(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType) (*proto.GetCommentListResp, *srvdef.SrvErr) {
+ filter := bson.M{
+ commentmod.FN.Comment.ObjectId: objectId,
+ commentmod.FN.Comment.ObjectType: objectType,
+ commentmod.FN.Comment.ReviewStatus: 1,
+ commentmod.FN.Comment.Level: 1,
+ }
+ total, err := commentmod.CountComments(ctx, filter)
+ if err != nil {
+ log.Warn("commentList.CountComments", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ resp := &proto.GetCommentListResp{
+ ObjectId: objectId,
+ Total: total,
+ List: make([]proto.CommentInfo, 0),
+ }
+ if total == 0 {
+ return resp, nil
+ }
+ imgCdn := configser.GetConfigInfo().ImgCdn
+ list, err := commentmod.FindManyComment(ctx, filter, options.Find().
+ SetProjection(commentListProjection()).
+ SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}).
+ SetLimit(commentListLimit))
+ if err != nil {
+ log.Warn("commentList.FindManyComment", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ parentIds := make([]int64, 0, len(list))
+ for _, item := range list {
+ parentIds = append(parentIds, item.Id)
+ }
+ subCommentMap, err := subCommentList(ctx, parentIds, imgCdn)
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ info := commentInfoData(item, imgCdn)
+ info.SubComment = subCommentMap[item.Id]
+ resp.List = append(resp.List, info)
+ }
+ return resp, nil
+}
+
+// subCommentList 批量查询二级评论,并按父评论ID分组
+func subCommentList(ctx context.Context, parentIds []int64, imgCdn string) (map[int64][]proto.CommentInfo, *srvdef.SrvErr) {
+ data := make(map[int64][]proto.CommentInfo, len(parentIds))
+ if len(parentIds) == 0 {
+ return data, nil
+ }
+ list, err := commentmod.FindManyComment(ctx, bson.M{
+ commentmod.FN.Comment.ParentsId: bson.M{"$in": parentIds},
+ commentmod.FN.Comment.ReviewStatus: 1,
+ }, options.Find().
+ SetProjection(commentListProjection()).
+ SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}).
+ SetLimit(subCommentListLimit))
+ if err != nil {
+ log.Warn("subCommentList.FindManyComment", log.Any("parentIds", parentIds), log.E(err))
+ return nil, err
+ }
+ for _, item := range list {
+ data[item.ParentsId] = append(data[item.ParentsId], commentInfoData(item, imgCdn))
+ }
+ return data, nil
+}
+
+// commentTreeChildren 按层批量查询当前页一级评论的子孙评论,避免 N+1 查询
+func commentTreeChildren(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType, parentIds []int64) (commentmod.CommentModelList, *srvdef.SrvErr) {
+ data := make(commentmod.CommentModelList, 0)
+ currentParentIds := uniqueCommentIds(parentIds)
+ if len(currentParentIds) == 0 {
+ return data, nil
+ }
+ seenIds := make(map[int64]struct{}, len(currentParentIds))
+ for _, id := range currentParentIds {
+ seenIds[id] = struct{}{}
+ }
+ for depth := 0; len(currentParentIds) > 0 && depth < commentTreeMaxDepth; depth++ {
+ filter := bson.M{
+ commentmod.FN.Comment.ObjectId: objectId,
+ commentmod.FN.Comment.ObjectType: objectType,
+ commentmod.FN.Comment.ParentsId: bson.M{"$in": currentParentIds},
+ commentmod.FN.Comment.ReviewStatus: 1,
+ }
+ list, err := commentmod.FindManyComment(ctx, filter, options.Find().
+ SetProjection(commentListProjection()).
+ SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}))
+ if err != nil {
+ log.Warn("commentTreeChildren.FindManyComment", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ nextParentIds := make([]int64, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if _, ok := seenIds[item.Id]; ok {
+ continue
+ }
+ seenIds[item.Id] = struct{}{}
+ data = append(data, item)
+ nextParentIds = append(nextParentIds, item.Id)
+ }
+ currentParentIds = nextParentIds
+ }
+ return data, nil
+}
+
+// buildCommentTree 将一级评论和子孙评论按 parentsId 组装为树形结构
+func buildCommentTree(topList commentmod.CommentModelList, children commentmod.CommentModelList, imgCdn string, liked map[int64]bool) []proto.CommentInfo {
+ nodes := make(map[int64]*commentTreeNode, len(topList)+len(children))
+ roots := make([]*commentTreeNode, 0, len(topList))
+ for _, item := range topList {
+ if item == nil {
+ continue
+ }
+ info := commentInfoData(item, imgCdn)
+ info.IsLike = liked[item.Id]
+ node := &commentTreeNode{info: info}
+ nodes[item.Id] = node
+ roots = append(roots, node)
+ }
+ for _, item := range children {
+ if item == nil {
+ continue
+ }
+ info := commentInfoData(item, imgCdn)
+ info.IsLike = liked[item.Id]
+ nodes[item.Id] = &commentTreeNode{info: info}
+ }
+ for _, item := range children {
+ if item == nil {
+ continue
+ }
+ parentNode, ok := nodes[item.ParentsId]
+ if !ok {
+ continue
+ }
+ if node, ok := nodes[item.Id]; ok {
+ parentNode.children = append(parentNode.children, node)
+ }
+ }
+ return commentTreeNodesToInfos(roots)
+}
+
+// commentTreeNodesToInfos 递归输出评论树响应
+func commentTreeNodesToInfos(nodes []*commentTreeNode) []proto.CommentInfo {
+ list := make([]proto.CommentInfo, 0, len(nodes))
+ for _, node := range nodes {
+ if node == nil {
+ continue
+ }
+ info := node.info
+ info.SubComment = commentTreeNodesToInfos(node.children)
+ list = append(list, info)
+ }
+ return list
+}
+
+// collectCommentIds 收集评论ID集合
+func collectCommentIds(list commentmod.CommentModelList) []int64 {
+ ids := make([]int64, 0, len(list))
+ for _, item := range list {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ ids = append(ids, item.Id)
+ }
+ return ids
+}
+
+// uniqueCommentIds 去重评论ID集合
+func uniqueCommentIds(ids []int64) []int64 {
+ seen := make(map[int64]struct{}, len(ids))
+ result := make([]int64, 0, 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
+}
+
+// commentInfoData 将评论模型转换为 APP 评论响应结构
+func commentInfoData(item *commentmod.CommentModel, imgCdn string) proto.CommentInfo {
+ return proto.CommentInfo{
+ Id: item.Id,
+ ObjectId: item.ObjectId,
+ UserName: item.UserName,
+ Avatar: netutil.MergeURL(imgCdn, item.Avatar),
+ Level: item.Level,
+ ParentsId: item.ParentsId,
+ ReplyUserName: item.ReplyUserName,
+ Text: item.Text,
+ CommentCount: item.CommentCount,
+ CountLike: item.CountLike,
+ SubComment: make([]proto.CommentInfo, 0),
+ CreatedAt: item.CreatedAt,
+ }
+}
+
+// commentListProjection 限定评论列表字段,减少详情页评论查询的数据传输
+func commentListProjection() bson.M {
+ return bson.M{
+ commentmod.FN.Comment.Id: 1,
+ commentmod.FN.Comment.ObjectId: 1,
+ commentmod.FN.Comment.UserName: 1,
+ commentmod.FN.Comment.Avatar: 1,
+ commentmod.FN.Comment.Level: 1,
+ commentmod.FN.Comment.ParentsId: 1,
+ commentmod.FN.Comment.ReplyUserName: 1,
+ commentmod.FN.Comment.Text: 1,
+ commentmod.FN.Comment.CommentCount: 1,
+ commentmod.FN.Comment.CountLike: 1,
+ commentmod.FN.Comment.CreatedAt: 1,
+ }
+}
+
+func SendComment(ctx context.Context, ip string, userId int64, req *proto.CommentSendReq, ua useragent.UA) *srvdef.SrvErr {
+ if req == nil || req.ObjectId <= 0 || req.ParentsId < 0 || !pubdef.ValidCommentObjectType(req.ObjectType) {
+ return srvdef.New(srvdef.ErrParamError, "评论参数有误")
+ }
+ req.Text = strings.TrimSpace(req.Text)
+ if req.Text == "" {
+ return srvdef.New(srvdef.ErrParamError, "评论内容不能为空")
+ }
+ if utf8.RuneCountInString(req.Text) > 200 {
+ return srvdef.New(srvdef.ErrParamError, "评论内容不能超过200字")
+ }
+ curValue, err := redis.Incr(redisconst.CommentLimitKey(ip), redisconst.CommentLimitKeyExpire)
+ if err != nil {
+ log.Error("SendComment redis incr failed", log.E(err))
+ }
+ //15秒超3评论提示频繁操作
+ if curValue > 1 {
+ log.Error("SendComment user comment too frequently!",
+ log.Any("ip", ip), log.Any("req", req), log.E(err))
+ return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
+ }
+
+ //操作频繁度判断
+ dayCurValue, err := redis.Incr(redisconst.CommentOneDayLimitKey(ip), redisconst.CommentDayLimitKeyExpire)
+ if err != nil {
+ log.Error("SendComment redis incr failed", log.E(err))
+ }
+ // 每天20条
+ if dayCurValue > 50 {
+ log.Error("SendComment user comment too frequently!",
+ log.Any("ip", ip), log.Any("req", req), log.E(err))
+ return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
+ }
+ if !pubdef.ValidCommentObjectType(req.ObjectType) {
+ return srvdef.New(srvdef.ErrParamError, "评论类型有误")
+ }
+ objectName := pubdef.CommentObjectTypeName(req.ObjectType)
+ var videoInfo2 videoinformationmod.VideoInformation
+ switch req.ObjectType {
+ case pubdef.CommentObjectTypeVideo:
+ videoInfo, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.ObjectId,
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ })
+ if err1 != nil {
+ log.Warn("SendComment.FindOneVideoInformation error", log.Any("objectId", req.ObjectId), log.E(err1))
+ return err1
+ }
+ videoInfo2 = *videoInfo
+ case pubdef.CommentObjectTypePlaylet:
+ _, err1 := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: req.ObjectId,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ })
+ if err1 != nil {
+ log.Warn("SendComment.FindOnePlaylet error", log.Any("objectId", req.ObjectId), log.E(err1))
+ return err1
+ }
+ case pubdef.CommentObjectTypePlayletChapter:
+ chapter, err1 := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: req.ObjectId,
+ })
+ if err1 != nil {
+ return err1
+ }
+ _, err1 = playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ })
+ if err1 != nil {
+ return err1
+ }
+ }
+ // 通过 token 中的 userId 查询用户信息,补充用户名和头像
+ userInfo, err1 := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ if err1 != nil {
+ log.Warn("SendComment.FindOneUser error", log.Any("userId", userId), log.E(err1))
+ return err1
+ }
+ userName := userInfo.NickName
+ if userName == "" {
+ userName = userInfo.AccountNo
+ }
+
+ level := int64(1)
+ replyUserName := ""
+ if req.ParentsId > 0 {
+ // 回复评论
+ parentsInfo, err1 := commentmod.FindOneComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: req.ParentsId,
+ commentmod.FN.Comment.ObjectId: req.ObjectId,
+ commentmod.FN.Comment.ObjectType: req.ObjectType,
+ commentmod.FN.Comment.ReviewStatus: 1,
+ })
+ if err1 != nil {
+ log.Warn("SendComment.FindOneComment error", log.Any("objectId", req.ObjectId), log.Any("parentsId", req.ParentsId), log.E(err1))
+ return err1
+ }
+ level = parentsInfo.Level + 1
+ if level > int64(commentTreeMaxDepth+1) {
+ return srvdef.New(srvdef.ErrParamError, "回复层级过深")
+ }
+ replyUserName = parentsInfo.UserName
+ }
+ info := commentmod.CommentModel{
+ Id: mongos.Lid.Comment.Get(),
+ Ip: ip,
+ ObjectId: req.ObjectId,
+ ObjectName: objectName,
+ ObjectType: req.ObjectType,
+ UserId: userId,
+ UserName: userName,
+ Avatar: userInfo.Avatar,
+ Level: level,
+ ParentsId: req.ParentsId,
+ ReplyUserName: replyUserName,
+ Text: req.Text,
+ CommentCount: 0,
+ ReviewStatus: 0,
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ }
+ if err1 := commentmod.InsertOneComment(ctx, &info); err1 != nil {
+ return err1
+ }
+ var reportErr error
+ switch req.ObjectType {
+ case pubdef.CommentObjectTypeVideo:
+ reportErr = reportser.ReportVideoComment(userId, ua, ip, &videoInfo2, req.Text)
+ }
+ if reportErr != nil {
+ log.Warn("SendComment.ReportCommentEvent error", log.Any("objectType", req.ObjectType), log.Any("objectId", req.ObjectId), log.E(reportErr))
+ }
+ return nil
+}
diff --git a/app/service/commentser/like.go b/app/service/commentser/like.go
new file mode 100644
index 0000000..c6f8050
--- /dev/null
+++ b/app/service/commentser/like.go
@@ -0,0 +1,63 @@
+package commentser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// SetCommentLike 设置评论点赞状态
+func SetCommentLike(ctx context.Context, userId int64, req *proto.CommentLikeSetReq) (*proto.CommentLikeSetResp, *srvdef.SrvErr) {
+ if userId <= 0 || req == nil || req.CommentId <= 0 || req.IsLike == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ comment, err := commentmod.FindOneComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: req.CommentId,
+ commentmod.FN.Comment.ReviewStatus: 1,
+ })
+ if err != nil {
+ return nil, err
+ }
+ var changed bool
+ if *req.IsLike {
+ changed, err = commentlikemod.Add(ctx, userId, req.CommentId)
+ } else {
+ changed, err = commentlikemod.Remove(ctx, userId, req.CommentId)
+ }
+ if err != nil {
+ return nil, err
+ }
+ count := comment.CountLike
+ if changed {
+ delta := int64(1)
+ if !*req.IsLike {
+ delta = -1
+ }
+ count, err = commentmod.ChangeLikeCount(ctx, req.CommentId, delta)
+ if err != nil {
+ if *req.IsLike {
+ _, _ = commentlikemod.Remove(ctx, userId, req.CommentId)
+ } else {
+ _, _ = commentlikemod.Add(ctx, userId, req.CommentId)
+ }
+ return nil, err
+ }
+ } else {
+ current, findErr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: req.CommentId})
+ if findErr != nil {
+ return nil, findErr
+ }
+ count = current.CountLike
+ }
+ if cacheErr := redis.Del(redisconst.CommentListCacheKey(comment.ObjectId, comment.ObjectType)); cacheErr != nil {
+ log.Warn("SetCommentLike clear cache", log.E(cacheErr))
+ }
+ return &proto.CommentLikeSetResp{CommentId: req.CommentId, IsLike: *req.IsLike, CountLike: count}, nil
+}
diff --git a/app/service/configser/announcement.go b/app/service/configser/announcement.go
new file mode 100644
index 0000000..239185b
--- /dev/null
+++ b/app/service/configser/announcement.go
@@ -0,0 +1,126 @@
+package configser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/app/appg"
+ "videoweb/pub/adcenter"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func UpdateAnnouncementData(ctx context.Context) {
+ config := GetConfigInfo()
+ resp := proto.AnnouncementInfo{
+ VideoInfo: make([]proto.VideoInfoData, 0),
+ PdfAddr: config.PdfAddr,
+ NewUrlAddr: config.DHNewUrlAddr1,
+ NewUrlAddr1: config.DHNewUrlAddr2,
+ NewUrlAddr2: config.DHNewUrlAddr3,
+ ForeverUrlAddr: config.ForeverUrlAddr,
+ AppAddr: config.AppAddr,
+ TranslationAddr: config.TranslationAddr,
+ }
+
+ videoFilter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.IsHot: true,
+ }
+ videoOpt := &options.FindOptions{}
+ videoOpt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Sort: -1})
+ videoOpt.SetLimit(2)
+
+ videoList, serr := videoinformationmod.FindManyVideoInformations(ctx, videoFilter, videoOpt)
+ if serr != nil {
+ log.Error("VideoCategoryData", log.Any("videoFilter", videoFilter), log.E(serr))
+ return
+ }
+ // 查询视频
+ for _, videoInfo := range videoList {
+ v := proto.VideoInfoToData(videoInfo, config.ImgCdn, GetCreateInfo(videoInfo.CreatorId, config.ImgCdn))
+ for _, tagId := range videoInfo.Tags {
+ tagInfo := GetVideoTag(tagId)
+ if tagInfo != nil {
+ v.Tags = append(v.Tags, proto.TagsInfoData{
+ Id: tagInfo.Id,
+ Name: tagInfo.Name,
+ Slug: tagInfo.Slug,
+ })
+ }
+ }
+ resp.VideoInfo = append(resp.VideoInfo, v)
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.Announcement = resp
+}
+
+// UpdateAdvitiseDataFromJt 从广告中心拉取福利页广告
+func UpdateAdvitiseDataFromJt() {
+ //config := GetConfigInfo()
+ var resp proto.AdvitiseData
+ jtAds, err := adcenter.JtAdvertiseThreeServer(appg.Conf.AdCenter.ApiDomain, appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode, appg.Conf.AdCenter.AesKey)
+ if err != nil {
+ log.Error("JtAdvertiseThreeServer error occur", log.E(err))
+ return
+ }
+ for _, _ = range jtAds {
+ //pos, err := strconv.Atoi(loc.AdvertiseLocationCode)
+ //if err != nil {
+ // log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode))
+ // continue
+ //}
+ ////if pos < 100001 {
+ //// continue
+ ////}
+ //ads := make([]proxy.AdvitiseInfo, 0)
+ //for _, ad := range loc.AdDetailInfoList {
+ // //extra := ad.GetExtraData()
+ // ads = append(ads, proxy.AdvitiseInfo{
+ // Id: ad.AdvertiseCode,
+ // Location: int64(pos),
+ // Title: ad.AdvertiseName,
+ // Images: netutil.MergeURL(config.ImgCdn, ad.GetCoverLsj()),
+ // Link: ad.GetRealLink(),
+ // })
+ //}
+ //
+ //switch pos {
+ //case 1:
+ // resp.HomeList = ads
+ //case 2:
+ // resp.DetailTopList = ads
+ //case 3:
+ // resp.NineGridList = ads
+ //case 4:
+ // resp.DetailbannerList = ads
+ //case 5:
+ // resp.LaunchScreenList = ads
+ //case 6:
+ // resp.MustAppList = ads
+ //case 7:
+ // resp.TitleAppList = ads
+ //case 8:
+ // resp.FriendList = ads
+ //case 9:
+ // resp.DialogList = ads
+ //case 10:
+ // resp.PostList = ads
+ //case 11:
+ // resp.FloatingList = ads
+ //case 12:
+ // resp.AList = ads
+ //case 13:
+ // resp.BList = ads
+ //case 14:
+ // resp.VideoList = ads
+ //}
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.Advitise = resp
+}
diff --git a/app/service/configser/catch.go b/app/service/configser/catch.go
new file mode 100644
index 0000000..3ea324a
--- /dev/null
+++ b/app/service/configser/catch.go
@@ -0,0 +1,454 @@
+package configser
+
+import (
+ "context"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/hotnavmod"
+ "videoweb/pub/models/ipblackmod"
+ "videoweb/pub/models/richtextcontentmod"
+ "videoweb/pub/models/videocreatermod"
+
+ "github.com/robfig/cron/v3"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ CacheMgr CacheInfo //
+ skdCron *cron.Cron
+)
+
+// richTextImgSrcRe 匹配富文本HTML里 img 标签的 src 图片地址,兼容单引号、双引号和JSON转义后的双引号
+var richTextImgSrcRe = regexp.MustCompile(`(?i)(
]*?\s*)src\s*=\s*(\\?"([^"\\]*)\\?"|'([^']*)')`)
+
+type CacheInfo struct {
+ Lock sync.RWMutex // 读写锁
+ Config proto.ConfigInfo // 系统配置缓存
+ Announcement proto.AnnouncementInfo
+ Advitise proto.AdvitiseData
+ IpBlackMaps map[string]struct{}
+ CreateInfo map[int64]*videocreatermod.VideoCreaterInfo // 作者信息
+ HotNavs []proto.HotNavItem // 热门导航
+ AppRichTexts []proto.AppRichTextItemResp // APP富文本内容列表
+}
+
+type appRichTextCacheItem struct {
+ Id int64 `bson:"_id,omitempty"`
+ Title string `bson:"title"`
+ Content string `bson:"content"`
+ Desc string `bson:"desc"`
+}
+
+func init() {
+ skdCron = cron.New(cron.WithSeconds()) // 秒级定时任务
+ CacheMgr.Config = proto.ConfigInfo{}
+ CacheMgr.CreateInfo = make(map[int64]*videocreatermod.VideoCreaterInfo)
+ CacheMgr.IpBlackMaps = make(map[string]struct{})
+}
+
+func Start() {
+ ctx := context.Background()
+
+ dataReport.StartSDKConfigCron()
+ UpdateConfigInfo(ctx)
+ //UpdateAdvitiseData(ctx)
+ UpdateAdvitiseDataFromJt()
+ UpdateAnnouncementData(ctx)
+ updateIpBlack()
+ updateHotNavCache(ctx)
+ updateAppRichTexts()
+ StartTopicCache()
+ go UpdateVideoRankCache()
+ go UpdatePlayletRankCache()
+ go UpdateAllFavoriteDataCount()
+ _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ UpdateConfigInfo(ctx)
+ //UpdateAdvitiseData(ctx)
+ UpdateAdvitiseDataFromJt()
+ UpdateAnnouncementData(ctx)
+ updateIpBlack()
+ updateHotNavCache(ctx)
+ updateAppRichTexts()
+ })
+
+ _, _ = skdCron.AddFunc("* */14 * * * ?", func() {
+ updateCreateCache(ctx)
+ })
+ // 每10分钟统计各标签关联数据量并刷新首页标签缓存
+ _, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
+ UpdateAllTagDataCount()
+ })
+ // 每10分钟统计各内容收藏量(整点 10 分)
+ _, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
+ UpdateAllFavoriteDataCount()
+ })
+ // 各内容榜单每 10 分钟刷新一次,错开整点避免任务扎堆
+ _, _ = skdCron.AddFunc("0 1/10 * * * ?", func() {
+ UpdateVideoRankCache()
+ })
+ _, _ = skdCron.AddFunc("0 5/10 * * * ?", func() {
+ UpdatePlayletRankCache()
+ })
+ skdCron.Start()
+}
+
+func UpdateConfigInfo(ctx context.Context) {
+ config := proto.ConfigInfo{}
+
+ filter := bson.M{cfg.FN.SysCfg.Id: 1}
+ sysCfg, _ := cfg.FindOneSysCfg(context.Background(), filter)
+ if sysCfg == nil {
+ return
+ }
+
+ config.NewUrlAddr = sysCfg.NewUrlAddr
+ config.NewUrlAddr1 = sysCfg.NewUrlAddr1
+ config.NewUrlAddr2 = sysCfg.NewUrlAddr2
+ config.ForeverUrlAddr = sysCfg.ForeverUrlAddr
+ config.GmailAddr = sysCfg.GmailAddr
+ config.AppAddr = sysCfg.AppAddr
+ config.WeiChart = sysCfg.WeiChart
+ config.QQ = sysCfg.QQ
+ config.TgGroup = sysCfg.TgGroup
+ config.PotaoGroup = sysCfg.PotaoGroup
+ config.GitAddr = sysCfg.GitAddr
+ config.TwitterAddr = sysCfg.TwitterAddr
+ config.Telegram = sysCfg.Telegram
+ config.TelegramAdd = sysCfg.TelegramAdd
+ config.TranslationAddr = sysCfg.TranslationAddr
+ config.ImgCdn = sysCfg.ImgAppCdn
+ config.TsCdn = sysCfg.MediaAppCdn
+ config.ImgAppUsCdn = sysCfg.ImgAppUsCdn
+ config.MediaAppUsCdn = sysCfg.MediaAppUsCdn
+ config.PdfAddr = sysCfg.PdfAddr
+
+ config.WebTitle = sysCfg.WebTitle
+ config.WebKeyworld = sysCfg.WebKeyworld
+ config.WebDescription = sysCfg.WebDescription
+ config.WebSiteName = sysCfg.WebSiteName
+ config.WebAddr = sysCfg.WebAddr
+ config.DHNewUrlAddr1 = sysCfg.DHNewUrlAddr1
+ config.DHNewUrlAddr2 = sysCfg.DHNewUrlAddr2
+ config.DHNewUrlAddr3 = sysCfg.DHNewUrlAddr3
+ config.UseIngAddress = sysCfg.UseAddress
+ config.FreeVPN = sysCfg.FreeVPN
+ config.DHKeyworld = sysCfg.DHKeyworld
+ config.DHTitle = sysCfg.DHTitle
+ config.DHDescription = sysCfg.DHDescription
+ config.DHSiteName = sysCfg.DHSiteName
+ config.ReportSDKConfig = dataReport.GetSDKDomainConfig()
+ config.ChannelCode = sysCfg.ChannelCode
+ config.EnableH265 = sysCfg.EnableH265
+ cfg.CacheAppKey(sysCfg.AppKey)
+ config.M3U8DayRateLimitMax = sysCfg.M3U8DayRateLimitMax
+
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.Config = config
+
+}
+
+func GetConfigInfo() proto.ConfigInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+
+ return CacheMgr.Config
+}
+
+func GetAdvitise() proto.AdvitiseData {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+
+ return CacheMgr.Advitise
+}
+
+func GetAnnouncement() proto.AnnouncementInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+
+ return CacheMgr.Announcement
+}
+
+func GetRandCDN() string {
+ config := GetConfigInfo()
+ return config.TsCdn
+}
+
+func updateHotNavCache(ctx context.Context) {
+ filter := bson.M{hotnavmod.FN.HotNav.Status: true}
+ opts := options.Find()
+ opts.SetSort(bson.D{
+ {Key: hotnavmod.FN.HotNav.Sort, Value: -1},
+ {Key: hotnavmod.FN.HotNav.CreatedAt, Value: -1},
+ })
+ opts.SetLimit(100)
+ list, err := hotnavmod.FindManyHotNav(ctx, filter, opts)
+ if err != nil {
+ log.Warn("updateHotNavCache.FindManyHotNav", log.E(err))
+ return
+ }
+ config := GetConfigInfo()
+ tempList := make([]proto.HotNavItem, len(list))
+ for i, item := range list {
+ tempList[i] = proto.HotNavItem{
+ Id: item.Id,
+ Title: item.Title,
+ Intro: item.Intro,
+ Url: item.Url,
+ Icon: netutil.MergeURL(config.ImgCdn, item.Icon),
+ }
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.HotNavs = tempList
+}
+
+func GetHotNavs() []proto.HotNavItem {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ result := make([]proto.HotNavItem, len(CacheMgr.HotNavs))
+ copy(result, CacheMgr.HotNavs)
+ return result
+}
+
+// GetFooterInfo 查询APP底部信息
+func GetFooterInfo() proto.FooterInfo {
+ config := GetConfigInfo()
+ return proto.FooterInfo{
+ Brand: buildFooterBrand(config),
+ NavList: buildFooterNavList(),
+ ContactList: buildFooterContactList(config),
+ }
+}
+
+// buildFooterBrand 构建APP底部品牌信息
+func buildFooterBrand(config proto.ConfigInfo) proto.FooterBrand {
+ return proto.FooterBrand{
+ Title: config.WebSiteName,
+ Content: config.WebDescription,
+ }
+}
+
+// buildFooterNavList 构建APP底部导航列表
+func buildFooterNavList() []proto.TranslationCategory {
+ bottom := translationser.GetTranslationBottomList()
+ return append([]proto.TranslationCategory{}, bottom.CategoryList...)
+}
+
+// buildFooterContactList 构建APP底部联系方式,固定返回原型要求的四项
+func buildFooterContactList(config proto.ConfigInfo) []proto.FooterContact {
+ return []proto.FooterContact{
+ {Name: "E-mail", Value: config.GmailAddr},
+ {Name: "商务合作", Value: config.Telegram},
+ {Name: "官方群", Value: firstNotEmpty(config.TgGroup, config.TelegramAdd)},
+ {Name: "官方推特", Value: config.TwitterAddr},
+ }
+}
+
+// firstNotEmpty 返回第一个非空字符串
+func firstNotEmpty(values ...string) string {
+ for _, value := range values {
+ value = strings.TrimSpace(value)
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+// GetAppRichTextList 查询APP富文本内容列表
+func GetAppRichTextList(rawIds string, rawTitles string) proto.AppRichTextListResp {
+ ids := parseInt64Set(rawIds)
+ titles := parseStringSet(rawTitles)
+ data := getCachedAppRichTexts()
+ if len(data) == 0 {
+ updateAppRichTexts()
+ data = getCachedAppRichTexts()
+ }
+ return proto.AppRichTextListResp{List: filterAppRichTexts(data, ids, titles)}
+}
+
+// getCachedAppRichTexts 读取APP富文本缓存副本
+func getCachedAppRichTexts() []proto.AppRichTextItemResp {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return append([]proto.AppRichTextItemResp{}, CacheMgr.AppRichTexts...)
+}
+
+// filterAppRichTexts 按富文本ID或标题过滤缓存数据
+func filterAppRichTexts(data []proto.AppRichTextItemResp, ids map[int64]struct{}, titles map[string]struct{}) []proto.AppRichTextItemResp {
+ list := make([]proto.AppRichTextItemResp, 0, len(data))
+ for _, item := range data {
+ if len(ids) > 0 {
+ if _, ok := ids[item.Id]; !ok {
+ continue
+ }
+ }
+ if len(titles) > 0 {
+ if _, ok := titles[item.Title]; !ok {
+ continue
+ }
+ }
+ list = append(list, item)
+ }
+ return list
+}
+
+// parseInt64Set 解析英文逗号分隔的ID集合
+func parseInt64Set(raw string) map[int64]struct{} {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil
+ }
+ resp := make(map[int64]struct{})
+ for _, item := range strings.Split(raw, ",") {
+ id, err := strconv.ParseInt(strings.TrimSpace(item), 10, 64)
+ if err != nil || id <= 0 {
+ continue
+ }
+ resp[id] = struct{}{}
+ }
+ return resp
+}
+
+// parseStringSet 解析英文逗号分隔的字符串集合
+func parseStringSet(raw string) map[string]struct{} {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil
+ }
+ resp := make(map[string]struct{})
+ for _, item := range strings.Split(raw, ",") {
+ item = strings.TrimSpace(item)
+ if item == "" {
+ continue
+ }
+ resp[item] = struct{}{}
+ }
+ return resp
+}
+
+// updateAppRichTexts 刷新APP富文本内容缓存
+func updateAppRichTexts() {
+ resp, err := queryAppRichTexts(context.Background())
+ if err != nil {
+ log.Warn("查询APP富文本内容失败", log.E(err))
+ return
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.AppRichTexts = resp
+}
+
+// queryAppRichTexts 查询启用状态的APP富文本内容,统一供定时缓存和接口兜底加载使用
+func queryAppRichTexts(ctx context.Context) ([]proto.AppRichTextItemResp, *srvdef.SrvErr) {
+ filter := bson.M{
+ richtextcontentmod.FN.RichTextContentModel.Status: true,
+ }
+ opts := options.Find().
+ SetProjection(bson.M{
+ richtextcontentmod.FN.RichTextContentModel.Id: 1,
+ richtextcontentmod.FN.RichTextContentModel.Title: 1,
+ richtextcontentmod.FN.RichTextContentModel.Content: 1,
+ richtextcontentmod.FN.RichTextContentModel.Desc: 1,
+ }).
+ SetSort(bson.D{
+ {Key: richtextcontentmod.FN.RichTextContentModel.Sort, Value: -1},
+ {Key: richtextcontentmod.FN.RichTextContentModel.UpdatedAt, Value: -1},
+ }).
+ SetLimit(100)
+ list := make([]appRichTextCacheItem, 0)
+ err := richtextcontentmod.FindManyRichTextContentModelModelWithBind(ctx, &list, filter, opts)
+ if err != nil {
+ log.Warn("查询APP富文本内容失败", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ imgCdn := GetConfigInfo().ImgCdn
+ resp := make([]proto.AppRichTextItemResp, 0, len(list))
+ for _, item := range list {
+ resp = append(resp, proto.AppRichTextItemResp{
+ Id: item.Id,
+ Title: item.Title,
+ Content: applyRichTextImgSrcCdn(imgCdn, item.Content),
+ Desc: item.Desc,
+ })
+ }
+ return resp, nil
+}
+
+// applyRichTextImgSrcCdn 给富文本 content 中 img 标签的 src 相对图片地址拼接图片CDN
+func applyRichTextImgSrcCdn(imgCdn string, content string) string {
+ if imgCdn == "" || content == "" {
+ return content
+ }
+ return richTextImgSrcRe.ReplaceAllStringFunc(content, func(match string) string {
+ parts := richTextImgSrcRe.FindStringSubmatch(match)
+ if len(parts) < 5 {
+ return match
+ }
+ prefix := parts[1]
+ quote := `"`
+ src := parts[3]
+ if src == "" {
+ quote = `'`
+ src = parts[4]
+ } else if strings.HasPrefix(parts[2], `\"`) {
+ quote = `\"`
+ }
+ src = strings.TrimSpace(src)
+ if src == "" || isAbsoluteRichTextSrc(src) {
+ return match
+ }
+ return prefix + "src=" + quote + netutil.MergeURL(imgCdn, src) + quote
+ })
+}
+
+// isAbsoluteRichTextSrc 判断图片地址是否已经是完整地址或特殊协议,避免重复拼接CDN
+func isAbsoluteRichTextSrc(src string) bool {
+ lowerSrc := strings.ToLower(src)
+ return strings.HasPrefix(lowerSrc, "http://") ||
+ strings.HasPrefix(lowerSrc, "https://") ||
+ strings.HasPrefix(lowerSrc, "data:") ||
+ strings.HasPrefix(lowerSrc, "blob:") ||
+ strings.HasPrefix(src, "//")
+}
+
+func updateIpBlack() {
+ ctx := context.Background()
+ list, err := ipblackmod.FindManyIPBlackModel(ctx, bson.M{})
+ if err != nil {
+ log.Error("updateIpBlack FindManyIPBlackModel error", log.E(err))
+ return
+ }
+ tempMaps := make(map[string]struct{})
+ for _, v := range list {
+ tempMaps[v.Ip] = struct{}{}
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.IpBlackMaps = tempMaps
+}
+
+func CheckIpBlack(ip string) bool {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ if _, ok := CacheMgr.IpBlackMaps[ip]; ok {
+ return true
+ }
+ return false
+}
+
+func Stop() {
+ dataReport.StopSDKConfigCron()
+ skdCron.Stop()
+}
diff --git a/app/service/configser/cdn.go b/app/service/configser/cdn.go
new file mode 100644
index 0000000..4cf3127
--- /dev/null
+++ b/app/service/configser/cdn.go
@@ -0,0 +1,85 @@
+package configser
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "strings"
+ "videoweb/app/api/proto"
+ "videoweb/app/appg"
+ "videoweb/pub/common/ipaddr"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ClientIPFromCtx 从 gin 请求上下文解析客户端 IP。
+func ClientIPFromCtx(ctx context.Context) string {
+ c, ok := ctx.(*gin.Context)
+ if !ok || c == nil {
+ return ""
+ }
+ return ipaddr.ResolveClientIP(c)
+}
+
+// IsOverseasIP 与 m3u8 TS 域名判断一致:按 v4/v6 库识别是否海外。
+func IsOverseasIP(ip string) bool {
+ ip = strings.TrimSpace(ip)
+ if ip == "" {
+ return false
+ }
+ detector := appg.DetectorV6
+ if ipaddr.IsIPv4(ip) {
+ detector = appg.DetectorV4
+ }
+ if detector == nil {
+ return false
+ }
+ ok, _ := detector.IsOverseasIP(ip)
+ return ok
+}
+
+// GetAppImageCdn 海外 IP 且配置了 imgAppUsCdn 时用海外图床,否则用 imgAppCdn。
+func GetAppImageCdn(ip string) string {
+ cfg := GetConfigInfo()
+ if IsOverseasIP(ip) {
+ if us := strings.TrimSpace(cfg.ImgAppUsCdn); us != "" {
+ return us
+ }
+ }
+ return strings.TrimSpace(cfg.ImgCdn)
+}
+
+// ImageCdnFromCtx 按当前请求 IP 选择图片 CDN。
+func ImageCdnFromCtx(ctx context.Context) string {
+ return GetAppImageCdn(ClientIPFromCtx(ctx))
+}
+
+// GetRequestConfig 返回当前请求应使用的配置副本,海外 IP 且海外图床非空时覆盖 ImgCdn。
+func GetRequestConfig(ctx context.Context) proto.ConfigInfo {
+ cfg := GetConfigInfo()
+ if cdn := GetAppImageCdn(ClientIPFromCtx(ctx)); cdn != "" {
+ cfg.ImgCdn = cdn
+ }
+ return cfg
+}
+
+// RewriteImageCdn 将已拼好的国内图床域名替换为当前请求应使用的图床,用于缓存数据。
+func RewriteImageCdn(ctx context.Context, data any) {
+ if data == nil {
+ return
+ }
+ from := strings.TrimRight(strings.TrimSpace(GetConfigInfo().ImgCdn), "/")
+ to := strings.TrimRight(GetAppImageCdn(ClientIPFromCtx(ctx)), "/")
+ if from == "" || to == "" || from == to {
+ return
+ }
+ raw, err := json.Marshal(data)
+ if err != nil {
+ return
+ }
+ replaced := bytes.ReplaceAll(raw, []byte(from), []byte(to))
+ if bytes.Equal(raw, replaced) {
+ return
+ }
+ _ = json.Unmarshal(replaced, data)
+}
diff --git a/app/service/configser/content_tag.go b/app/service/configser/content_tag.go
new file mode 100644
index 0000000..059e450
--- /dev/null
+++ b/app/service/configser/content_tag.go
@@ -0,0 +1,116 @@
+package configser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/contenttagmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// AppContentTagList 查询APP端有绑定内容数量的启用统一标签列表
+func AppContentTagList(ctx context.Context, req *proto.AppContentTagListReq) (*proto.AppContentTagListResp, *srvdef.SrvErr) {
+ if req == nil {
+ req = &proto.AppContentTagListReq{}
+ }
+ filter := bson.M{
+ contenttagmod.FN.ContentTag.Status: true,
+ }
+ bindCountExpr := appContentTagTotalCountExpr()
+ if req.ContentTagType != nil {
+ if !isValidAppContentTagType(*req.ContentTagType) {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ countField := appContentTagCountField(*req.ContentTagType)
+ filter[contenttagmod.FN.ContentTag.Types] = *req.ContentTagType
+ filter[countField] = bson.M{"$gt": 0}
+ bindCountExpr = bson.M{"$ifNull": bson.A{"$" + countField, 0}}
+ } else {
+ filter["$or"] = []bson.M{
+ {contenttagmod.FN.ContentTag.VideoCount: bson.M{"$gt": 0}},
+ {contenttagmod.FN.ContentTag.PlayletCount: bson.M{"$gt": 0}},
+ }
+ }
+ list := make([]*proto.AppContentTagItem, 0)
+ pipeline := []bson.M{
+ {"$match": filter},
+ {"$addFields": bson.M{"bindCount": bindCountExpr}},
+ {"$sort": bson.D{
+ {Key: "bindCount", Value: -1},
+ {Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
+ {Key: contenttagmod.FN.ContentTag.Id, Value: -1},
+ }},
+ {"$project": bson.M{
+ contenttagmod.FN.ContentTag.Id: 1,
+ contenttagmod.FN.ContentTag.Name: 1,
+ contenttagmod.FN.ContentTag.Slug: 1,
+ contenttagmod.FN.ContentTag.Types: 1,
+ contenttagmod.FN.ContentTag.Rank: 1,
+ contenttagmod.FN.ContentTag.VideoCount: 1,
+ contenttagmod.FN.ContentTag.PlayletCount: 1,
+ "seoInfo": 1,
+ "bindCount": 1,
+ }},
+ }
+ if err := contenttagmod.AggregateContentTag(ctx, &list, pipeline); err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ if item != nil && item.SeoInfo.Keyworld == nil {
+ item.SeoInfo.Keyworld = make([]string, 0)
+ }
+ }
+ return &proto.AppContentTagListResp{List: list}, nil
+}
+
+// HasEnabledContentTagBind 判断统一标签是否启用、支持当前内容类型且已有绑定内容
+func HasEnabledContentTagBind(ctx context.Context, tagId int64, tagType contenttagmod.ContentTagType) (bool, *srvdef.SrvErr) {
+ if tagId <= 0 {
+ return false, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
+ }
+ if !isValidAppContentTagType(tagType) {
+ return false, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ countField := appContentTagCountField(tagType)
+ _, err := contenttagmod.FindOneContentTag(ctx, bson.M{
+ contenttagmod.FN.ContentTag.Id: tagId,
+ contenttagmod.FN.ContentTag.Status: true,
+ contenttagmod.FN.ContentTag.Types: tagType,
+ countField: bson.M{"$gt": 0},
+ }, options.FindOne().SetProjection(bson.M{contenttagmod.FN.ContentTag.Id: 1}))
+ if err != nil {
+ if err.Code == srvdef.ErrNoData {
+ return false, nil
+ }
+ return false, err
+ }
+ return true, nil
+}
+
+// appContentTagTotalCountExpr 计算统一标签视频和短剧绑定数量合计,只用于查询排序不落库
+func appContentTagTotalCountExpr() bson.M {
+ return bson.M{"$add": bson.A{
+ bson.M{"$ifNull": bson.A{"$" + contenttagmod.FN.ContentTag.VideoCount, 0}},
+ bson.M{"$ifNull": bson.A{"$" + contenttagmod.FN.ContentTag.PlayletCount, 0}},
+ }}
+}
+
+// isValidAppContentTagType 判断APP端统一标签类型是否有效
+func isValidAppContentTagType(tagType contenttagmod.ContentTagType) bool {
+ return tagType == contenttagmod.ContentTagTypeVideo ||
+ tagType == contenttagmod.ContentTagTypePlaylet
+}
+
+// appContentTagCountField 返回当前内容类型对应的绑定数量字段
+func appContentTagCountField(tagType contenttagmod.ContentTagType) string {
+ switch tagType {
+ case contenttagmod.ContentTagTypeVideo:
+ return contenttagmod.FN.ContentTag.VideoCount
+ case contenttagmod.ContentTagTypePlaylet:
+ return contenttagmod.FN.ContentTag.PlayletCount
+ default:
+ return ""
+ }
+}
diff --git a/app/service/configser/creater.go b/app/service/configser/creater.go
new file mode 100644
index 0000000..e7d7ae2
--- /dev/null
+++ b/app/service/configser/creater.go
@@ -0,0 +1,54 @@
+package configser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/models/videocreatermod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func updateCreateCache(ctx context.Context) {
+ // 更新作者相关缓存
+ createList, err := videocreatermod.FindManyVideoCreaterInfos(ctx, bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Status: true,
+ }, options.Find().SetLimit(200).SetSort(bson.M{videocreatermod.FN.VideoCreaterInfo.Sort: -1}))
+ if err != nil {
+ log.Warn("updateCreateCache FindManyVideoCreaterIinfos", log.E(err))
+ return
+ }
+ tempCreateMaps := make(map[int64]*videocreatermod.VideoCreaterInfo)
+ for _, createInfo := range createList {
+ tempCreateMaps[createInfo.Id] = createInfo
+ }
+ CacheMgr.Lock.Lock()
+ CacheMgr.CreateInfo = tempCreateMaps
+ CacheMgr.Lock.Unlock()
+}
+
+func GetCreateInfo(createId int64, imageCdn string) proto.PostCreateInfo {
+ if createId <= 0 {
+ return proto.PostCreateInfo{}
+ }
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ v, ok := CacheMgr.CreateInfo[createId]
+ if !ok {
+ return proto.PostCreateInfo{}
+ }
+ return proto.PostCreateInfo{
+ Id: v.Id,
+ Name: v.Name,
+ Slug: v.Slug,
+ Image: netutil.MergeURL(imageCdn, v.Image),
+ Works: v.Works,
+ CreateType: v.CreateType,
+ Desc: v.Desc,
+ Background: v.Background,
+ CreatedAt: v.CreatedAt,
+ SeoInfo: v.SeoInfo,
+ }
+}
diff --git a/app/service/configser/h265.go b/app/service/configser/h265.go
new file mode 100644
index 0000000..755b34e
--- /dev/null
+++ b/app/service/configser/h265.go
@@ -0,0 +1,35 @@
+package configser
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/enum/redisconst"
+)
+
+// ApplyVideoH265Policy 根据 sys_cfg.enableH265 决定是否向客户端暴露 H265 地址。
+func ApplyVideoH265Policy(data *proto.VideoInfoData) {
+ if data == nil || GetConfigInfo().EnableH265 {
+ return
+ }
+ proto.StripH265Url(data)
+}
+
+// ApplyVideoH265PolicyList 批量处理视频列表。
+func ApplyVideoH265PolicyList(list []proto.VideoInfoData) []proto.VideoInfoData {
+ if GetConfigInfo().EnableH265 {
+ return list
+ }
+ for i := range list {
+ proto.StripH265Url(&list[i])
+ }
+ return list
+}
+
+// GetM3U8DayRateLimitMax 返回每 IP 每天最多可播放的不同正片数。
+// sys_cfg.m3u8DayRateLimitMax <= 0 时使用默认 80。
+func GetM3U8DayRateLimitMax() int64 {
+ max := GetConfigInfo().M3U8DayRateLimitMax
+ if max <= 0 {
+ return redisconst.M3U8DayRateLimitMax
+ }
+ return max
+}
diff --git a/app/service/configser/playletrankcache.go b/app/service/configser/playletrankcache.go
new file mode 100644
index 0000000..a6e0155
--- /dev/null
+++ b/app/service/configser/playletrankcache.go
@@ -0,0 +1,280 @@
+package configser
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ PlayletRankMgr periodRankCache[proto.PlayletRankData]
+ playletRankRefreshLock sync.Mutex
+ playletRankCacheVersion atomic.Int64
+)
+
+func UpdatePlayletRankCache() {
+ version := playletcache.RankCacheVersion()
+ ctx := context.Background()
+ imgCdn := GetConfigInfo().ImgCdn
+ PlayletRankMgr.replace(
+ buildPlayletRankList(ctx, imgCdn, RankPeriodTotal),
+ buildPlayletRankList(ctx, imgCdn, RankPeriodWeek),
+ buildPlayletRankList(ctx, imgCdn, RankPeriodMonth),
+ buildPlayletRankList(ctx, imgCdn, RankPeriodYear),
+ )
+ playletRankCacheVersion.Store(version)
+}
+
+func refreshPlayletRankCacheIfEmpty() {
+ if PlayletRankMgr.hasAny() {
+ return
+ }
+ playletRankRefreshLock.Lock()
+ defer playletRankRefreshLock.Unlock()
+ if PlayletRankMgr.hasAny() {
+ return
+ }
+ UpdatePlayletRankCache()
+}
+
+func GetPlayletRankList(p RankPeriod, limit int) []proto.PlayletRankData {
+ version := playletcache.RankCacheVersion()
+ if version != playletRankCacheVersion.Load() {
+ playletRankRefreshLock.Lock()
+ if version != playletRankCacheVersion.Load() {
+ UpdatePlayletRankCache()
+ }
+ playletRankRefreshLock.Unlock()
+ }
+ return PlayletRankMgr.get(p, limit, refreshPlayletRankCacheIfEmpty)
+}
+
+func playletPeriodWatchField(p RankPeriod) string {
+ switch p {
+ case RankPeriodWeek:
+ return playletmod.FN.Playlet.WeekWatchs
+ case RankPeriodMonth:
+ return playletmod.FN.Playlet.MonthWatchs
+ case RankPeriodYear:
+ return playletmod.FN.Playlet.YearWatchs
+ default:
+ return ""
+ }
+}
+
+func buildPlayletRankList(ctx context.Context, imgCdn string, p RankPeriod) []proto.PlayletRankData {
+ var ranked playletmod.PlayletList
+ if p == RankPeriodTotal {
+ if err := playletmod.AggregatePlaylet(ctx, &ranked, contentLikeRankPipeline(
+ playletmod.FN.Playlet.Status, playletmod.StatusUp,
+ playletmod.FN.Playlet.CountLike, playletmod.FN.Playlet.VirtualCountLike,
+ playletmod.FN.Playlet.UpdatedAt, playletmod.FN.Playlet.Id,
+ rankLimit,
+ )); err != nil {
+ log.Warn("buildPlayletRankList like rank query failed", log.E(err))
+ ranked = playletmod.PlayletList{}
+ }
+ } else {
+ watchField := playletPeriodWatchField(p)
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ watchField: bson.M{"$gt": 0},
+ }, contentWatchRankOpts(watchField, playletmod.FN.Playlet.UpdatedAt, playletmod.FN.Playlet.Id, rankLimit))
+ if err != nil {
+ log.Warn("buildPlayletRankList watch rank query failed", log.Any("period", p), log.E(err))
+ ranked = playletmod.PlayletList{}
+ } else {
+ ranked = list
+ }
+ }
+ if int64(len(ranked)) < rankLimit {
+ needFill := rankLimit - int64(len(ranked))
+ excludeIds := make([]int64, 0, len(ranked))
+ for _, v := range ranked {
+ if v != nil {
+ excludeIds = append(excludeIds, v.Id)
+ }
+ }
+ var filled playletmod.PlayletList
+ if err := playletmod.AggregatePlaylet(ctx, &filled, contentRankFillPipeline(
+ playletmod.FN.Playlet.Id, playletmod.FN.Playlet.Status,
+ playletmod.StatusUp, excludeIds, needFill,
+ )); err != nil {
+ log.Warn("buildPlayletRankList fill sample failed", log.Any("period", p), log.E(err))
+ } else {
+ ranked = append(ranked, filled...)
+ }
+ }
+ tagNameMap, themeNameMap := loadPlayletRankNameMaps(ctx, ranked)
+ result := make([]proto.PlayletRankData, 0, len(ranked))
+ for _, item := range ranked {
+ if item == nil {
+ continue
+ }
+ result = append(result, playletToRankData(item, imgCdn, tagNameMap, themeNameMap, p))
+ }
+ return result
+}
+
+func playletToRankData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, p RankPeriod) proto.PlayletRankData {
+ return proto.PlayletRankData{
+ PlayletData: proto.PlayletData{
+ Id: item.Id,
+ Status: item.Status,
+ IsRecommend: item.IsRecommend,
+ IsNew: !item.OnlineAt.IsZero() && item.OnlineAt.After(utils.BeginOfWeekByTime(utils.Now())),
+ IsFeatured: item.IsFeatured,
+ IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
+ ShowOriginal: item.ShowOriginal,
+ OnlineAt: item.OnlineAt,
+ MediaCreatedAt: item.MediaCreatedAt,
+ MediaUpdatedAt: item.MediaUpdatedAt,
+ Title: item.Title,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: netutil.MergeURL(imgCdn, item.DisplayCover()),
+ Preview: netutil.MergeURL(imgCdn, strings.TrimSpace(item.Preview)),
+ TagIds: item.TagIds,
+ TagNames: playletRankTagNamesByIDs(item.TagIds, tagNameMap),
+ TagSlugs: playletRankTagSlugsByIDs(item.TagIds, tagNameMap),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: playletNamesByIDs(item.ThemeIds, themeNameMap),
+ ContentType: item.ContentType,
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.DisplayCountBrowse(),
+ HeatValue: item.DisplayHeatValue(),
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ VirtualCountLike: item.VirtualCountLike,
+ DisplayCountLike: item.CountLike + item.VirtualCountLike,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: playletRankSeo(item.SeoInfo),
+ },
+ RankScore: periodWatchNum(p, item.WeekWatchs, item.MonthWatchs, item.YearWatchs, item.CountLike+item.VirtualCountLike),
+ }
+}
+
+func playletRankSeo(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 != "" {
+ words = append(words, word)
+ }
+ }
+ seo.Keyworld = words
+ return seo
+}
+
+func playletNamesByIDs(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
+}
+
+func loadPlayletRankNameMaps(ctx context.Context, list playletmod.PlayletList) (contenttagmod.ContentTagMap, map[int64]string) {
+ tagIDs := make([]int64, 0)
+ themeIDs := make([]int64, 0)
+ seenTag := make(map[int64]struct{})
+ seenTheme := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, id := range item.TagIds {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seenTag[id]; ok {
+ continue
+ }
+ seenTag[id] = struct{}{}
+ tagIDs = append(tagIDs, id)
+ }
+ for _, id := range item.ThemeIds {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seenTheme[id]; ok {
+ continue
+ }
+ seenTheme[id] = struct{}{}
+ themeIDs = append(themeIDs, id)
+ }
+ }
+ tagNameMap := make(contenttagmod.ContentTagMap, len(tagIDs))
+ if len(tagIDs) > 0 {
+ if tags, err := contenttagmod.GetMapTags(ctx, tagIDs); err != nil {
+ log.Warn("loadPlayletRankNameMaps tags failed", log.E(err))
+ } else {
+ tagNameMap = tags
+ }
+ }
+ themeNameMap := make(map[int64]string, len(themeIDs))
+ if len(themeIDs) > 0 {
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIDs},
+ }, options.Find().SetProjection(bson.M{
+ playletthememod.FN.PlayletTheme.Id: 1,
+ playletthememod.FN.PlayletTheme.Name: 1,
+ }))
+ if err != nil {
+ log.Warn("loadPlayletRankNameMaps themes failed", log.E(err))
+ } else {
+ for _, theme := range themes {
+ if theme != nil {
+ themeNameMap[theme.Id] = theme.Name
+ }
+ }
+ }
+ }
+ return tagNameMap, themeNameMap
+}
+
+func playletRankTagNamesByIDs(ids []int64, tags contenttagmod.ContentTagMap) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if tag := tags[id]; tag != nil && tag.Name != "" {
+ result = append(result, tag.Name)
+ }
+ }
+ return result
+}
+
+func playletRankTagSlugsByIDs(ids []int64, tags contenttagmod.ContentTagMap) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if tag := tags[id]; tag != nil && tag.Name != "" {
+ result = append(result, tag.Slug)
+ }
+ }
+ return result
+}
diff --git a/app/service/configser/rankcache.go b/app/service/configser/rankcache.go
new file mode 100644
index 0000000..3d996d4
--- /dev/null
+++ b/app/service/configser/rankcache.go
@@ -0,0 +1,144 @@
+package configser
+
+import (
+ "sync"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const rankLimit = 48
+
+// NormalizeRankPage 榜单分页:页码从 1 起,pageSize 不是 rankLimit 时强制改为 rankLimit。
+func NormalizeRankPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize != int64(rankLimit) {
+ pageSize = int64(rankLimit)
+ }
+ return pageNum, pageSize
+}
+
+type periodRankCache[T any] struct {
+ Lock sync.RWMutex
+ Total []T
+ Week []T
+ Month []T
+ Year []T
+}
+
+func (c *periodRankCache[T]) hasAny() bool {
+ c.Lock.RLock()
+ defer c.Lock.RUnlock()
+ return len(c.Total) > 0 || len(c.Week) > 0 || len(c.Month) > 0 || len(c.Year) > 0
+}
+
+func (c *periodRankCache[T]) pick(p RankPeriod) []T {
+ switch p {
+ case RankPeriodWeek:
+ return c.Week
+ case RankPeriodMonth:
+ return c.Month
+ case RankPeriodYear:
+ return c.Year
+ default:
+ return c.Total
+ }
+}
+
+func (c *periodRankCache[T]) replace(total, week, month, year []T) {
+ c.Lock.Lock()
+ c.Total, c.Week, c.Month, c.Year = total, week, month, year
+ c.Lock.Unlock()
+}
+
+func (c *periodRankCache[T]) get(p RankPeriod, limit int, refreshIfEmpty func()) []T {
+ c.Lock.RLock()
+ list := c.pick(p)
+ empty := len(list) == 0
+ c.Lock.RUnlock()
+ if empty {
+ refreshIfEmpty()
+ c.Lock.RLock()
+ list = c.pick(p)
+ c.Lock.RUnlock()
+ }
+ return sliceLimit(list, limit)
+}
+
+func sliceLimit[T any](s []T, limit int) []T {
+ if limit <= 0 || limit >= len(s) {
+ return s
+ }
+ return s[:limit]
+}
+
+// SliceRankPage 对榜单缓存做分页切片
+func SliceRankPage[T any](all []T, pageNum, pageSize int64) []T {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ return []T{}
+ }
+ start := (pageNum - 1) * pageSize
+ if start >= int64(len(all)) {
+ return []T{}
+ }
+ end := start + pageSize
+ if end > int64(len(all)) {
+ end = int64(len(all))
+ }
+ return append([]T{}, all[start:end]...)
+}
+
+func contentLikeRankPipeline(statusField string, statusVal interface{}, realLikes, fakeLikes, updatedAt, idField string, limit int64) []bson.M {
+ return []bson.M{
+ {"$match": bson.M{statusField: statusVal}},
+ {"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + realLikes, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + fakeLikes, 0}},
+ }}}},
+ {"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
+ {"$sort": bson.D{
+ {Key: "rankScore", Value: -1},
+ {Key: updatedAt, Value: -1},
+ {Key: idField, Value: -1},
+ }},
+ {"$limit": limit},
+ }
+}
+
+func contentWatchRankOpts(watchField, updatedAt, idField string, limit int64) *options.FindOptions {
+ return options.Find().
+ SetSort(bson.D{
+ {Key: watchField, Value: -1},
+ {Key: updatedAt, Value: -1},
+ {Key: idField, Value: -1},
+ }).
+ SetLimit(limit)
+}
+
+func contentRankFillPipeline(idField, statusField string, statusVal interface{}, excludeIds []int64, needFill int64) []bson.M {
+ return []bson.M{
+ {"$match": bson.M{
+ idField: bson.M{"$nin": excludeIds},
+ statusField: statusVal,
+ }},
+ {"$sample": bson.M{"size": needFill}},
+ }
+}
+
+func periodWatchNum(p RankPeriod, week, month, year, total int64) int64 {
+ switch p {
+ case RankPeriodWeek:
+ return week
+ case RankPeriodMonth:
+ return month
+ case RankPeriodYear:
+ return year
+ default:
+ return total
+ }
+}
diff --git a/app/service/configser/rankstat.go b/app/service/configser/rankstat.go
new file mode 100644
index 0000000..29e33d8
--- /dev/null
+++ b/app/service/configser/rankstat.go
@@ -0,0 +1,91 @@
+package configser
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/videofavoritemod"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+// favoriteCountResult 收藏聚合结果
+type favoriteCountResult struct {
+ Id int64 `bson:"_id"`
+ Count int64 `bson:"count"`
+}
+
+// UpdateAllFavoriteDataCount 统计视频收藏量并写回DB
+func UpdateAllFavoriteDataCount() {
+ updateVideoFavoriteDataCount(context.Background())
+}
+
+// buildFavoritePipeline 构建按时间窗口统计收藏量的聚合 pipeline
+// contentIdField: favorite表中内容ID字段名(如 videoId)
+// since: 统计起始时间,零值表示统计全量
+func buildFavoritePipeline(contentIdField string, since time.Time) []bson.M {
+ matchStage := bson.M{}
+ if !since.IsZero() {
+ matchStage["createdAt"] = bson.M{"$gte": since}
+ }
+ pipeline := []bson.M{}
+ if len(matchStage) > 0 {
+ pipeline = append(pipeline, bson.M{"$match": matchStage})
+ }
+ pipeline = append(pipeline,
+ bson.M{"$group": bson.M{
+ "_id": "$" + contentIdField,
+ "count": bson.M{"$sum": 1},
+ }},
+ )
+ return pipeline
+}
+
+// updateVideoFavoriteDataCount 统计视频收藏量,更新 Likes/WeekLikes/MonthLikes/YearLikes
+func updateVideoFavoriteDataCount(ctx context.Context) {
+ now := time.Now()
+ type period struct {
+ field string
+ since time.Time
+ }
+ periods := []period{
+ {videoinformationmod.FN.VideoInformation.Real.Likes, time.Time{}},
+ {videoinformationmod.FN.VideoInformation.Real.WeekLikes, now.AddDate(0, 0, -7)},
+ {videoinformationmod.FN.VideoInformation.Real.MonthLikes, now.AddDate(0, 0, -30)},
+ {videoinformationmod.FN.VideoInformation.Real.YearLikes, now.AddDate(0, 0, -365)},
+ }
+ idField := videofavoritemod.FN.VideoFavorite.VideoID
+ for _, p := range periods {
+ var results []favoriteCountResult
+ pipeline := buildFavoritePipeline(idField, p.since)
+ if serr := videofavoritemod.AggregateVideoFavorite(ctx, &results, pipeline); serr != nil {
+ log.Warn("updateVideoFavoriteDataCount aggregate failed", log.Any("field", p.field))
+ continue
+ }
+ if len(results) == 0 {
+ continue
+ }
+ writes := make([]mongo.WriteModel, 0, len(results))
+ for _, r := range results {
+ writes = append(writes, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: r.Id}).
+ SetUpdate(bson.M{"$set": bson.M{p.field: r.Count}}),
+ )
+ }
+ if _, serr := videoinformationmod.BulkWriteVideoInformation(ctx, writes); serr != nil {
+ log.Warn("updateVideoFavoriteDataCount bulk update failed", log.Any("field", p.field))
+ }
+ }
+}
+
+// RankPeriod 排行榜时间维度
+type RankPeriod int
+
+const (
+ RankPeriodTotal RankPeriod = 0 // 总榜
+ RankPeriodWeek RankPeriod = 1 // 周榜
+ RankPeriodMonth RankPeriod = 2 // 月榜
+ RankPeriodYear RankPeriod = 3 // 年榜
+)
diff --git a/app/service/configser/sysconfig.go b/app/service/configser/sysconfig.go
new file mode 100644
index 0000000..208abee
--- /dev/null
+++ b/app/service/configser/sysconfig.go
@@ -0,0 +1,17 @@
+package configser
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/cfg"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// GetSystemConfig 读取系统配置(MongoDB id=1)。
+func GetSystemConfig(ctx context.Context) (*cfg.SysCfg, *srvdef.SrvErr) {
+ filter := bson.M{
+ cfg.FN.SysCfg.Id: 1,
+ }
+ return cfg.FindOneSysCfg(ctx, filter)
+}
diff --git a/app/service/configser/tagstat.go b/app/service/configser/tagstat.go
new file mode 100644
index 0000000..c4dd965
--- /dev/null
+++ b/app/service/configser/tagstat.go
@@ -0,0 +1,102 @@
+package configser
+
+import (
+ "context"
+ "sync"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+// tagCountResult 聚合统计结果
+type tagCountResult struct {
+ Id int64 `bson:"_id"`
+ Count int64 `bson:"count"`
+}
+
+// UpdateAllTagDataCount 并发统计视频和短剧各标签的关联数据量并写回统一标签表
+func UpdateAllTagDataCount() {
+ ctx := context.Background()
+ wg := sync.WaitGroup{}
+ wg.Add(2)
+ go func() { defer wg.Done(); updateVideoTagDataCount(ctx) }()
+ go func() { defer wg.Done(); updatePlayletTagDataCount(ctx) }()
+ wg.Wait()
+ updateTagCache()
+}
+
+// updateVideoTagDataCount 统计视频标签的关联视频数量,更新 content_tag.videoCount 字段
+func updateVideoTagDataCount(ctx context.Context) {
+ pipeline := []bson.M{
+ {"$match": bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }},
+ {"$unwind": "$" + videoinformationmod.FN.VideoInformation.Tags},
+ {"$group": bson.M{
+ "_id": "$" + videoinformationmod.FN.VideoInformation.Tags,
+ "count": bson.M{"$sum": 1},
+ }},
+ }
+ var results []tagCountResult
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &results, pipeline); err != nil {
+ log.Warn("updateVideoTagDataCount aggregate failed", log.E(err))
+ return
+ }
+ if len(results) == 0 {
+ results = make([]tagCountResult, 0)
+ }
+ syncContentTagDataCount(ctx, contenttagmod.ContentTagTypeVideo, contenttagmod.FN.ContentTag.VideoCount, results, "updateVideoTagDataCount")
+}
+
+// updatePlayletTagDataCount 统计短剧标签的关联短剧数量,更新 content_tag.playletCount 字段
+func updatePlayletTagDataCount(ctx context.Context) {
+ pipeline := []bson.M{
+ {"$match": bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }},
+ {"$unwind": "$" + playletmod.FN.Playlet.TagIds},
+ {"$group": bson.M{
+ "_id": "$" + playletmod.FN.Playlet.TagIds,
+ "count": bson.M{"$sum": 1},
+ }},
+ }
+ var results []tagCountResult
+ if err := playletmod.AggregatePlaylet(ctx, &results, pipeline); err != nil {
+ log.Warn("updatePlayletTagDataCount aggregate failed", log.E(err))
+ return
+ }
+ if len(results) == 0 {
+ results = make([]tagCountResult, 0)
+ }
+ syncContentTagDataCount(ctx, contenttagmod.ContentTagTypePlaylet, contenttagmod.FN.ContentTag.PlayletCount, results, "updatePlayletTagDataCount")
+}
+
+func syncContentTagDataCount(ctx context.Context, tagType contenttagmod.ContentTagType, countField string, results []tagCountResult, logName string) {
+ if err := contenttagmod.UpdateManyContentTag(ctx, bson.M{
+ contenttagmod.FN.ContentTag.Types: tagType,
+ }, bson.M{"$set": bson.M{countField: 0}}); err != nil {
+ log.Warn(logName+" reset failed", log.E(err))
+ return
+ }
+ if len(results) == 0 {
+ return
+ }
+ writes := make([]mongo.WriteModel, 0, len(results))
+ for _, r := range results {
+ writes = append(writes, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{
+ contenttagmod.FN.ContentTag.Id: r.Id,
+ contenttagmod.FN.ContentTag.Types: tagType,
+ }).
+ SetUpdate(bson.M{"$set": bson.M{countField: r.Count}}),
+ )
+ }
+ if _, err := contenttagmod.BulkWriteContentTag(ctx, writes); err != nil {
+ log.Warn(logName+" bulk update failed", log.E(err))
+ }
+}
diff --git a/app/service/configser/topic.go b/app/service/configser/topic.go
new file mode 100644
index 0000000..86fb112
--- /dev/null
+++ b/app/service/configser/topic.go
@@ -0,0 +1,233 @@
+package configser
+
+import (
+ "context"
+ "sort"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/contenttagmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type TopicCache struct {
+ Lock sync.RWMutex // 读写锁
+
+ ActressWorks []proto.ActressInfoData
+ ActressModelMaps map[int64]*actress.Actress
+
+ AllTagMaps map[int64]proto.TagsInfoData
+ VideoTagMaps map[int64]proto.TagsInfoData
+
+ // 首页展示的标签,按当前类型绑定数量降序
+ HomeVideoTagList []proto.TagsInfoData
+ HomePlayletTagList []proto.TagsInfoData
+}
+
+var TopicMgr TopicCache
+
+func StartTopicCache() {
+ TopicMgr = TopicCache{}
+ TopicMgr.Lock = sync.RWMutex{}
+ TopicMgr.ActressWorks = make([]proto.ActressInfoData, 0)
+ TopicMgr.ActressModelMaps = make(map[int64]*actress.Actress)
+ TopicMgr.HomeVideoTagList = make([]proto.TagsInfoData, 0)
+ TopicMgr.HomePlayletTagList = make([]proto.TagsInfoData, 0)
+ TopicMgr.AllTagMaps = make(map[int64]proto.TagsInfoData)
+ TopicMgr.VideoTagMaps = make(map[int64]proto.TagsInfoData)
+
+ updateTagCache()
+ updateCreateCache(context.Background())
+ updateCategoryCache()
+ _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ updateTagCache()
+ updateCreateCache(context.Background())
+ updateCategoryCache()
+ })
+}
+func updateTagCache() {
+ updateContentTagCache()
+}
+func updateCategoryCache() {
+ updateActressCache()
+}
+
+func updateActressCache() {
+ ctx := context.Background()
+ filter := bson.M{actress.FN.Actress.Status: true}
+ config := GetConfigInfo()
+ otps := &options.FindOptions{}
+ otps.SetSort(bson.D{
+ {Key: actress.FN.Actress.HotValue, Value: -1},
+ {Key: actress.FN.Actress.Id, Value: -1},
+ })
+ list, err := actress.FindManyActresss(ctx, filter, otps)
+ if err != nil {
+ log.Warn("updateActressCache", log.E(err))
+ return
+ }
+ tempWorks := make([]proto.ActressInfoData, len(list))
+ tempModel := make(map[int64]*actress.Actress)
+ for i := 0; i < len(list); i++ {
+ info := proto.ActressInfoToData(*list[i], config.ImgCdn)
+ tempWorks[i] = info
+ tempModel[info.Id] = list[i]
+ }
+ sort.SliceStable(tempWorks, func(i, j int) bool {
+ if tempWorks[i].HotValue == tempWorks[j].HotValue {
+ return tempWorks[i].Id > tempWorks[j].Id
+ }
+ return tempWorks[i].HotValue > tempWorks[j].HotValue
+ })
+ TopicMgr.Lock.Lock()
+ defer TopicMgr.Lock.Unlock()
+ TopicMgr.ActressWorks = tempWorks
+ TopicMgr.ActressModelMaps = tempModel
+}
+
+func GetActressInfo(actressId int64) *actress.Actress {
+ TopicMgr.Lock.RLock()
+ defer TopicMgr.Lock.RUnlock()
+ v, ok := TopicMgr.ActressModelMaps[actressId]
+ if !ok {
+ return nil
+ }
+ return v
+}
+func updateContentTagCache() {
+ ctx := context.Background()
+ filter := bson.M{
+ contenttagmod.FN.ContentTag.Status: true,
+ "$or": []bson.M{
+ {contenttagmod.FN.ContentTag.VideoCount: bson.M{"$gt": 0}},
+ {contenttagmod.FN.ContentTag.PlayletCount: bson.M{"$gt": 0}},
+ },
+ }
+ list, err := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
+ {Key: contenttagmod.FN.ContentTag.Id, Value: -1},
+ }).
+ SetProjection(bson.M{
+ contenttagmod.FN.ContentTag.Id: 1,
+ contenttagmod.FN.ContentTag.Name: 1,
+ contenttagmod.FN.ContentTag.Slug: 1,
+ contenttagmod.FN.ContentTag.Types: 1,
+ contenttagmod.FN.ContentTag.Rank: 1,
+ contenttagmod.FN.ContentTag.VideoCount: 1,
+ contenttagmod.FN.ContentTag.PlayletCount: 1,
+ "seoInfo": 1,
+ }))
+ if err != nil {
+ log.Warn("updateContentTagCache", log.E(err))
+ return
+ }
+ allMaps := make(map[int64]proto.TagsInfoData, len(list))
+ videoMaps := make(map[int64]proto.TagsInfoData)
+ videoHomeList := make([]proto.TagsInfoData, 0)
+ playletHomeList := make([]proto.TagsInfoData, 0)
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, tagType := range item.Types {
+ info := contentTagInfoByType(item, tagType)
+ allMaps[item.Id] = info
+ switch tagType {
+ case contenttagmod.ContentTagTypeVideo:
+ videoMaps[item.Id] = info
+ if info.Works > 0 {
+ videoHomeList = append(videoHomeList, info)
+ }
+ case contenttagmod.ContentTagTypePlaylet:
+ if info.Works > 0 {
+ playletHomeList = append(playletHomeList, info)
+ }
+ }
+ }
+ }
+ sortHomeContentTagList(videoHomeList)
+ sortHomeContentTagList(playletHomeList)
+ TopicMgr.Lock.Lock()
+ defer TopicMgr.Lock.Unlock()
+ TopicMgr.AllTagMaps = allMaps
+ TopicMgr.VideoTagMaps = videoMaps
+ TopicMgr.HomeVideoTagList = videoHomeList
+ TopicMgr.HomePlayletTagList = playletHomeList
+}
+
+func contentTagInfoByType(item *contenttagmod.ContentTag, tagType contenttagmod.ContentTagType) proto.TagsInfoData {
+ return proto.TagsInfoData{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ SeoInfo: item.SeoInfo,
+ Works: contentTagWorksByType(item, tagType),
+ }
+}
+
+func contentTagWorksByType(item *contenttagmod.ContentTag, tagType contenttagmod.ContentTagType) int64 {
+ switch tagType {
+ case contenttagmod.ContentTagTypeVideo:
+ return item.VideoCount
+ case contenttagmod.ContentTagTypePlaylet:
+ return item.PlayletCount
+ default:
+ return 0
+ }
+}
+
+func sortHomeContentTagList(list []proto.TagsInfoData) {
+ sort.Slice(list, func(i, j int) bool {
+ if list[i].Works == list[j].Works {
+ return list[i].Id > list[j].Id
+ }
+ return list[i].Works > list[j].Works
+ })
+}
+
+func GetActressWorks() []proto.ActressInfoData {
+ TopicMgr.Lock.RLock()
+ defer TopicMgr.Lock.RUnlock()
+ return TopicMgr.ActressWorks
+}
+
+func GetVideoTag(tagId int64) *proto.TagsInfoData {
+ TopicMgr.Lock.RLock()
+ defer TopicMgr.Lock.RUnlock()
+ tag, ok := TopicMgr.VideoTagMaps[tagId]
+ if !ok {
+ return nil
+ }
+ return &tag
+}
+
+// VideoTagNamesAndSlugs 按视频标签ID顺序返回名称和slug
+func VideoTagNamesAndSlugs(tagIDs []int64) ([]string, []string) {
+ names := make([]string, 0, len(tagIDs))
+ slugs := make([]string, 0, len(tagIDs))
+ TopicMgr.Lock.RLock()
+ defer TopicMgr.Lock.RUnlock()
+ for _, id := range tagIDs {
+ if tag, ok := TopicMgr.VideoTagMaps[id]; ok {
+ names = append(names, tag.Name)
+ slugs = append(slugs, tag.Slug)
+ }
+ }
+ return names, slugs
+}
+
+func GetHomeVideoTagList() []proto.TagsInfoData {
+ TopicMgr.Lock.RLock()
+ defer TopicMgr.Lock.RUnlock()
+ return TopicMgr.HomeVideoTagList
+}
+
+func GetHomePlayletTagList() []proto.TagsInfoData {
+ TopicMgr.Lock.RLock()
+ defer TopicMgr.Lock.RUnlock()
+ return TopicMgr.HomePlayletTagList
+}
diff --git a/app/service/configser/videorankcache.go b/app/service/configser/videorankcache.go
new file mode 100644
index 0000000..13ce7ac
--- /dev/null
+++ b/app/service/configser/videorankcache.go
@@ -0,0 +1,128 @@
+package configser
+
+import (
+ "context"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+var (
+ VideoRankMgr periodRankCache[proto.ContentData]
+ videoRankRefreshLock sync.Mutex
+)
+
+func UpdateVideoRankCache() {
+ ctx := context.Background()
+ imgCdn := GetConfigInfo().ImgCdn
+ VideoRankMgr.replace(
+ buildVideoRankList(ctx, imgCdn, RankPeriodTotal),
+ buildVideoRankList(ctx, imgCdn, RankPeriodWeek),
+ buildVideoRankList(ctx, imgCdn, RankPeriodMonth),
+ buildVideoRankList(ctx, imgCdn, RankPeriodYear),
+ )
+}
+
+func refreshVideoRankCacheIfEmpty() {
+ if VideoRankMgr.hasAny() {
+ return
+ }
+ videoRankRefreshLock.Lock()
+ defer videoRankRefreshLock.Unlock()
+ if VideoRankMgr.hasAny() {
+ return
+ }
+ UpdateVideoRankCache()
+}
+
+func GetVideoRankList(p RankPeriod, limit int) []proto.ContentData {
+ return VideoRankMgr.get(p, limit, refreshVideoRankCacheIfEmpty)
+}
+
+func videoPeriodWatchField(p RankPeriod) string {
+ switch p {
+ case RankPeriodWeek:
+ return videoinformationmod.FN.VideoInformation.Real.WeekWatchs
+ case RankPeriodMonth:
+ return videoinformationmod.FN.VideoInformation.Real.MonthWatchs
+ case RankPeriodYear:
+ return videoinformationmod.FN.VideoInformation.Real.YearWatchs
+ default:
+ return ""
+ }
+}
+
+func videoToContentData(item *videoinformationmod.VideoInformation, imgCdn string) proto.ContentData {
+ tagNames, tagSlugs := VideoTagNamesAndSlugs(item.Tags)
+ return proto.ContentData{
+ Id: item.Id,
+ Title: item.Title,
+ Slug: item.Slug,
+ AddedTime: item.AddedTime,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Tags: item.Tags,
+ TagsName: tagNames,
+ TagSlugs: tagSlugs,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ }
+}
+
+func videoToRankContentData(item *videoinformationmod.VideoInformation, imgCdn string, p RankPeriod) proto.ContentData {
+ data := videoToContentData(item, imgCdn)
+ data.WatchNum = periodWatchNum(p, item.Real.WeekWatchs, item.Real.MonthWatchs, item.Real.YearWatchs, data.WatchNum)
+ return data
+}
+
+func buildVideoRankList(ctx context.Context, imgCdn string, p RankPeriod) []proto.ContentData {
+ var ranked videoinformationmod.VideoInformationList
+ if p == RankPeriodTotal {
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &ranked, contentLikeRankPipeline(
+ videoinformationmod.FN.VideoInformation.Status, pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.Real.Likes, videoinformationmod.FN.VideoInformation.Fake.Likes,
+ videoinformationmod.FN.VideoInformation.UpdatedAt, videoinformationmod.FN.VideoInformation.Id,
+ rankLimit,
+ )); err != nil {
+ log.Warn("buildVideoRankList like rank query failed", log.E(err))
+ ranked = videoinformationmod.VideoInformationList{}
+ }
+ } else {
+ watchField := videoPeriodWatchField(p)
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ watchField: bson.M{"$gt": 0},
+ }, contentWatchRankOpts(watchField, videoinformationmod.FN.VideoInformation.UpdatedAt, videoinformationmod.FN.VideoInformation.Id, rankLimit))
+ if err != nil {
+ log.Warn("buildVideoRankList watch rank query failed", log.Any("period", p), log.E(err))
+ ranked = videoinformationmod.VideoInformationList{}
+ } else {
+ ranked = list
+ }
+ }
+ if int64(len(ranked)) < rankLimit {
+ needFill := rankLimit - int64(len(ranked))
+ excludeIds := make([]int64, 0, len(ranked))
+ for _, v := range ranked {
+ excludeIds = append(excludeIds, v.Id)
+ }
+ var filled videoinformationmod.VideoInformationList
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &filled, contentRankFillPipeline(
+ videoinformationmod.FN.VideoInformation.Id, videoinformationmod.FN.VideoInformation.Status,
+ pubdef.InformationStatusVerifyOK, excludeIds, needFill,
+ )); err != nil {
+ log.Warn("buildVideoRankList fill sample failed", log.Any("period", p), log.E(err))
+ } else {
+ ranked = append(ranked, filled...)
+ }
+ }
+ result := make([]proto.ContentData, 0, len(ranked))
+ for _, item := range ranked {
+ result = append(result, videoToRankContentData(item, imgCdn, p))
+ }
+ return result
+}
diff --git a/app/service/displaycount/displaycount.go b/app/service/displaycount/displaycount.go
new file mode 100644
index 0000000..b55a95a
--- /dev/null
+++ b/app/service/displaycount/displaycount.go
@@ -0,0 +1,226 @@
+package displaycount
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "hash/fnv"
+ "math"
+ "strings"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/videofavoritemod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videolikemod"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type countRow struct {
+ Id int64 `bson:"_id"`
+ Count int64 `bson:"count"`
+}
+
+type videoCounts struct {
+ Watch int64
+ Like int64
+ Favorite int64
+}
+
+// Value 真实数大于零时放大十倍,零值按内容和指标生成稳定展示数
+func Value(real, id int64, kind string) int64 {
+ if real > 0 {
+ if real > math.MaxInt64/10 {
+ return math.MaxInt64
+ }
+ return real * 10
+ }
+ h := fnv.New64a()
+ _, _ = h.Write([]byte(kind))
+ var key [8]byte
+ binary.LittleEndian.PutUint64(key[:], uint64(id))
+ _, _ = h.Write(key[:])
+ return 100 + int64(h.Sum64()%901)
+}
+
+// Transform 将 App 内容接口中的现有计数字段转换为展示值
+func Transform(c *gin.Context, data interface{}) interface{} {
+ path := c.Request.URL.Path
+ if !contentPath(path) || data == nil {
+ return data
+ }
+ raw, err := json.Marshal(data)
+ if err != nil {
+ return data
+ }
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.UseNumber()
+ var body interface{}
+ if err := decoder.Decode(&body); err != nil {
+ return data
+ }
+ ids := make(map[int64]struct{})
+ collectVideoIDs(body, path, ids)
+ counts, err := loadVideoCounts(c, ids)
+ if err != nil {
+ log.Warn("displaycount: load video counts failed", log.E(err))
+ return data
+ }
+ apply(body, path, c.Query("rankPeriod"), counts)
+ return body
+}
+
+func contentPath(path string) bool {
+ return strings.HasPrefix(path, "/api/app/playlet/") ||
+ strings.HasPrefix(path, "/api/app/video/") ||
+ strings.HasPrefix(path, "/api/app/home") ||
+ strings.HasPrefix(path, "/api/app/actress/work/") ||
+ strings.HasPrefix(path, "/search")
+}
+
+func collectVideoIDs(node interface{}, path string, ids map[int64]struct{}) {
+ switch item := node.(type) {
+ case map[string]interface{}:
+ if isVideo(item, path) {
+ if id := number(item["id"]); id > 0 {
+ ids[id] = struct{}{}
+ }
+ }
+ for _, child := range item {
+ collectVideoIDs(child, path, ids)
+ }
+ case []interface{}:
+ for _, child := range item {
+ collectVideoIDs(child, path, ids)
+ }
+ }
+}
+
+func isVideo(item map[string]interface{}, path string) bool {
+ if item["type"] != nil && number(item["type"]) == 2 {
+ return false
+ }
+ if _, ok := item["watchNum"]; ok {
+ _, hasLike := item["likeNum"]
+ return hasLike
+ }
+ if _, ok := item["watchTimes"]; ok {
+ _, hasVideo := item["video"]
+ return hasVideo && number(item["type"]) != 2
+ }
+ return path == "/api/app/video/like" && item["likeNum"] != nil && item["id"] != nil
+}
+
+func loadVideoCounts(c *gin.Context, idSet map[int64]struct{}) (map[int64]videoCounts, error) {
+ result := make(map[int64]videoCounts, len(idSet))
+ if len(idSet) == 0 {
+ return result, nil
+ }
+ ids := make([]int64, 0, len(idSet))
+ for id := range idSet {
+ ids = append(ids, id)
+ }
+ videos, err := videoinformationmod.FindManyVideoInformations(c, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, video := range videos {
+ result[video.Id] = videoCounts{Watch: video.Real.WatchTimes}
+ }
+ likes := make([]countRow, 0)
+ if err := videolikemod.AggregateVideoLike(c, &likes, countPipeline(videolikemod.FN.VideoLike.VideoID, ids)); err != nil {
+ return nil, err
+ }
+ for _, row := range likes {
+ value := result[row.Id]
+ value.Like = row.Count
+ result[row.Id] = value
+ }
+ favorites := make([]countRow, 0)
+ if err := videofavoritemod.AggregateVideoFavorite(c, &favorites, countPipeline(videofavoritemod.FN.VideoFavorite.VideoID, ids)); err != nil {
+ return nil, err
+ }
+ for _, row := range favorites {
+ value := result[row.Id]
+ value.Favorite = row.Count
+ result[row.Id] = value
+ }
+ return result, nil
+}
+
+func countPipeline(field string, ids []int64) []bson.M {
+ return []bson.M{
+ {"$match": bson.M{field: bson.M{"$in": ids}}},
+ {"$group": bson.M{"_id": "$" + field, "count": bson.M{"$sum": 1}}},
+ }
+}
+
+func apply(node interface{}, path, rankPeriod string, counts map[int64]videoCounts) {
+ switch item := node.(type) {
+ case map[string]interface{}:
+ id := number(item["id"])
+ if id > 0 && isVideo(item, path) {
+ count := counts[id]
+ if _, ok := item["watchNum"]; ok {
+ item["watchNum"] = Value(count.Watch, id, "video-watch")
+ }
+ if _, ok := item["watchTimes"]; ok {
+ item["watchTimes"] = Value(count.Watch, id, "video-watch")
+ }
+ item["likeNum"] = Value(count.Like, id, "video-like")
+ item["countCollect"] = Value(count.Favorite, id, "video-favorite")
+ } else if id > 0 && item["countBrowse"] != nil && item["countCollect"] != nil {
+ kind := "playlet"
+ if item["currentEpisode"] != nil {
+ kind = "playlet-chapter"
+ }
+ if item["rankScore"] != nil && path == "/api/app/playlet/rank/list" {
+ if rankPeriod == "" || rankPeriod == "0" {
+ item["rankScore"] = Value(number(item["countLike"]), id, kind+"-like")
+ } else {
+ item["rankScore"] = Value(number(item["rankScore"]), id, kind+"-period-watch-"+rankPeriod)
+ }
+ }
+ item["countBrowse"] = Value(number(item["countBrowse"]), id, kind+"-watch")
+ item["countCollect"] = Value(number(item["countCollect"]), id, kind+"-favorite")
+ if item["displayCountLike"] != nil {
+ like := Value(number(item["countLike"]), id, kind+"-like")
+ item["displayCountLike"] = like
+ item["countLike"] = like
+ } else if item["countLike"] != nil {
+ item["countLike"] = Value(number(item["countLike"]), id, kind+"-like")
+ }
+ } else if id > 0 && path == "/api/app/playlet/favorite" && item["countCollect"] != nil {
+ item["countCollect"] = Value(number(item["countCollect"]), id, "playlet-favorite")
+ } else if id > 0 && path == "/api/app/playlet/like" && item["likeNum"] != nil {
+ item["likeNum"] = Value(number(item["likeNum"]), id, "playlet-like")
+ }
+ for _, child := range item {
+ apply(child, path, rankPeriod, counts)
+ }
+ case []interface{}:
+ for _, child := range item {
+ apply(child, path, rankPeriod, counts)
+ }
+ }
+}
+
+func number(v interface{}) int64 {
+ switch n := v.(type) {
+ case json.Number:
+ value, _ := n.Int64()
+ return value
+ case int64:
+ return n
+ case float64:
+ return int64(n)
+ default:
+ return 0
+ }
+}
diff --git a/app/service/healthser/health.go b/app/service/healthser/health.go
new file mode 100644
index 0000000..495df99
--- /dev/null
+++ b/app/service/healthser/health.go
@@ -0,0 +1,217 @@
+package healthser
+
+import (
+ "context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "math"
+ "net/http"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+
+ "videoweb/app/api/proto"
+ "videoweb/app/appg"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+ "videoweb/pub/redis"
+
+ "github.com/shirou/gopsutil/v3/cpu"
+ "github.com/shirou/gopsutil/v3/disk"
+ "github.com/shirou/gopsutil/v3/mem"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+const (
+ timestampTolerance = 5 * time.Minute
+ bytesInGB = 1024 * 1024 * 1024
+ healthGroupID = "69840d508591cdac5a3defcb"
+ healthSecretKey = "5e47c292e775ebe8bb0d79057cd17778"
+)
+
+func HealthCheck(ctx context.Context, req *proto.HealthCheckRequest) (*proto.HealthCheckData, *srvdef.SrvErr) {
+ if req.GroupID != healthGroupID {
+ log.Warn("health check invalid groupId", log.Any("groupId", req.GroupID))
+ return nil, srvdef.New(srvdef.ErrParamError, "invalid groupId")
+ }
+ if !isTimestampValid(req.Timestamp) {
+ log.Warn("health check timestamp expired", log.Any("timestamp", req.Timestamp))
+ return nil, srvdef.New(srvdef.ErrParamError, "timestamp expired")
+ }
+ if !verifySign(req.GroupID, req.Timestamp, healthSecretKey, req.Sign) {
+ log.Warn("health check invalid sign", log.Any("groupId", req.GroupID))
+ return nil, srvdef.New(srvdef.ErrParamError, "invalid sign")
+ }
+
+ data := collectServerMetrics()
+ data.Timestamp = time.Now().Unix()
+ data.Mysql = disabledStatus("mysql not used")
+ data.Kafka = disabledStatus("kafka not used")
+ data.Redis = pingRedis()
+ data.Mongodb = pingMongo(ctx)
+ data.Elasticsearch = pingElasticsearch(ctx)
+ return data, nil
+}
+
+func verifySign(groupID string, ts int64, secret string, sign string) bool {
+ payload := fmt.Sprintf("%s:%d", groupID, ts)
+ mac := hmac.New(sha256.New, []byte(secret))
+ mac.Write([]byte(payload))
+ expected := hex.EncodeToString(mac.Sum(nil))
+ return hmac.Equal([]byte(strings.ToLower(sign)), []byte(expected))
+}
+
+func isTimestampValid(ts int64) bool {
+ now := time.Now().Unix()
+ diff := now - ts
+ if diff < 0 {
+ diff = -diff
+ }
+ return diff <= int64(timestampTolerance.Seconds())
+}
+
+func disabledStatus(msg string) *proto.ComponentStatus {
+ return &proto.ComponentStatus{Status: "disabled", Msg: msg}
+}
+
+func okStatus() *proto.ComponentStatus {
+ return &proto.ComponentStatus{Status: "ok", Msg: "success"}
+}
+
+func pingRedis() *proto.ComponentStatus {
+ cli := redis.Client()
+ if cli == nil {
+ return &proto.ComponentStatus{Status: "err", Msg: "redis client not initialized"}
+ }
+ if err := cli.Ping().Err(); err != nil {
+ log.Warn("health check redis ping failed", log.E(err))
+ return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
+ }
+ status := okStatus()
+ status.Master = appg.Conf.Redis.Addr
+ return status
+}
+
+func pingMongo(ctx context.Context) *proto.ComponentStatus {
+ if mongos.Shop == nil {
+ return &proto.ComponentStatus{Status: "err", Msg: "mongo client not initialized"}
+ }
+ pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ if err := mongos.Shop.Database().RunCommand(pingCtx, bson.D{{Key: "ping", Value: 1}}).Err(); err != nil {
+ log.Warn("health check mongo ping failed", log.E(err))
+ return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
+ }
+ return okStatus()
+}
+
+func pingElasticsearch(ctx context.Context) *proto.ComponentStatus {
+ addr := strings.TrimSpace(appg.Conf.Es.Addr)
+ if addr == "" {
+ return disabledStatus("elasticsearch not configured")
+ }
+ url := strings.TrimSuffix(addr, "/") + "/_cluster/health"
+ reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
+ if err != nil {
+ log.Warn("health check es request build failed", log.E(err))
+ return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
+ }
+ if appg.Conf.Es.User != "" || appg.Conf.Es.Pass != "" {
+ req.SetBasicAuth(appg.Conf.Es.User, appg.Conf.Es.Pass)
+ }
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ log.Warn("health check es request failed", log.E(err))
+ return &proto.ComponentStatus{Status: "err", Msg: err.Error()}
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return &proto.ComponentStatus{Status: "err", Msg: fmt.Sprintf("es status %d", resp.StatusCode)}
+ }
+ return okStatus()
+}
+
+func collectServerMetrics() *proto.HealthCheckData {
+ name, err := os.Hostname()
+ if err != nil || name == "" {
+ name = "app-server"
+ }
+ ip := netutil.GetLocalIP()
+ if ip == "" {
+ ip = "127.0.0.1"
+ }
+
+ cores := runtime.NumCPU()
+ usedCores := calcUsedCores(cores)
+ memMetrics := memoryMetrics()
+ diskMetrics := diskMetrics()
+
+ return &proto.HealthCheckData{
+ ServerName: name,
+ ServerIP: ip,
+ CPU: proto.HealthCPUMetrics{
+ Cores: cores,
+ Used: usedCores,
+ },
+ Memory: memMetrics,
+ Disk: diskMetrics,
+ }
+}
+
+func calcUsedCores(cores int) float64 {
+ usage, err := cpu.Percent(200*time.Millisecond, false)
+ if err != nil || len(usage) == 0 {
+ if err != nil {
+ log.Warn("health check cpu percent failed", log.E(err))
+ }
+ return 0
+ }
+ used := float64(cores) * usage[0] / 100
+ return math.Round(used*10) / 10
+}
+
+func memoryMetrics() proto.HealthMemoryMetrics {
+ vm, err := mem.VirtualMemory()
+ if err != nil {
+ log.Warn("health check memory stats failed", log.E(err))
+ return proto.HealthMemoryMetrics{}
+ }
+ return proto.HealthMemoryMetrics{
+ Total: bytesToGB(vm.Total),
+ Used: bytesToGB(vm.Used),
+ }
+}
+
+func diskMetrics() proto.HealthMemoryMetrics {
+ path := rootPath()
+ du, err := disk.Usage(path)
+ if err != nil {
+ log.Warn("health check disk stats failed", log.E(err))
+ return proto.HealthMemoryMetrics{}
+ }
+ return proto.HealthMemoryMetrics{
+ Total: bytesToGB(du.Total),
+ Used: bytesToGB(du.Used),
+ }
+}
+
+func bytesToGB(value uint64) int64 {
+ if value == 0 {
+ return 0
+ }
+ return int64(math.Round(float64(value) / bytesInGB))
+}
+
+func rootPath() string {
+ if runtime.GOOS == "windows" {
+ return "C:\\"
+ }
+ return "/"
+}
diff --git a/app/service/homeser/cache.go b/app/service/homeser/cache.go
new file mode 100644
index 0000000..201dac7
--- /dev/null
+++ b/app/service/homeser/cache.go
@@ -0,0 +1,44 @@
+package homeser
+
+import (
+ "encoding/json"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/redis"
+)
+
+const (
+ appHomeCacheKey = "app:home:data"
+ appHomeCacheTTL = time.Minute * 5
+)
+
+// getHomeDataCache 读取 APP 首页 Redis 缓存,缓存异常时降级查库
+func getHomeDataCache() *proto.DataResp {
+ cacheData, err1 := redis.Get(appHomeCacheKey)
+ if err1 != nil {
+ log.Warn("读取APP首页缓存失败", log.Any("cacheKey", appHomeCacheKey), log.E(err1))
+ return nil
+ }
+ if cacheData == nil || *cacheData == "" {
+ return nil
+ }
+ resp := &proto.DataResp{}
+ if err1 := json.Unmarshal([]byte(*cacheData), resp); err1 != nil {
+ log.Warn("解析APP首页缓存失败", log.Any("cacheKey", appHomeCacheKey), log.E(err1))
+ return nil
+ }
+ return resp
+}
+
+// setHomeDataCache 写入 APP 首页 Redis 缓存,缓存异常不影响主流程
+func setHomeDataCache(resp proto.DataResp) {
+ bytes, err1 := json.Marshal(resp)
+ if err1 != nil {
+ log.Warn("序列化APP首页缓存失败", log.Any("cacheKey", appHomeCacheKey), log.E(err1))
+ return
+ }
+ if err1 := redis.Set(appHomeCacheKey, string(bytes), appHomeCacheTTL); err1 != nil {
+ log.Warn("写入APP首页缓存失败", log.Any("cacheKey", appHomeCacheKey), log.E(err1))
+ }
+}
diff --git a/app/service/homeser/home.go b/app/service/homeser/home.go
new file mode 100644
index 0000000..4ca8fc4
--- /dev/null
+++ b/app/service/homeser/home.go
@@ -0,0 +1,191 @@
+package homeser
+
+import (
+ "context"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/actressser"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/playletser"
+ "videoweb/app/service/translationser"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/homebannermod"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const homeBannerListLimit int64 = 30
+
+var homeBannerAppProjection = 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},
+}
+
+// HomeData 获取首页数据,包含公共配置、公告、广告和各业务首页数据
+func HomeData(ctx context.Context) (proto.DataResp, *srvdef.SrvErr) {
+ if cacheData := getHomeDataCache(); cacheData != nil {
+ configser.RewriteImageCdn(ctx, cacheData)
+ return *cacheData, nil
+ }
+ // 数据组装
+ config := configser.GetConfigInfo()
+ data := proto.DataResp{
+ Path: homeRequestPath(ctx),
+ Config: config,
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ HomeBannerList: make([]proto.HomeBannerInfo, 0),
+ PlayletData: proto.HomePlayletDataResp{
+ RecommendList: make([]proto.PlayletData, 0),
+ NewestList: make([]proto.PlayletData, 0),
+ HotList: make([]proto.PlayletData, 0),
+ },
+ VideoData: proto.HomeVideoDataResp{
+ RecommendList: make([]proto.VideoInfo, 0),
+ NewestList: make([]proto.VideoInfo, 0),
+ HotList: make([]proto.VideoInfo, 0),
+ },
+ ActressInfoList: make([]proto.ActressInfo, 0),
+ }
+ // 并分查询
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var err1 *srvdef.SrvErr
+ setErr := func(err *srvdef.SrvErr) {
+ if err == nil {
+ return
+ }
+ mu.Lock()
+ defer mu.Unlock()
+ if err1 == nil {
+ err1 = err
+ }
+ }
+ wg.Add(4)
+ go func() {
+ defer wg.Done()
+ list, err := HomeBannerList(ctx, config.ImgCdn)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ data.HomeBannerList = list
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ playletData, err := playletser.HomePlayletData(ctx, config.ImgCdn)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ data.PlayletData = playletData
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ videoData, err := videoser.HomeVideoDataForHome(ctx, config.ImgCdn)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ data.VideoData = videoData
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ list, err := actressser.HomeActressData(ctx)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ data.ActressInfoList = list
+ mu.Unlock()
+ }()
+ wg.Wait()
+ if err1 != nil {
+ return data, err1
+ }
+ setHomeDataCache(data)
+ configser.RewriteImageCdn(ctx, &data)
+ return data, nil
+}
+
+// HomeBannerList 查询启用的首页banner信息列表
+func HomeBannerList(ctx context.Context, imgCdn string) ([]proto.HomeBannerInfo, *srvdef.SrvErr) {
+ list, err := homebannermod.FindManyHomeBanner(ctx, bson.M{
+ homebannermod.FN.HomeBanner.Status: true,
+ }, options.Find().
+ SetProjection(homeBannerAppProjection).
+ SetSort(bson.D{
+ {Key: homebannermod.FN.HomeBanner.Rank, Value: -1},
+ {Key: homebannermod.FN.HomeBanner.CreatedAt, Value: -1},
+ {Key: homebannermod.FN.HomeBanner.Id, Value: -1},
+ }).
+ SetLimit(homeBannerListLimit))
+ if err != nil {
+ return nil, err
+ }
+ result := make([]proto.HomeBannerInfo, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, proto.HomeBannerInfo{
+ Id: item.Id,
+ Name: item.Name,
+ Image: netutil.MergeURL(imgCdn, item.Image),
+ VerticalImage: netutil.MergeURL(imgCdn, item.VerticalImage),
+ Url: item.Url,
+ Rank: item.Rank,
+ Status: item.Status,
+ })
+ }
+ return result, nil
+}
+
+// HomeBaseData 获取首页公共配置数据
+func HomeBaseData(ctx context.Context) proto.HomeBaseResp {
+ resp := proto.HomeBaseResp{
+ Translations: translationser.GetTranslationData(),
+ Config: configser.GetRequestConfig(ctx),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ }
+ configser.RewriteImageCdn(ctx, &resp)
+ return resp
+}
+
+// HomeTagsData 获取首页标签数据
+func HomeTagsData(ctx context.Context) proto.HomeTagsResp {
+ resp := proto.HomeTagsResp{
+ HomeBaseResp: HomeBaseData(ctx),
+ VideoTagsList: configser.GetHomeVideoTagList(),
+ PlayletTagsList: configser.GetHomePlayletTagList(),
+ }
+ configser.RewriteImageCdn(ctx, &resp)
+ return resp
+}
+
+// homeRequestPath 从 gin 上下文中提取当前请求路径,避免 API 层重复组装首页响应字段
+func homeRequestPath(ctx context.Context) string {
+ ginCtx, ok := ctx.(*gin.Context)
+ if !ok || ginCtx.Request == nil || ginCtx.Request.URL == nil {
+ return ""
+ }
+ return ginCtx.Request.URL.Path
+}
diff --git a/app/service/moreser/mixed.go b/app/service/moreser/mixed.go
new file mode 100644
index 0000000..170f270
--- /dev/null
+++ b/app/service/moreser/mixed.go
@@ -0,0 +1,137 @@
+package moreser
+
+// NormalizePage 规范化分页参数,限制单次请求数量避免大分页拖垮 MongoDB。
+func NormalizePage(pageNum, pageSize, defaultPageSize, maxPageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultPageSize
+ }
+ if pageSize > maxPageSize {
+ pageSize = maxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// InsertedAdsCount 计算实际可穿插的广告数量,每 interval 条内容后插入 1 条广告。
+func InsertedAdsCount(contentTotal, adsTotal, interval int64) int64 {
+ if contentTotal <= 0 || adsTotal <= 0 || interval <= 0 {
+ return 0
+ }
+ insertSlots := contentTotal / interval
+ if insertSlots > adsTotal {
+ return adsTotal
+ }
+ return insertSlots
+}
+
+// ContentPosition 计算内容原始下标在混排列表中的全局位置。
+func ContentPosition(contentIndex, adsTotal, interval int64) int64 {
+ if interval <= 0 {
+ return contentIndex
+ }
+ adsBefore := contentIndex / interval
+ if adsBefore > adsTotal {
+ adsBefore = adsTotal
+ }
+ return contentIndex + adsBefore
+}
+
+// FirstContentIndexAtOrAfter 找到指定混排位置之后第一条内容下标。
+func FirstContentIndexAtOrAfter(pos, contentTotal, adsTotal, interval int64) int64 {
+ left, right := int64(0), contentTotal
+ for left < right {
+ mid := left + (right-left)/2
+ if ContentPosition(mid, adsTotal, interval) < pos {
+ left = mid + 1
+ continue
+ }
+ right = mid
+ }
+ return left
+}
+
+// ContentRange 计算当前混排分页需要读取的内容下标范围。
+func ContentRange(start, end, contentTotal, adsTotal, interval int64) (int64, int64) {
+ contentStart := FirstContentIndexAtOrAfter(start, contentTotal, adsTotal, interval)
+ contentEnd := FirstContentIndexAtOrAfter(end, contentTotal, adsTotal, interval)
+ if contentStart >= contentEnd {
+ return 0, 0
+ }
+ return contentStart, contentEnd - contentStart
+}
+
+// ContentRangeWithoutTotal 计算当前混排分页需要读取的内容范围,不依赖内容总数。
+// 适用于列表接口不返回 totalCount 的场景;广告位仍按“每 interval 条内容后插入 1 条广告”计算。
+func ContentRangeWithoutTotal(start, end, adsTotal, interval int64) (int64, int64) {
+ if start < 0 || end <= start {
+ return 0, 0
+ }
+ contentStart := contentIndexAtMixedPosition(start, adsTotal, interval)
+ contentEnd := contentIndexAtMixedPosition(end, adsTotal, interval)
+ if contentStart >= contentEnd {
+ return 0, 0
+ }
+ return contentStart, contentEnd - contentStart
+}
+
+// ContentIndexAtPositionWithoutTotal 计算混排位置对应的内容下标,不判断该位置是否为广告。
+func ContentIndexAtPositionWithoutTotal(pos, adsTotal, interval int64) int64 {
+ return contentIndexAtMixedPosition(pos, adsTotal, interval)
+}
+
+// AdIndexAtPositionWithoutTotal 计算混排位置对应的广告下标,返回 -1 表示该位置不是广告。
+func AdIndexAtPositionWithoutTotal(pos, adsTotal, interval int64) int64 {
+ if adsTotal <= 0 || interval <= 0 || pos < 0 {
+ return -1
+ }
+ cycle := interval + 1
+ if pos >= adsTotal*cycle {
+ return -1
+ }
+ if pos%cycle != interval {
+ return -1
+ }
+ return pos / cycle
+}
+
+// contentIndexAtMixedPosition 返回混排位置之前已经出现的内容数量。
+func contentIndexAtMixedPosition(pos, adsTotal, interval int64) int64 {
+ if pos <= 0 {
+ return 0
+ }
+ if adsTotal <= 0 || interval <= 0 {
+ return pos
+ }
+ adCycle := interval + 1
+ adAreaSize := adsTotal * adCycle
+ if pos >= adAreaSize {
+ return pos - adsTotal
+ }
+ fullCycles := pos / adCycle
+ remain := pos % adCycle
+ return fullCycles*interval + minInt64(remain, interval)
+}
+
+func minInt64(a, b int64) int64 {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+// AdIndexAtPosition 计算混排位置对应的广告下标,返回 -1 表示该位置不是广告。
+func AdIndexAtPosition(pos, contentTotal, adsTotal, interval int64) int64 {
+ if interval <= 0 {
+ return -1
+ }
+ nextContent := FirstContentIndexAtOrAfter(pos+1, contentTotal, adsTotal, interval)
+ prevContent := nextContent - 1
+ if prevContent >= 0 && (prevContent+1)%interval == 0 && (prevContent+1)/interval-1 < adsTotal {
+ if ContentPosition(prevContent, adsTotal, interval)+1 == pos {
+ return (prevContent+1)/interval - 1
+ }
+ }
+ return -1
+}
diff --git a/app/service/navigationser/header.go b/app/service/navigationser/header.go
new file mode 100644
index 0000000..038bc33
--- /dev/null
+++ b/app/service/navigationser/header.go
@@ -0,0 +1,134 @@
+package navigationser
+
+import (
+ "context"
+ "strings"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ headerNavigationCategoryLimit int64 = 30
+ headerNavigationInfoLimit int64 = 50
+)
+
+var (
+ headerNavigationCategoryProjection = bson.D{
+ {Key: translationcategorymod.FN.TranslationCategory.Id, Value: 1},
+ {Key: translationcategorymod.FN.TranslationCategory.Name, Value: 1},
+ {Key: translationcategorymod.FN.TranslationCategory.Icon, Value: 1},
+ {Key: translationcategorymod.FN.TranslationCategory.CategoryType, Value: 1},
+ {Key: translationcategorymod.FN.TranslationCategory.UrlValue, Value: 1},
+ }
+ headerNavigationInfoProjection = bson.D{
+ {Key: translationmod.FN.TranslationInfo.Id, Value: 1},
+ {Key: translationmod.FN.TranslationInfo.Name, Value: 1},
+ {Key: translationmod.FN.TranslationInfo.TransCategoryId, Value: 1},
+ {Key: translationmod.FN.TranslationInfo.IsFriendAds, Value: 1},
+ {Key: translationmod.FN.TranslationInfo.Link, Value: 1},
+ }
+)
+
+// HeaderNavigation 查询头部导航配置
+func HeaderNavigation(ctx context.Context) (proto.AppTranslationInfo, *srvdef.SrvErr) {
+ categoryList, err := translationcategorymod.FindManyTranslationCategorys(ctx, bson.M{
+ translationcategorymod.FN.TranslationCategory.Status: true,
+ translationcategorymod.FN.TranslationCategory.CategoryType: bson.M{
+ "$in": []pubdef.TranslationType{
+ pubdef.TranslationTypeUrl,
+ pubdef.TranslationTypeSub,
+ },
+ },
+ }, options.Find().
+ SetProjection(headerNavigationCategoryProjection).
+ SetSort(bson.D{
+ {Key: translationcategorymod.FN.TranslationCategory.Rank, Value: -1},
+ {Key: translationcategorymod.FN.TranslationCategory.Id, Value: -1},
+ }).
+ SetLimit(headerNavigationCategoryLimit))
+ if err != nil {
+ return proto.AppTranslationInfo{}, err
+ }
+ resp := proto.AppTranslationInfo{CategoryList: make([]proto.TranslationCategory, 0, len(categoryList))}
+ if len(categoryList) == 0 {
+ return resp, nil
+ }
+ categoryIds := make([]int64, 0, len(categoryList))
+ for _, item := range categoryList {
+ if item != nil && item.Id > 0 {
+ categoryIds = append(categoryIds, item.Id)
+ }
+ }
+ translationListByCategory, err := queryHeaderTranslationList(ctx, categoryIds)
+ if err != nil {
+ return proto.AppTranslationInfo{}, err
+ }
+ imgCdn := configser.ImageCdnFromCtx(ctx)
+ for _, item := range categoryList {
+ if item == nil {
+ continue
+ }
+ resp.CategoryList = append(resp.CategoryList, proto.TranslationCategory{
+ ID: item.Id,
+ Name: item.Name,
+ Icon: netutil.MergeURL(imgCdn, item.Icon),
+ UrlValue: item.UrlValue,
+ CategoryType: item.CategoryType,
+ TranslationList: translationListByCategory[item.Id],
+ })
+ }
+ return resp, nil
+}
+
+// queryHeaderTranslationList 批量查询头部导航二级配置并按分类ID分组
+func queryHeaderTranslationList(ctx context.Context, categoryIds []int64) (map[int64][]proto.TranslationInfo, *srvdef.SrvErr) {
+ result := make(map[int64][]proto.TranslationInfo, len(categoryIds))
+ if len(categoryIds) == 0 {
+ return result, nil
+ }
+ list, err := translationmod.FindManyTranslationInfos(ctx, bson.M{
+ translationmod.FN.TranslationInfo.Status: true,
+ translationmod.FN.TranslationInfo.TransCategoryId: bson.M{"$in": categoryIds},
+ }, options.Find().
+ SetProjection(headerNavigationInfoProjection).
+ SetSort(bson.D{
+ {Key: translationmod.FN.TranslationInfo.Rank, Value: -1},
+ {Key: translationmod.FN.TranslationInfo.Id, Value: -1},
+ }).
+ SetLimit(headerNavigationInfoLimit))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil || item.IsFriendAds {
+ continue
+ }
+ info := proto.TranslationInfo{
+ ID: item.Id,
+ Name: item.Name,
+ TransCategoryId: item.TransCategoryId,
+ UrlValue: headerTranslationURL(item),
+ }
+ if info.UrlValue == "" {
+ continue
+ }
+ result[item.TransCategoryId] = append(result[item.TransCategoryId], info)
+ }
+ return result, nil
+}
+
+// headerTranslationURL 解析导航明细跳转地址
+func headerTranslationURL(item *translationmod.TranslationInfo) string {
+ if item == nil {
+ return ""
+ }
+ return strings.TrimSpace(item.Link)
+}
diff --git a/app/service/playletser/favorite.go b/app/service/playletser/favorite.go
new file mode 100644
index 0000000..5379e97
--- /dev/null
+++ b/app/service/playletser/favorite.go
@@ -0,0 +1,126 @@
+package playletser
+
+import (
+ "context"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/reportser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/models/playletfavoritemod"
+ "videoweb/pub/models/playletmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// FavoritePlaylet 收藏或取消收藏上架短剧,重复请求不重复变更收藏数
+func FavoritePlaylet(ctx context.Context, userID int64, ip string, ua useragent.UA, req *proto.FavoritePlayletReq) (*proto.FavoritePlayletResp, *srvdef.SrvErr) {
+ if req == nil || userID <= 0 || req.Id <= 0 || req.IsFavorite == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: req.Id, playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.FindOne().SetProjection(bson.M{
+ playletmod.FN.Playlet.Id: 1, playletmod.FN.Playlet.Title: 1,
+ playletmod.FN.Playlet.TagIds: 1, playletmod.FN.Playlet.ThemeIds: 1,
+ playletmod.FN.Playlet.CountCollect: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ changed := false
+ flag := int64(1)
+ if *req.IsFavorite {
+ changed, err = playletfavoritemod.Upsert(ctx, userID, req.Id, time.Now())
+ } else {
+ flag = 2
+ changed, err = playletfavoritemod.Delete(ctx, bson.M{
+ playletfavoritemod.FieldUserID: userID, playletfavoritemod.FieldPlayletID: req.Id,
+ })
+ }
+ if err != nil {
+ return nil, err
+ }
+ if changed {
+ delta := int64(1)
+ if !*req.IsFavorite {
+ delta = -1
+ }
+ if delta > 0 || playlet.CountCollect > 0 {
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{
+ "$inc": bson.M{playletmod.FN.Playlet.CountCollect: delta},
+ }); err != nil {
+ return nil, err
+ }
+ playlet.CountCollect += delta
+ }
+ if err := reportser.ReportPlayletCollect(userID, ua, ip, playlet, flag); err != nil {
+ log.Warn("ReportPlayletCollect failed", log.Any("playletId", req.Id), log.Any("userId", userID), log.E(err))
+ }
+ }
+ return &proto.FavoritePlayletResp{Id: req.Id, IsFavorite: *req.IsFavorite, CountCollect: playlet.CountCollect}, nil
+}
+
+// QueryFavoritePlayletList 按收藏时间倒序查询当前用户收藏的上架短剧
+func QueryFavoritePlayletList(ctx context.Context, userID int64, req *proto.QueryFavoritePlayletListReq) (*proto.QueryFavoritePlayletListResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
+ pipeline := []bson.M{
+ {"$match": bson.M{playletfavoritemod.FieldUserID: userID}},
+ {"$lookup": bson.M{
+ "from": playletmod.TN.Playlet,
+ "localField": playletfavoritemod.FieldPlayletID,
+ "foreignField": playletmod.FN.Playlet.Id,
+ "as": "playlet",
+ }},
+ {"$unwind": "$playlet"},
+ {"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
+ }
+ countRows := make([]struct {
+ Total int64 `bson:"total"`
+ }, 0)
+ countPipeline := append(append([]bson.M(nil), pipeline...), bson.M{"$count": "total"})
+ if err := playletfavoritemod.Aggregate(ctx, &countRows, countPipeline); err != nil {
+ return nil, err
+ }
+ total := int64(0)
+ if len(countRows) > 0 {
+ total = countRows[0].Total
+ }
+ resp := &proto.QueryFavoritePlayletListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletData, 0),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ pagePipeline := append(append([]bson.M(nil), pipeline...),
+ bson.M{"$sort": bson.D{{Key: playletfavoritemod.FieldCreatedAt, Value: -1}, {Key: playletfavoritemod.FieldID, Value: -1}}},
+ bson.M{"$skip": (pageNum - 1) * pageSize}, bson.M{"$limit": pageSize},
+ bson.M{"$replaceRoot": bson.M{"newRoot": "$playlet"}},
+ bson.M{"$project": appPlayletRecommendProjection},
+ )
+ list := make(playletmod.PlayletList, 0)
+ if err := playletfavoritemod.Aggregate(ctx, &list, pagePipeline); err != nil {
+ return nil, err
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ cdn := configser.GetRequestConfig(ctx).ImgCdn
+ items := make([]*proto.PlayletData, 0, len(list))
+ for _, item := range list {
+ data := playletToAppData(item, cdn, tagNames, themeNames)
+ items = append(items, &data)
+ }
+ resp.List = items
+ return resp, nil
+}
diff --git a/app/service/playletser/like_list.go b/app/service/playletser/like_list.go
new file mode 100644
index 0000000..3460e11
--- /dev/null
+++ b/app/service/playletser/like_list.go
@@ -0,0 +1,73 @@
+package playletser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletlikemod"
+ "videoweb/pub/models/playletmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// QueryLikePlayletList 按点赞时间倒序查询当前用户点赞的上架短剧
+func QueryLikePlayletList(ctx context.Context, userID int64, req *proto.QueryLikePlayletListReq) (*proto.QueryLikePlayletListResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
+ pipeline := []bson.M{
+ {"$match": bson.M{playletlikemod.FN.PlayletLike.UserID: userID}},
+ {"$lookup": bson.M{
+ "from": playletmod.TN.Playlet,
+ "localField": playletlikemod.FN.PlayletLike.PlayletID,
+ "foreignField": playletmod.FN.Playlet.Id,
+ "as": "playlet",
+ }},
+ {"$unwind": "$playlet"},
+ {"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
+ }
+ countRows := make([]struct {
+ Total int64 `bson:"total"`
+ }, 0)
+ if err := playletlikemod.AggregatePlayletLike(ctx, &countRows, append(append([]bson.M(nil), pipeline...), bson.M{"$count": "total"})); err != nil {
+ return nil, err
+ }
+ total := int64(0)
+ if len(countRows) > 0 {
+ total = countRows[0].Total
+ }
+ resp := &proto.QueryLikePlayletListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletData, 0),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ pagePipeline := append([]bson.M{pipeline[0], {"$sort": bson.D{{Key: playletlikemod.FN.PlayletLike.CreatedAt, Value: -1}, {Key: playletlikemod.FN.PlayletLike.Id, Value: -1}}}}, pipeline[1:]...)
+ pagePipeline = append(pagePipeline,
+ bson.M{"$skip": (pageNum - 1) * pageSize},
+ bson.M{"$limit": pageSize},
+ bson.M{"$replaceRoot": bson.M{"newRoot": "$playlet"}},
+ bson.M{"$project": appPlayletRecommendProjection},
+ )
+ list := make(playletmod.PlayletList, 0)
+ if err := playletlikemod.AggregatePlayletLike(ctx, &list, pagePipeline); err != nil {
+ return nil, err
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ cdn := configser.GetRequestConfig(ctx).ImgCdn
+ items := make([]*proto.PlayletData, 0, len(list))
+ for _, item := range list {
+ data := playletToAppData(item, cdn, tagNames, themeNames)
+ items = append(items, &data)
+ }
+ resp.List = items
+ return resp, nil
+}
diff --git a/app/service/playletser/playlet.go b/app/service/playletser/playlet.go
new file mode 100644
index 0000000..237722f
--- /dev/null
+++ b/app/service/playletser/playlet.go
@@ -0,0 +1,2589 @@
+package playletser
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/reportser"
+ "videoweb/app/service/slugser"
+ "videoweb/app/service/translationser"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models"
+ "videoweb/pub/models/announcementmod"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletfavoritemod"
+ "videoweb/pub/models/playlethomeconfigmod"
+ "videoweb/pub/models/playletlikemod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ playletGuessMinPageSize int64 = 8
+ playletGuessMaxPageSize int64 = 100
+ playletRecommendDefaultPageSize int64 = 20
+ playletRecommendMaxPageSize int64 = 100
+ playletRecommendAdInterval int64 = 8
+ playletListAdInterval int64 = 4
+ playletContentPlayletAdInterval int64 = 4
+ playletContentVideoAdInterval int64 = 5
+ homePlayletListLimit int64 = 12
+ playletDetailHotLimit int64 = 11
+)
+
+// PlayletGuessList 分页查询猜你喜欢短剧,按观看量降序返回上架短剧
+func PlayletGuessList(ctx context.Context, req *proto.PlayletGuessListReq) (*proto.PlayletGuessListResp, *srvdef.SrvErr) {
+ if req == nil || req.Id < 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID无效")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletGuessMinPageSize, playletGuessMaxPageSize)
+ if pageSize < playletGuessMinPageSize {
+ pageSize = playletGuessMinPageSize
+ }
+ filter := bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ if req.Id > 0 {
+ filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": req.Id}
+ }
+ total, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletGuessListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total,
+ PageNum: int(pageNum),
+ PageSize: int(pageSize),
+ List: make([]*proto.PlayletRecommendListItem, 0),
+ }}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ list, err := findPlayletRecommendPage(ctx, filter, bson.D{
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }, (pageNum-1)*pageSize, pageSize)
+ if err != nil {
+ return nil, err
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ items := make([]*proto.PlayletRecommendListItem, 0, len(list))
+ for _, item := range list {
+ if item != nil {
+ items = append(items, playletToRecommendListItem(item, cdn, tagNames, themeNames))
+ }
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// playletRankAggItem 短剧排行榜聚合结果,只保留榜单展示需要的轻量字段
+type playletRankAggItem struct {
+ playletmod.Playlet `bson:",inline"`
+ RankScore int64 `bson:"rankScore"`
+}
+
+type playletRankCount struct {
+ Total int64 `bson:"total"`
+}
+
+// PlayletThemeList 查询全部已启用短剧题材
+func PlayletThemeList(ctx context.Context) (*proto.PlayletThemeListResp, *srvdef.SrvErr) {
+ list, err := playletthememod.FindManyPlayletTheme(ctx,
+ bson.M{playletthememod.FN.PlayletTheme.Status: true},
+ options.Find().
+ SetProjection(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Slug, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Icon, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Rank, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.SeoInfo.Title, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.SeoInfo.Description, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.SeoInfo.Keyworld, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Rank, Value: -1},
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: -1},
+ }),
+ )
+ if err != nil {
+ return nil, err
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ resp := &proto.PlayletThemeListResp{List: make([]proto.PlayletThemeItem, 0, len(list))}
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ resp.List = append(resp.List, proto.PlayletThemeItem{
+ Id: item.Id, Name: item.Name, Slug: item.Slug,
+ Icon: netutil.MergeURL(cdn, item.Icon), Rank: item.Rank, SeoInfo: item.SeoInfo,
+ })
+ }
+ return resp, nil
+}
+
+var appPlayletRecommendProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Status, Value: 1},
+ {Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
+ {Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
+ {Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
+ {Key: playletmod.FN.Playlet.ShowOriginal, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaCreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaUpdatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, 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.Preview, Value: 1},
+ {Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.ContentType, Value: 1},
+ {Key: playletmod.FN.Playlet.PayType, Value: 1},
+ {Key: playletmod.FN.Playlet.SingleCoins, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.FreeEpisodeCount, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualHeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Description, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Keyworld, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+}
+
+// playletDetailProjection 短剧详情投影,额外返回详情页需要的评论数量
+func playletDetailProjection() bson.D {
+ projection := make(bson.D, 0, len(appPlayletRecommendProjection)+1)
+ projection = append(projection, appPlayletRecommendProjection...)
+ projection = append(projection, bson.E{Key: playletmod.FN.Playlet.CountComment, Value: 1})
+ return projection
+}
+
+// PlayletRecommendList 分页查询 app 端短剧推荐列表,并按固定间隔穿插列表广告
+func PlayletRecommendList(ctx context.Context, req *proto.PlayletRecommendListReq) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ categoryId, err := slugser.PlayletCategoryID(ctx, req.CategoryId, req.CategorySlug, false)
+ if err != nil {
+ return nil, err
+ }
+ tagId, err := slugser.ContentTagID(ctx, req.TagId, req.TagSlug, false)
+ if err != nil {
+ return nil, err
+ }
+ req.CategoryId = categoryId
+ req.TagId = tagId
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
+ filter, sort := playletRecommendFilterAndSort(req.TagId, req.CategoryId, req.SortList)
+ config := configser.GetRequestConfig(ctx)
+ ads := configser.GetAdvitise().PostList
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ if len(ads) == 0 {
+ return playletRecommendListWithoutAds(ctx, filter, sort, req.CategoryId, pageNum, pageSize, totalCount, config.ImgCdn)
+ }
+ adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), playletRecommendAdInterval)
+ mixedTotalCount := totalCount + adsTotalCount
+ start := (pageNum - 1) * pageSize
+ if start >= mixedTotalCount || totalCount == 0 {
+ resp := emptyPlayletRecommendListPage(pageNum, pageSize)
+ resp.TotalCount = mixedTotalCount
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedTotalCount {
+ end = mixedTotalCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, playletRecommendAdInterval)
+ list := playletmod.PlayletList{}
+ if contentLimit > 0 {
+ list, err = findPlayletRecommendPage(ctx, filter, sort, contentStart, contentLimit, req.CategoryId)
+ if err != nil {
+ return nil, err
+ }
+ }
+ tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ items := buildPlayletRecommendMixedList(list, ads, config.ImgCdn, tagNameMap, themeNameMap, start, end, contentStart, adsTotalCount)
+ return &proto.PlayletRecommendListResp{BasePageResp: response.BasePageResp{
+ TotalCount: mixedTotalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }}, nil
+}
+
+// PlayletList 分页查询已上架短剧,每四部短剧后穿插一条广告
+func PlayletList(ctx context.Context, req *proto.PlayletListReq) (*proto.PlayletListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ if req.SortType != 0 && req.SortType != 1 && req.SortType != 2 {
+ return nil, srvdef.New(srvdef.ErrParamError, "排序方式无效", "")
+ }
+ filter := playletListFilter()
+ if slug := strings.TrimSpace(req.ThemeSlug); slug != "" || req.ThemeId > 0 {
+ themeFilter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
+ if slug != "" {
+ themeFilter[playletthememod.FN.PlayletTheme.Slug] = slug
+ } else {
+ themeFilter[playletthememod.FN.PlayletTheme.Id] = req.ThemeId
+ }
+ theme, err := playletthememod.FindOnePlayletTheme(ctx, themeFilter)
+ if err != nil {
+ return nil, err
+ }
+ filter[playletmod.FN.Playlet.ThemeIds] = theme.Id
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
+ playletCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ advitise := configser.GetAdvitise()
+ ads := advitise.PostList
+ adCount := playletListInsertedAdsCount(playletCount, len(ads))
+ mixedCount := playletCount + adCount
+ resp := &proto.PlayletListResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: mixedCount, PageNum: int(pageNum), PageSize: int(pageSize),
+ List: make([]*proto.PlayletRecommendListItem, 0),
+ },
+ Advitise: advitise,
+ }
+ start := (pageNum - 1) * pageSize
+ if start >= mixedCount {
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedCount {
+ end = mixedCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, playletCount, adCount, playletListAdInterval)
+ list := make(playletmod.PlayletList, 0)
+ if contentLimit > 0 {
+ list, err = findPlayletRecommendPage(ctx, filter, playletListSort(req.SortType), contentStart, contentLimit)
+ if err != nil {
+ return nil, err
+ }
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ resp.List = buildPlayletListMixedPage(list, ads, configser.GetRequestConfig(ctx).ImgCdn,
+ tagNames, themeNames, start, end, contentStart, adCount)
+ return resp, nil
+}
+
+// ContentList 分页查询上架短剧或视频,短剧每四条、视频每五条穿插一条广告
+func ContentList(ctx context.Context, req *proto.PlayletContentListReq) (*proto.PlayletContentListResp, *srvdef.SrvErr) {
+ if req == nil || (req.DataType != 1 && req.DataType != 2) {
+ return nil, srvdef.New(srvdef.ErrParamError, "数据类型无效", "")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
+ var (
+ contentCount int64
+ err *srvdef.SrvErr
+ )
+ if req.DataType == 1 {
+ contentCount, err = playletmod.CountPlaylet(ctx, bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp})
+ } else {
+ contentCount, err = videoinformationmod.CountVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ })
+ }
+ if err != nil {
+ return nil, err
+ }
+ advitise := configser.GetAdvitise()
+ ads := advitise.PostList
+ interval := playletContentAdInterval(req.DataType)
+ adCount := playletContentInsertedAdsCount(contentCount, len(ads), interval)
+ mixedCount := contentCount + adCount
+ resp := &proto.PlayletContentListResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: mixedCount, PageNum: int(pageNum), PageSize: int(pageSize),
+ List: make([]*proto.PlayletContentListItem, 0),
+ },
+ Advitise: advitise,
+ }
+ start := (pageNum - 1) * pageSize
+ if start >= mixedCount {
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedCount {
+ end = mixedCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, contentCount, adCount, interval)
+ imgCdn := configser.GetRequestConfig(ctx).ImgCdn
+ items := make([]*proto.PlayletContentListItem, 0, contentLimit)
+ if contentLimit > 0 {
+ items, err = findPlayletContentPage(ctx, req.DataType, contentStart, contentLimit, imgCdn)
+ if err != nil {
+ return nil, err
+ }
+ }
+ resp.List = buildPlayletContentMixedPage(items, ads, imgCdn,
+ start, end, contentStart, adCount, interval)
+ return resp, nil
+}
+
+// playletContentAdInterval 根据数据类型返回广告插入间隔
+func playletContentAdInterval(dataType int64) int64 {
+ if dataType == 2 {
+ return playletContentVideoAdInterval
+ }
+ return playletContentPlayletAdInterval
+}
+
+// playletContentInsertedAdsCount 只在后续仍有内容时插入广告,素材不足时循环使用
+func playletContentInsertedAdsCount(contentCount int64, adsCount int, interval int64) int64 {
+ if contentCount <= 1 || adsCount == 0 {
+ return 0
+ }
+ return (contentCount - 1) / interval
+}
+
+// findPlayletContentPage 按数据类型查询当前页需要的内容并转换为统一列表项
+func findPlayletContentPage(ctx context.Context, dataType, skip, limit int64, imgCdn string) ([]*proto.PlayletContentListItem, *srvdef.SrvErr) {
+ items := make([]*proto.PlayletContentListItem, 0, limit)
+ if dataType == 1 {
+ list, err := findPlayletRecommendPage(ctx,
+ bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}, playletContentPlayletSort(), skip, limit)
+ if err != nil {
+ return nil, err
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ data := playletToAppData(item, imgCdn, tagNames, themeNames)
+ items = append(items, &proto.PlayletContentListItem{Type: 0, Playlet: &data})
+ }
+ return items, nil
+ }
+ list := make(videoinformationmod.VideoInformationList, 0)
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &list, playletContentVideoPipeline(skip, limit)); err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ data := playletContentVideoInfo(item, imgCdn)
+ items = append(items, &proto.PlayletContentListItem{Type: 0, Video: &data})
+ }
+ return items, nil
+}
+
+// playletContentPlayletSort 先按短剧累计浏览数排序,再按热度和 ID 稳定排序
+func playletContentPlayletSort() bson.D {
+ return bson.D{
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+}
+
+// playletContentVideoPipeline 按视频展示观看数分页,展示观看数为真实数与后台配置数之和
+func playletContentVideoPipeline(skip, limit int64) []bson.M {
+ return []bson.M{
+ {"$match": bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
+ {"$addFields": bson.M{"watchScore": bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Real.WatchTimes, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Fake.WatchTimes, 0}},
+ }}}},
+ {"$sort": bson.D{
+ {Key: "watchScore", Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$project": playletContentVideoProjection()},
+ }
+}
+
+// playletContentVideoProjection 只读取视频卡片需要的字段
+func playletContentVideoProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.Keyworld: 1,
+ videoinformationmod.FN.VideoInformation.Desc: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ }
+}
+
+// playletContentVideoInfo 将视频模型转换为列表卡片
+func playletContentVideoInfo(item *videoinformationmod.VideoInformation, imgCdn string) proto.VideoInfo {
+ tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
+ return proto.VideoInfo{ContentData: proto.ContentData{
+ Id: item.Id, Title: item.Title, Slug: item.Slug, AddedTime: item.AddedTime,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg), Tags: item.Tags, TagsName: tagNames, TagSlugs: tagSlugs,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ SeoInfo: pubdef.SeoValue{Title: item.Title, Keyworld: append([]string{}, item.Keyworld...), Description: item.Desc},
+ }}
+}
+
+// buildPlayletContentMixedPage 按全局位置组装内容与广告并保持跨页间隔一致
+func buildPlayletContentMixedPage(content []*proto.PlayletContentListItem, ads []proxy.AdvitiseInfo, imgCdn string,
+ start, end, contentStart, adCount, interval int64) []*proto.PlayletContentListItem {
+ items := make([]*proto.PlayletContentListItem, 0, end-start)
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adCount, interval)
+ if adIndex >= 0 && len(ads) > 0 {
+ ad := ads[adIndex%int64(len(ads))]
+ ad.Images = netutil.MergeURL(imgCdn, ad.Images)
+ items = append(items, &proto.PlayletContentListItem{Type: 2, Advertise: &ad})
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adCount, interval) - contentStart
+ if contentIndex >= 0 && contentIndex < int64(len(content)) {
+ items = append(items, content[contentIndex])
+ }
+ }
+ return items
+}
+
+// playletListFilter 只查询已上架短剧
+func playletListFilter() bson.M {
+ return bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
+}
+
+// playletListInsertedAdsCount 只在仍有后续短剧时插入广告,广告素材不足时循环使用
+func playletListInsertedAdsCount(playletCount int64, adsCount int) int64 {
+ if playletCount <= 1 || adsCount == 0 {
+ return 0
+ }
+ return (playletCount - 1) / playletListAdInterval
+}
+
+// playletListSort 根据列表排序方式生成稳定的短剧顺序
+func playletListSort(sortType int64) bson.D {
+ switch sortType {
+ case 2:
+ return bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
+ default:
+ return bson.D{{Key: playletmod.FN.Playlet.HeatValue, Value: -1}, {Key: playletmod.FN.Playlet.CountBrowse, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
+ }
+}
+
+// buildPlayletListMixedPage 根据全局位置组装当前页,避免跨页后重新计算广告间隔
+func buildPlayletListMixedPage(list playletmod.PlayletList, ads []proxy.AdvitiseInfo, imgCdn string,
+ tagNames contenttagmod.ContentTagMap, themeNames map[int64]string, start, end, contentStart, adCount int64) []*proto.PlayletRecommendListItem {
+ items := make([]*proto.PlayletRecommendListItem, 0, end-start)
+ contentByIndex := make(map[int64]*playletmod.Playlet, len(list))
+ for i, item := range list {
+ contentByIndex[contentStart+int64(i)] = item
+ }
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adCount, playletListAdInterval)
+ if adIndex >= 0 && len(ads) > 0 {
+ items = append(items, playletAdToRecommendListItem(ads[adIndex%int64(len(ads))], imgCdn))
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adCount, playletListAdInterval)
+ if item := contentByIndex[contentIndex]; item != nil {
+ items = append(items, playletToRecommendListItem(item, imgCdn, tagNames, themeNames))
+ }
+ }
+ return items
+}
+
+// PlayletTagList 根据统一标签ID分页查询已上架短剧
+func PlayletTagList(ctx context.Context, req *proto.ContentTagPageReq) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
+ }
+ tagId, err := slugser.ContentTagID(ctx, req.Id, req.Slug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.Id = tagId
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, playletRecommendDefaultPageSize, playletRecommendMaxPageSize)
+ ok, err := configser.HasEnabledContentTagBind(ctx, req.Id, contenttagmod.ContentTagTypePlaylet)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return emptyPlayletRecommendListPage(pageNum, pageSize), nil
+ }
+ filter := bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ playletmod.FN.Playlet.TagIds: req.Id,
+ }
+ sort := bson.D{
+ {Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ config := configser.GetRequestConfig(ctx)
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ return playletRecommendListWithoutAds(ctx, filter, sort, 0, pageNum, pageSize, totalCount, config.ImgCdn)
+}
+
+// QueryPlayletInfo 查询短剧详情、剧集列表和点赞状态
+func QueryPlayletInfo(ctx context.Context, userId int64, ip, ua string, req *proto.QueryPlayletInfoReq) (*proto.QueryPlayletInfoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ playletId, err := slugser.PlayletID(ctx, req.Id, req.Slug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.Id = playletId
+ config := configser.GetRequestConfig(ctx)
+ playletInfo, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: req.Id,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.FindOne().SetProjection(playletDetailProjection()))
+ if err != nil {
+ return nil, err
+ }
+ go incrementPlayletWatchCount(playletInfo)
+ chapterList, isLike, err := queryPlayletDetailExtraData(ctx, userId, playletInfo, ip, ua)
+ if err != nil {
+ return nil, err
+ }
+ isFavorite, err := queryPlayletIsFavorite(ctx, userId, playletInfo.Id)
+ if err != nil {
+ return nil, err
+ }
+ tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, playletmod.PlayletList{playletInfo})
+ if err != nil {
+ return nil, err
+ }
+ if err := applyOfficialChapterAccess(ctx, playletInfo, chapterList, ip); err != nil {
+ return nil, err
+ }
+ return &proto.QueryPlayletInfoResp{
+ FreeEpisodeCount: playletInfo.EffectiveFreeEpisodeCount(),
+ Path: playletRequestPath(ctx),
+ Translations: translationser.GetTranslationData(),
+ Config: config,
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ PlayletInfo: playletDetailInfoData(playletInfo, config.ImgCdn, tagNameMap, themeNameMap, isLike, isFavorite),
+ ChapterList: chapterList,
+ IsLike: isLike,
+ IsFavorite: isFavorite,
+ }, nil
+}
+
+// QueryPlayletChapterInfo 查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态
+func QueryPlayletChapterInfo(ctx context.Context, userId int64, ip, ua string, req *proto.QueryPlayletChapterInfoReq) (*proto.QueryPlayletChapterInfoResp, *srvdef.SrvErr) {
+ if req == nil || req.Id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ config := configser.GetRequestConfig(ctx)
+ chapterInfo, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: req.Id,
+ }, options.FindOne().SetProjection(playletChapterProjection()))
+ if err != nil {
+ return nil, err
+ }
+ playletInfo, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: chapterInfo.PlayletId,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.FindOne().SetProjection(appPlayletRecommendProjection))
+ if err != nil {
+ return nil, err
+ }
+ if !playletInfo.IsOfficialEpisodeOpen(chapterInfo.CurrentEpisode) {
+ return nil, srvdef.New(srvdef.ErrNoData, "本集暂未在官网开放", "")
+ }
+ if err = ensureOriginalChapterWatermarkReady(playletInfo, chapterInfo); err != nil {
+ return nil, err
+ }
+ go incrementPlayletWatchCount(playletInfo)
+ go incrementPlayletChapterWatchCount(chapterInfo.Id)
+
+ hotPlayletList, chapterList, isLike, err := queryPlayletChapterDetailExtraData(ctx, userId, playletInfo, ip, ua)
+ if err != nil {
+ return nil, err
+ }
+ isFavorite, err := queryPlayletIsFavorite(ctx, userId, playletInfo.Id)
+ if err != nil {
+ return nil, err
+ }
+ allList := make(playletmod.PlayletList, 0, len(hotPlayletList)+1)
+ allList = append(allList, playletInfo)
+ allList = append(allList, hotPlayletList...)
+ tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, allList)
+ if err != nil {
+ return nil, err
+ }
+ chapterData := make([]proto.PlayletChapterInfo, 0, len(chapterList)+1)
+ chapterData = append(chapterData, chapterList...)
+ chapterData = append(chapterData, playletChapterToInfo(chapterInfo, ip, ua, isOriginalPlaylet(playletInfo)))
+ if err := applyOfficialChapterAccess(ctx, playletInfo, chapterData, ip); err != nil {
+ return nil, err
+ }
+ return &proto.QueryPlayletChapterInfoResp{
+ FreeEpisodeCount: playletInfo.EffectiveFreeEpisodeCount(),
+ Path: playletRequestPath(ctx),
+ Translations: translationser.GetTranslationData(),
+ Config: config,
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ ChapterInfo: chapterData[len(chapterData)-1],
+ PlayletInfo: playletToAppData(playletInfo, config.ImgCdn, tagNameMap, themeNameMap),
+ ChapterList: chapterData[:len(chapterData)-1],
+ HotPlayletList: playletListToAppData(hotPlayletList, config.ImgCdn, tagNameMap, themeNameMap),
+ IsLike: isLike,
+ IsFavorite: isFavorite,
+ }, nil
+}
+
+func applyOfficialChapterAccess(ctx context.Context, playlet *playletmod.Playlet, chapters []proto.PlayletChapterInfo, ip string) *srvdef.SrvErr {
+ if len(chapters) == 0 {
+ return nil
+ }
+ ids := make([]int64, 0, len(chapters))
+ for i := range chapters {
+ ids = append(ids, chapters[i].Id)
+ }
+ raw, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids},
+ playletchaptermod.FN.PlayletChapter.PlayletId: playlet.Id,
+ })
+ if err != nil {
+ return err
+ }
+ byID := make(map[int64]*playletchaptermod.PlayletChapter, len(raw))
+ for _, chapter := range raw {
+ byID[chapter.Id] = chapter
+ }
+ for i := range chapters {
+ chapter := byID[chapters[i].Id]
+ open := chapter != nil && playlet.IsOfficialEpisodeOpen(chapter.CurrentEpisode)
+ chapters[i].IsOfficialOpen = &open
+ chapters[i].VideoUrl = ""
+ if !open {
+ continue
+ }
+ url, useNewKey := chapterPlaySource(chapter, isOriginalPlaylet(playlet))
+ if url != "" {
+ chapters[i].VideoUrl = videoser.GetM3U8OfficialParam(chapter.Id, playlet.Id, url, ip, useNewKey)
+ }
+ }
+ return nil
+}
+
+// LikePlaylet 当前登录用户点赞或取消点赞指定短剧
+func LikePlaylet(ctx context.Context, userId int64, ip string, ua useragent.UA, req *proto.LikePlayletReq) (*proto.LikePlayletResp, *srvdef.SrvErr) {
+ if req == nil || userId <= 0 || req.Id <= 0 || req.IsLike == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ playletInfo, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: req.Id,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.FindOne().SetProjection(bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.Title: 1,
+ playletmod.FN.Playlet.Slug: 1,
+ playletmod.FN.Playlet.ThemeIds: 1,
+ playletmod.FN.Playlet.TagIds: 1,
+ playletmod.FN.Playlet.TotalEpisode: 1,
+ playletmod.FN.Playlet.CountLike: 1,
+ playletmod.FN.Playlet.VirtualCountLike: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ if *req.IsLike {
+ created, err := playletlikemod.UpsertOnePlayletLike(ctx, userId, req.Id, time.Now())
+ if err != nil {
+ return nil, err
+ }
+ if created {
+ playletInfo.CountLike++
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountLike: 1}}); err != nil {
+ return nil, err
+ }
+ if err := reportser.ReportPlayletLike(userId, ua, ip, playletInfo, 1); err != nil {
+ log.Warn("ReportPlayletLike failed", log.Any("playletId", req.Id), log.Any("userId", userId), log.E(err))
+ }
+ }
+ } else {
+ deleted, err := playletlikemod.DeleteOnePlayletLike(ctx, bson.M{
+ playletlikemod.FN.PlayletLike.UserID: userId,
+ playletlikemod.FN.PlayletLike.PlayletID: req.Id,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if deleted {
+ if playletInfo.CountLike > 0 {
+ playletInfo.CountLike--
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountLike: -1}}); err != nil {
+ return nil, err
+ }
+ }
+ if err := reportser.ReportPlayletLike(userId, ua, ip, playletInfo, 2); err != nil {
+ log.Warn("ReportPlayletLike failed", log.Any("playletId", req.Id), log.Any("userId", userId), log.E(err))
+ }
+ }
+ }
+ return &proto.LikePlayletResp{
+ Id: req.Id,
+ IsLike: *req.IsLike,
+ LikeNum: playletInfo.CountLike,
+ }, nil
+}
+
+// QueryPlayletRank 分页查询短剧排行榜,读取独立的 10 分钟内存缓存。
+// 点赞榜按累计点赞量、周/月/年榜按对应周期观看量取前 50;不足时随机补位。分页 pageSize 不是 50 时强制为 50。
+func QueryPlayletRank(ctx context.Context, req *proto.PlayletRankReq) (*proto.PlayletRankResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ period := proto.PlayletRankPeriod(req.RankPeriod)
+ if period < proto.PlayletRankPeriodLike || period > proto.PlayletRankPeriodYear {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := configser.NormalizeRankPage(req.PageNum, req.PageSize)
+ all := configser.GetPlayletRankList(configser.RankPeriod(period), 0)
+ items := configser.SliceRankPage(all, pageNum, pageSize)
+ return &proto.PlayletRankResp{BasePageResp: response.BasePageResp{
+ TotalCount: int64(len(all)),
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
+}
+
+// HomePlayletData 查询首页推荐、最新、最热三组短剧数据
+func HomePlayletData(ctx context.Context, imgCdn string) (proto.HomePlayletDataResp, *srvdef.SrvErr) {
+ resp := proto.HomePlayletDataResp{
+ RecommendList: make([]proto.PlayletData, 0),
+ NewestList: make([]proto.PlayletData, 0),
+ HotList: make([]proto.PlayletData, 0),
+ }
+ var (
+ recommendList playletmod.PlayletList
+ newestList playletmod.PlayletList
+ hotList playletmod.PlayletList
+ wg sync.WaitGroup
+ mu sync.Mutex
+ err1 *srvdef.SrvErr
+ )
+ setErr := func(err *srvdef.SrvErr) {
+ if err == nil {
+ return
+ }
+ mu.Lock()
+ defer mu.Unlock()
+ if err1 == nil {
+ err1 = err
+ }
+ }
+ queryList := func(sortType proto.PlayletRecommendSort, target *playletmod.PlayletList) {
+ defer wg.Done()
+ filter, sort := playletRecommendFilterAndSort(0, 0, int64(sortType))
+ list, err := findPlayletRecommendPage(ctx, filter, sort, 0, homePlayletListLimit)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ *target = list
+ }
+ wg.Add(3)
+ go queryList(proto.PlayletRecommendSortRecommend, &recommendList)
+ go queryList(proto.PlayletRecommendSortNewest, &newestList)
+ go queryList(proto.PlayletRecommendSortHot, &hotList)
+ wg.Wait()
+ if err1 != nil {
+ return resp, err1
+ }
+ allList := make(playletmod.PlayletList, 0, len(recommendList)+len(newestList)+len(hotList))
+ allList = append(allList, recommendList...)
+ allList = append(allList, newestList...)
+ allList = append(allList, hotList...)
+ tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, allList)
+ if err != nil {
+ return resp, err
+ }
+ resp.RecommendList = playletListToAppData(recommendList, imgCdn, tagNameMap, themeNameMap)
+ resp.NewestList = playletListToAppData(newestList, imgCdn, tagNameMap, themeNameMap)
+ resp.HotList = playletListToAppData(hotList, imgCdn, tagNameMap, themeNameMap)
+ return resp, nil
+}
+
+// playletRecommendFilterAndSort 根据排序枚举构建短剧查询条件和排序规则
+func playletRecommendFilterAndSort(tagId, categoryId, sortList int64) (bson.M, bson.D) {
+ filter := bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ if tagId > 0 {
+ filter[playletmod.FN.Playlet.TagIds] = tagId
+ }
+ if categoryId > 0 {
+ filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{categoryId}}
+ }
+ switch proto.PlayletRecommendSort(sortList) {
+ case proto.PlayletRecommendSortNewest:
+ return filter, bson.D{
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ case proto.PlayletRecommendSortHot:
+ return filter, bson.D{
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ default:
+ if categoryId <= 0 {
+ filter[playletmod.FN.Playlet.IsRecommend] = true
+ } else {
+ return filter, playletmod.CategoryRankSort()
+ }
+ return filter, bson.D{
+ {Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ }
+}
+
+// findPlayletRecommendPage 查询当前混排分页需要的短剧数据
+func findPlayletRecommendPage(ctx context.Context, filter bson.M, sort bson.D, skip, limit int64, categoryIds ...int64) (playletmod.PlayletList, *srvdef.SrvErr) {
+ if len(categoryIds) > 0 && categoryIds[0] > 0 && len(sort) > 0 && sort[0].Key == "_categoryRank" {
+ return playletmod.FindManyPlayletByCategoryRank(ctx, filter, categoryIds[0], skip, limit, sort, appPlayletRecommendProjection)
+ }
+ computedSort := make(bson.D, len(sort))
+ copy(computedSort, sort)
+ addFields := bson.M{}
+ for i := range computedSort {
+ switch computedSort[i].Key {
+ case playletmod.FN.Playlet.CountBrowse:
+ computedSort[i].Key = "displayCountBrowse"
+ addFields["displayCountBrowse"] = bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.CountBrowse, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.VirtualCountBrowse, 0}},
+ }}
+ case playletmod.FN.Playlet.HeatValue:
+ computedSort[i].Key = "displayHeatValue"
+ addFields["displayHeatValue"] = bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.HeatValue, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.VirtualHeatValue, 0}},
+ }}
+ }
+ }
+ if len(addFields) > 0 {
+ list := make(playletmod.PlayletList, 0)
+ pipeline := []bson.M{
+ {"$match": filter},
+ {"$addFields": addFields},
+ {"$sort": computedSort},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$project": appPlayletRecommendProjection},
+ }
+ if err := playletmod.AggregatePlaylet(ctx, &list, pipeline, options.Aggregate().SetAllowDiskUse(true)); err != nil {
+ return nil, err
+ }
+ return list, nil
+ }
+ return playletmod.FindManyPlaylet(ctx, filter, options.Find().
+ SetProjection(appPlayletRecommendProjection).
+ SetSort(sort).
+ SetSkip(skip).
+ SetLimit(limit))
+}
+
+// playletListToAppData 批量转换短剧模型为 app 短剧数据
+func playletListToAppData(list playletmod.PlayletList, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) []proto.PlayletData {
+ result := make([]proto.PlayletData, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, playletToAppData(item, imgCdn, tagNameMap, themeNameMap))
+ }
+ return result
+}
+
+// playletRecommendListWithoutAds 无广告时直接按短剧分页查询
+func playletRecommendListWithoutAds(ctx context.Context, filter bson.M, sort bson.D, categoryId, pageNum, pageSize, totalCount int64, imgCdn string) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
+ list, err := findPlayletRecommendPage(ctx, filter, sort, (pageNum-1)*pageSize, pageSize, categoryId)
+ if err != nil {
+ return nil, err
+ }
+ tagNameMap, themeNameMap, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletRecommendListItem, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, playletToRecommendListItem(item, imgCdn, tagNameMap, themeNameMap))
+ }
+ return &proto.PlayletRecommendListResp{BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }}, nil
+}
+
+// buildPlayletRecommendMixedList 按混排后的全局位置切出当前页短剧和广告数据
+func buildPlayletRecommendMixedList(list playletmod.PlayletList, ads []proxy.AdvitiseInfo, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, start, end, contentStart, adsTotal int64) []*proto.PlayletRecommendListItem {
+ items := make([]*proto.PlayletRecommendListItem, 0, end-start)
+ contentByIndex := make(map[int64]*playletmod.Playlet, len(list))
+ for i, item := range list {
+ contentByIndex[contentStart+int64(i)] = item
+ }
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, playletRecommendAdInterval)
+ if adIndex >= 0 && adIndex < adsTotal {
+ items = append(items, playletAdToRecommendListItem(ads[adIndex], imgCdn))
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, playletRecommendAdInterval)
+ if item := contentByIndex[contentIndex]; item != nil {
+ items = append(items, playletToRecommendListItem(item, imgCdn, tagNameMap, themeNameMap))
+ }
+ }
+ return items
+}
+
+// playletToRecommendListItem 将短剧模型转换为 app 推荐短剧列表项
+func playletToRecommendListItem(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) *proto.PlayletRecommendListItem {
+ return &proto.PlayletRecommendListItem{
+ Type: proto.PlayletRecommendItemTypePlaylet,
+ PlayletData: playletToAppData(item, imgCdn, tagNameMap, themeNameMap),
+ }
+}
+
+// playletAdToRecommendListItem 将广告转换为 app 推荐短剧列表项
+func playletAdToRecommendListItem(ad proxy.AdvitiseInfo, imgCdn string) *proto.PlayletRecommendListItem {
+ return &proto.PlayletRecommendListItem{
+ Type: proto.PlayletRecommendItemTypeAdvertise,
+ AdvertiseId: ad.Id,
+ Link: ad.Link,
+ Location: ad.Location,
+ PlayletData: proto.PlayletData{
+ Title: ad.Title,
+ Cover: netutil.MergeURL(imgCdn, ad.Images),
+ },
+ }
+}
+
+// queryPlayletDetailExtraData 并发查询详情页附加数据,缩短详情接口整体等待时间
+func queryPlayletDetailExtraData(ctx context.Context, userId int64, playletInfo *playletmod.Playlet, ip, ua string) ([]proto.PlayletChapterInfo, bool, *srvdef.SrvErr) {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var firstErr *srvdef.SrvErr
+ chapterList := make([]proto.PlayletChapterInfo, 0)
+ isLike := false
+ setErr := func(err *srvdef.SrvErr) {
+ if err == nil {
+ return
+ }
+ mu.Lock()
+ if firstErr == nil {
+ firstErr = err
+ }
+ mu.Unlock()
+ }
+
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ data, err := queryPlayletChapterList(ctx, playletInfo.Id, ip, ua, isOriginalPlaylet(playletInfo))
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ chapterList = data
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ data, err := queryPlayletIsLike(ctx, userId, playletInfo.Id)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ isLike = data
+ mu.Unlock()
+ }()
+ wg.Wait()
+ if firstErr != nil {
+ return nil, false, firstErr
+ }
+ return chapterList, isLike, nil
+}
+
+// queryPlayletChapterDetailExtraData 并发查询剧集详情页附加数据,热门短剧只按相同分类查询且不做兜底
+func queryPlayletChapterDetailExtraData(ctx context.Context, userId int64, playletInfo *playletmod.Playlet, ip, ua string) (playletmod.PlayletList, []proto.PlayletChapterInfo, bool, *srvdef.SrvErr) {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var firstErr *srvdef.SrvErr
+ hotPlayletList := make(playletmod.PlayletList, 0)
+ chapterList := make([]proto.PlayletChapterInfo, 0)
+ isLike := false
+ setErr := func(err *srvdef.SrvErr) {
+ if err == nil {
+ return
+ }
+ mu.Lock()
+ if firstErr == nil {
+ firstErr = err
+ }
+ mu.Unlock()
+ }
+
+ wg.Add(3)
+ go func() {
+ defer wg.Done()
+ data, err := queryPlayletChapterDetailHotList(ctx, playletInfo)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ hotPlayletList = data
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ data, err := queryPlayletChapterList(ctx, playletInfo.Id, ip, ua, isOriginalPlaylet(playletInfo))
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ chapterList = data
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ data, err := queryPlayletIsLike(ctx, userId, playletInfo.Id)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ isLike = data
+ mu.Unlock()
+ }()
+ wg.Wait()
+ if firstErr != nil {
+ return nil, nil, false, firstErr
+ }
+ return hotPlayletList, chapterList, isLike, nil
+}
+
+// incrementPlayletWatchCount 异步增加短剧浏览、热度和周/月/年窗口观看数。
+// 周/月/年窗口按北京时间切:周一 00:00、每月 1 日 00:00、每年 1 月 1 日 00:00。
+func incrementPlayletWatchCount(playletInfo *playletmod.Playlet) {
+ if playletInfo == nil {
+ return
+ }
+ now := utils.Now()
+ incDoc := bson.M{
+ playletmod.FN.Playlet.CountBrowse: 1,
+ playletmod.FN.Playlet.HeatValue: 1,
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.WatchUpdatedAt: now,
+ }
+ if playletInfo.WatchUpdatedAt.After(utils.BeginOfWeekByTime(now)) {
+ incDoc[playletmod.FN.Playlet.WeekWatchs] = 1
+ } else {
+ setDoc[playletmod.FN.Playlet.WeekWatchs] = 1
+ }
+ if playletInfo.WatchUpdatedAt.After(utils.BeginOfMonth(now)) {
+ incDoc[playletmod.FN.Playlet.MonthWatchs] = 1
+ } else {
+ setDoc[playletmod.FN.Playlet.MonthWatchs] = 1
+ }
+ if playletInfo.WatchUpdatedAt.After(utils.BeginOfYear(now)) {
+ incDoc[playletmod.FN.Playlet.YearWatchs] = 1
+ } else {
+ setDoc[playletmod.FN.Playlet.YearWatchs] = 1
+ }
+ if err := playletmod.UpdateOnePlaylet(context.Background(), bson.M{
+ playletmod.FN.Playlet.Id: playletInfo.Id,
+ }, bson.M{"$set": setDoc, "$inc": incDoc}); err != nil {
+ log.Warn("incrementPlayletWatchCount failed", log.Any("playletId", playletInfo.Id), log.E(err))
+ }
+}
+
+// incrementPlayletChapterWatchCount 异步增加短剧剧集浏览数,避免阻塞剧集详情读取
+func incrementPlayletChapterWatchCount(chapterId int64) {
+ if chapterId <= 0 {
+ return
+ }
+ if err := playletchaptermod.UpdateOnePlayletChapter(context.Background(), bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: chapterId,
+ }, bson.M{"$inc": bson.M{
+ playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
+ }}); err != nil {
+ log.Warn("更新短剧剧集浏览数失败", log.Any("chapterId", chapterId), log.E(err))
+ }
+}
+
+// queryPlayletChapterDetailHotList 查询剧集详情页热门短剧,按相同分类、累计点击量、更新时间排序,最多返回11条
+func queryPlayletChapterDetailHotList(ctx context.Context, playletInfo *playletmod.Playlet) (playletmod.PlayletList, *srvdef.SrvErr) {
+ filter := bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$ne": playletInfo.Id},
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ if len(playletInfo.CategoryIds) > 0 {
+ filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": playletInfo.CategoryIds}
+ }
+ return findPlayletRecommendPage(ctx, filter, bson.D{
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }, 0, playletDetailHotLimit)
+}
+
+// queryPlayletChapterList 查询短剧剧集列表,并按集数升序返回。
+// 原创短剧只返回已有水印地址的剧集:首次未处理好的不露出,换水印期间仍可播旧地址。
+func queryPlayletChapterList(ctx context.Context, playletId int64, ip, ua string, isOriginal bool) ([]proto.PlayletChapterInfo, *srvdef.SrvErr) {
+ filter := bson.M{
+ playletchaptermod.FN.PlayletChapter.PlayletId: playletId,
+ }
+ if isOriginal {
+ // 必须含非空白水印地址;空串、空白、缺字段都排除
+ filter[playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl] = bson.M{"$regex": "\\S"}
+ }
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, filter, options.Find().
+ SetProjection(playletChapterProjection()).
+ SetSort(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ data := make([]proto.PlayletChapterInfo, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if isOriginal && strings.TrimSpace(item.WatermarkVideoUrl) == "" {
+ continue
+ }
+ data = append(data, playletChapterToInfo(item, ip, ua, isOriginal))
+ }
+ return data, nil
+}
+
+// playletChapterProjection 限定详情页剧集字段,避免返回整条剧集文档
+func playletChapterProjection() bson.M {
+ return bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: 1,
+ playletchaptermod.FN.PlayletChapter.PlayletId: 1,
+ playletchaptermod.FN.PlayletChapter.MediaContentId: 1,
+ playletchaptermod.FN.PlayletChapter.CurrentEpisode: 1,
+ playletchaptermod.FN.PlayletChapter.Title: 1,
+ playletchaptermod.FN.PlayletChapter.Md5: 1,
+ playletchaptermod.FN.PlayletChapter.Hash: 1,
+ playletchaptermod.FN.PlayletChapter.Height: 1,
+ playletchaptermod.FN.PlayletChapter.Width: 1,
+ playletchaptermod.FN.PlayletChapter.Ratio: 1,
+ playletchaptermod.FN.PlayletChapter.VideoUrl: 1,
+ playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl: 1,
+ playletchaptermod.FN.PlayletChapter.MediaSize: 1,
+ playletchaptermod.FN.PlayletChapter.MediaTime: 1,
+ playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
+ playletchaptermod.FN.PlayletChapter.CountCollect: 1,
+ playletchaptermod.FN.PlayletChapter.CountLike: 1,
+ playletchaptermod.FN.PlayletChapter.CountComment: 1,
+ playletchaptermod.FN.PlayletChapter.PayType: 1,
+ playletchaptermod.FN.PlayletChapter.SingleCoins: 1,
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: 1,
+ playletchaptermod.FN.PlayletChapter.CreatedAt: 1,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: 1,
+ }
+}
+
+// playletChapterToInfo 将短剧剧集模型转换为详情页剧集数据。
+// 原创短剧只下水印地址,避免未处理好的剧集回退成原片地址。
+func playletChapterToInfo(item *playletchaptermod.PlayletChapter, ip, ua string, isOriginal bool) proto.PlayletChapterInfo {
+ _ = ua
+ videoURL, useNewKey := chapterPlaySource(item, isOriginal)
+ if videoURL != "" {
+ videoURL = videoser.GetPlayletM3U8AuthParam(item.Id, videoURL, ip, useNewKey)
+ }
+ return proto.PlayletChapterInfo{
+ Id: item.Id,
+ PlayletId: item.PlayletId,
+ MediaContentId: item.MediaContentId,
+ CurrentEpisode: item.CurrentEpisode,
+ Title: item.Title,
+ Md5: item.Md5,
+ Hash: item.Hash,
+ Height: item.Height,
+ Width: item.Width,
+ Ratio: item.Ratio,
+ VideoUrl: videoURL,
+ MediaSize: item.MediaSize,
+ MediaTime: item.MediaTime,
+ CountBrowse: item.CountBrowse,
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ CountComment: item.CountComment,
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ WatermarkStatus: item.WatermarkStatus,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+// chapterPlaySource 优先用水印地址。没有水印时,非原创短剧回退原片,并标记使用原片密钥。
+func chapterPlaySource(chapter *playletchaptermod.PlayletChapter, isOriginal bool) (string, bool) {
+ if chapter == nil {
+ return "", false
+ }
+ if url := strings.TrimSpace(chapter.WatermarkVideoUrl); url != "" {
+ return url, false
+ }
+ if isOriginal {
+ return "", false
+ }
+ url := strings.TrimSpace(chapter.VideoUrl)
+ return url, url != ""
+}
+
+// isOriginalPlaylet 判断是否原创短剧。
+// 后台新增剧没有媒资ID;历史数据可能缺 isOriginal 字段,按无媒资ID兜底。
+func isOriginalPlaylet(playlet *playletmod.Playlet) bool {
+ if playlet == nil {
+ return false
+ }
+ return playlet.IsOriginal || playlet.MediaCenterId == 0
+}
+
+// ensureOriginalChapterWatermarkReady 原创剧集必须已有水印地址才允许播放。
+func ensureOriginalChapterWatermarkReady(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) *srvdef.SrvErr {
+ if !isOriginalPlaylet(playlet) || chapter == nil {
+ return nil
+ }
+ if strings.TrimSpace(chapter.WatermarkVideoUrl) != "" {
+ return nil
+ }
+ return srvdef.New(srvdef.ErrNoData, "剧集处理中,请稍后观看", "剧集处理中,请稍后观看")
+}
+
+// queryPlayletIsLike 判断当前登录用户是否已点赞该短剧,未登录直接返回 false
+func queryPlayletIsLike(ctx context.Context, userId, playletId int64) (bool, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return false, nil
+ }
+ count, err := playletlikemod.CountPlayletLike(ctx, bson.M{
+ playletlikemod.FN.PlayletLike.UserID: userId,
+ playletlikemod.FN.PlayletLike.PlayletID: playletId,
+ })
+ if err != nil {
+ return false, err
+ }
+ return count > 0, nil
+}
+
+// playletDetailInfoData 将短剧模型转换为详情页专用响应结构
+func playletDetailInfoData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, isLike, isFavorite bool) proto.PlayletDetailInfo {
+ return proto.PlayletDetailInfo{
+ PlayletData: playletToAppData(item, imgCdn, tagNameMap, themeNameMap),
+ CountComment: item.CountComment,
+ IsLike: isLike,
+ IsFavorite: isFavorite,
+ }
+}
+
+// queryPlayletIsFavorite 判断当前用户是否已收藏短剧,未登录返回 false
+func queryPlayletIsFavorite(ctx context.Context, userID, playletID int64) (bool, *srvdef.SrvErr) {
+ if userID <= 0 {
+ return false, nil
+ }
+ count, err := playletfavoritemod.Count(ctx, bson.M{playletfavoritemod.FieldUserID: userID, playletfavoritemod.FieldPlayletID: playletID})
+ return count > 0, err
+}
+
+// countPlayletTotalLikeRank 统计累计点赞榜可展示短剧数量
+func countPlayletTotalLikeRank(ctx context.Context) (int64, *srvdef.SrvErr) {
+ rows := make([]playletRankCount, 0)
+ if err := playletmod.AggregatePlaylet(ctx, &rows, playletTotalLikeRankCountPipeline()); err != nil {
+ return 0, err
+ }
+ if len(rows) == 0 {
+ return 0, nil
+ }
+ return rows[0].Total, nil
+}
+
+// aggregatePlayletTotalLikeRank 分页查询累计点赞榜短剧
+func aggregatePlayletTotalLikeRank(ctx context.Context, skip, limit int64) ([]playletRankAggItem, *srvdef.SrvErr) {
+ list := make([]playletRankAggItem, 0)
+ if err := playletmod.AggregatePlaylet(ctx, &list, playletTotalLikeRankPagePipeline(skip, limit)); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// countPlayletPeriodLikeRank 统计周期新增点赞榜可展示短剧数量
+func countPlayletPeriodLikeRank(ctx context.Context, since time.Time) (int64, *srvdef.SrvErr) {
+ rows := make([]playletRankCount, 0)
+ if err := playletlikemod.AggregatePlayletLike(ctx, &rows, playletPeriodLikeRankCountPipeline(since)); err != nil {
+ return 0, err
+ }
+ if len(rows) == 0 {
+ return 0, nil
+ }
+ return rows[0].Total, nil
+}
+
+// aggregatePlayletPeriodLikeRank 分页查询周期新增点赞榜短剧
+func aggregatePlayletPeriodLikeRank(ctx context.Context, since time.Time, skip, limit int64) ([]playletRankAggItem, *srvdef.SrvErr) {
+ list := make([]playletRankAggItem, 0)
+ if err := playletlikemod.AggregatePlayletLike(ctx, &list, playletPeriodLikeRankPagePipeline(since, skip, limit)); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// playletRankPeriodSince 将排行榜周期转换为点赞起始时间
+func playletRankPeriodSince(period proto.PlayletRankPeriod) time.Time {
+ now := time.Now()
+ switch period {
+ case proto.PlayletRankPeriodWeek:
+ return now.AddDate(0, 0, -7)
+ case proto.PlayletRankPeriodMonth:
+ return now.AddDate(0, -1, 0)
+ case proto.PlayletRankPeriodYear:
+ return now.AddDate(-1, 0, 0)
+ default:
+ return time.Time{}
+ }
+}
+
+// playletTotalLikeRankCountPipeline 构建累计点赞榜数量统计管道
+func playletTotalLikeRankCountPipeline() []bson.M {
+ return append(playletTotalLikeRankBasePipeline(), bson.M{"$count": "total"})
+}
+
+// playletTotalLikeRankPagePipeline 构建累计点赞榜分页查询管道
+func playletTotalLikeRankPagePipeline(skip, limit int64) []bson.M {
+ pipeline := playletTotalLikeRankBasePipeline()
+ pipeline = append(pipeline,
+ bson.M{"$sort": bson.D{
+ {Key: "rankScore", Value: -1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }},
+ bson.M{"$skip": skip},
+ bson.M{"$limit": limit},
+ bson.M{"$project": playletRankProjection()},
+ )
+ return pipeline
+}
+
+// playletTotalLikeRankBasePipeline 构建累计点赞榜基础过滤管道
+func playletTotalLikeRankBasePipeline() []bson.M {
+ return []bson.M{
+ {"$match": bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}},
+ {"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.CountLike, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + playletmod.FN.Playlet.VirtualCountLike, 0}},
+ }}}},
+ {"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
+ }
+}
+
+// playletPeriodLikeRankCountPipeline 构建周期新增点赞榜数量统计管道
+func playletPeriodLikeRankCountPipeline(since time.Time) []bson.M {
+ return append(playletPeriodLikeRankBasePipeline(since), bson.M{"$count": "total"})
+}
+
+// playletPeriodLikeRankPagePipeline 构建周期新增点赞榜分页查询管道
+func playletPeriodLikeRankPagePipeline(since time.Time, skip, limit int64) []bson.M {
+ pipeline := playletPeriodLikeRankBasePipeline(since)
+ pipeline = append(pipeline,
+ bson.M{"$sort": bson.D{
+ {Key: "rankScore", Value: -1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }},
+ bson.M{"$skip": skip},
+ bson.M{"$limit": limit},
+ bson.M{"$project": playletRankProjection()},
+ )
+ return pipeline
+}
+
+// playletPeriodLikeRankBasePipeline 构建周期新增点赞榜基础过滤管道
+func playletPeriodLikeRankBasePipeline(since time.Time) []bson.M {
+ return []bson.M{
+ {"$match": bson.M{playletlikemod.FN.PlayletLike.CreatedAt: bson.M{"$gte": since}}},
+ {"$group": bson.M{
+ "_id": "$" + playletlikemod.FN.PlayletLike.PlayletID,
+ "rankScore": bson.M{"$sum": 1},
+ }},
+ {"$lookup": bson.M{
+ "from": playletmod.TN.Playlet,
+ "localField": "_id",
+ "foreignField": playletmod.FN.Playlet.Id,
+ "as": "playlet",
+ }},
+ {"$unwind": "$playlet"},
+ {"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
+ {"$replaceRoot": bson.M{"newRoot": bson.M{"$mergeObjects": []interface{}{"$playlet", bson.M{"rankScore": "$rankScore"}}}}},
+ }
+}
+
+// playletRankProjection 限定排行榜返回字段,减少分页查询的数据传输体积
+func playletRankProjection() bson.M {
+ return bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.Status: 1,
+ playletmod.FN.Playlet.IsRecommend: 1,
+ playletmod.FN.Playlet.OnlineAt: 1,
+ playletmod.FN.Playlet.MediaCreatedAt: 1,
+ playletmod.FN.Playlet.MediaUpdatedAt: 1,
+ playletmod.FN.Playlet.Title: 1,
+ playletmod.FN.Playlet.Slug: 1,
+ playletmod.FN.Playlet.Desc: 1,
+ playletmod.FN.Playlet.Anchor: 1,
+ playletmod.FN.Playlet.Cover: 1,
+ playletmod.FN.Playlet.CompressedCover: 1,
+ playletmod.FN.Playlet.Preview: 1,
+ playletmod.FN.Playlet.CategoryIds: 1,
+ playletmod.FN.Playlet.TagIds: 1,
+ playletmod.FN.Playlet.ThemeIds: 1,
+ playletmod.FN.Playlet.ContentType: 1,
+ playletmod.FN.Playlet.PayType: 1,
+ playletmod.FN.Playlet.SingleCoins: 1,
+ playletmod.FN.Playlet.TotalEpisode: 1,
+ playletmod.FN.Playlet.UpdateStatus: 1,
+ playletmod.FN.Playlet.CountBrowse: 1,
+ playletmod.FN.Playlet.VirtualCountBrowse: 1,
+ playletmod.FN.Playlet.HeatValue: 1,
+ playletmod.FN.Playlet.VirtualHeatValue: 1,
+ playletmod.FN.Playlet.CountCollect: 1,
+ playletmod.FN.Playlet.CountLike: 1,
+ playletmod.FN.Playlet.VirtualCountLike: 1,
+ playletmod.FN.Playlet.SeoInfo.Title: 1,
+ playletmod.FN.Playlet.SeoInfo.Description: 1,
+ playletmod.FN.Playlet.SeoInfo.Keyworld: 1,
+ playletmod.FN.Playlet.CreatedAt: 1,
+ playletmod.FN.Playlet.UpdatedAt: 1,
+ "rankScore": 1,
+ }
+}
+
+// playletRankAggItemsToPlayletList 将排行榜聚合数据转换为短剧列表,复用标签题材批量查询
+func playletRankAggItemsToPlayletList(list []playletRankAggItem) playletmod.PlayletList {
+ result := make(playletmod.PlayletList, 0, len(list))
+ for i := range list {
+ result = append(result, &list[i].Playlet)
+ }
+ return result
+}
+
+// playletRankToData 将排行榜聚合结果转换为榜单展示数据
+func playletRankToData(item playletRankAggItem, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) proto.PlayletRankData {
+ return proto.PlayletRankData{
+ PlayletData: playletToAppData(&item.Playlet, imgCdn, tagNameMap, themeNameMap),
+ RankScore: item.RankScore,
+ }
+}
+
+// playletToAppData 将短剧模型转换为 app 短剧基础数据
+func playletToAppData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) proto.PlayletData {
+ if item == nil {
+ return proto.PlayletData{}
+ }
+ return proto.PlayletData{
+ Id: item.Id,
+ Status: item.Status,
+ IsRecommend: item.IsRecommend,
+ IsNew: isNewPlaylet(item.OnlineAt),
+ IsFeatured: item.IsFeatured,
+ IsOriginal: isOriginalPlaylet(item),
+ ShowOriginal: item.ShowOriginal,
+ OnlineAt: item.OnlineAt,
+ MediaCreatedAt: item.MediaCreatedAt,
+ MediaUpdatedAt: item.MediaUpdatedAt,
+ Title: item.Title,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: netutil.MergeURL(imgCdn, item.DisplayCover()),
+ Preview: netutil.MergeURL(imgCdn, strings.TrimSpace(item.Preview)),
+ TagIds: item.TagIds,
+ TagNames: playletTagNamesFromMap(item.TagIds, tagNameMap),
+ TagSlugs: playletTagSlugsFromMap(item.TagIds, tagNameMap),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: playletThemeNamesFromMap(item.ThemeIds, themeNameMap),
+ ContentType: item.ContentType,
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.DisplayCountBrowse(),
+ HeatValue: item.DisplayHeatValue(),
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ VirtualCountLike: item.VirtualCountLike,
+ DisplayCountLike: item.CountLike + item.VirtualCountLike,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: normalizePlayletSeoValue(item.SeoInfo),
+ }
+}
+
+type playletHistoryRow struct {
+ ChapterID int64 `bson:"chapterId"`
+ Episode int64 `bson:"episode"`
+ UpdatedAt time.Time `bson:"updatedAt"`
+ Playlet playletmod.Playlet `bson:"playlet"`
+}
+
+// ReportPlayletHistory 校验上架短剧和剧集后写入当前用户观看历史
+func ReportPlayletHistory(ctx context.Context, userID int64, req *proto.PlayletHistoryReportReq) (*proto.PlayletHistoryReportResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil || req.PlayletID <= 0 || req.ChapterID <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: req.PlayletID, playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ })
+ if err != nil {
+ return nil, err
+ }
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: req.ChapterID,
+ playletchaptermod.FN.PlayletChapter.PlayletId: req.PlayletID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if !playlet.IsOfficialEpisodeOpen(chapter.CurrentEpisode) {
+ return nil, srvdef.New(srvdef.ErrNoData, "本集暂未在官网开放", "")
+ }
+ if err := ensureOriginalChapterWatermarkReady(playlet, chapter); err != nil {
+ return nil, err
+ }
+ if chapter.CurrentEpisode <= 0 || strings.TrimSpace(chapter.VideoUrl) == "" && strings.TrimSpace(chapter.WatermarkVideoUrl) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "剧集不可播放", "")
+ }
+ now := time.Now().UTC()
+ if err := models.TouchWatchHistory(ctx, models.WatchHistory{
+ UserID: userID, DataType: models.WatchHistoryTypePlaylet,
+ ContentID: req.PlayletID, ChapterID: req.ChapterID, Episode: chapter.CurrentEpisode,
+ CreatedAt: now, UpdatedAt: now,
+ }); err != nil {
+ return nil, err
+ }
+ return &proto.PlayletHistoryReportResp{WatchedAt: now}, nil
+}
+
+// QueryPlayletHistoryList 按最近观看时间倒序分页返回当前用户的上架短剧
+func QueryPlayletHistoryList(ctx context.Context, userID int64, req *proto.PlayletHistoryListReq) (*proto.PlayletHistoryListResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
+ base := []bson.M{
+ {"$match": bson.M{models.WatchHistoryUserID: userID, models.WatchHistoryDataType: models.WatchHistoryTypePlaylet, models.WatchHistoryUpdatedAt: bson.M{"$gte": time.Now().UTC().AddDate(0, 0, -models.WatchHistoryRetentionDays)}}},
+ {"$lookup": bson.M{"from": playletmod.TN.Playlet, "localField": models.WatchHistoryContentID, "foreignField": playletmod.FN.Playlet.Id, "as": "playlet"}},
+ {"$unwind": "$playlet"},
+ {"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
+ }
+ countRows := make([]struct {
+ Total int64 `bson:"total"`
+ }, 0)
+ if err := models.AggregateWatchHistory(ctx, &countRows, append(append([]bson.M(nil), base...), bson.M{"$count": "total"})); err != nil {
+ return nil, err
+ }
+ total := int64(0)
+ if len(countRows) > 0 {
+ total = countRows[0].Total
+ }
+ resp := &proto.PlayletHistoryListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletHistoryItem, 0),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ sortStage := bson.M{"$sort": bson.D{{Key: models.WatchHistoryUpdatedAt, Value: -1}, {Key: "_id", Value: -1}}}
+ pipeline := append([]bson.M{base[0], sortStage}, base[1:]...)
+ pipeline = append(pipeline, bson.M{"$skip": (pageNum - 1) * pageSize}, bson.M{"$limit": pageSize})
+ rows := make([]playletHistoryRow, 0)
+ if err := models.AggregateWatchHistory(ctx, &rows, pipeline); err != nil {
+ return nil, err
+ }
+ list := make(playletmod.PlayletList, 0, len(rows))
+ for i := range rows {
+ list = append(list, &rows[i].Playlet)
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ cdn := configser.GetRequestConfig(ctx).ImgCdn
+ items := make([]*proto.PlayletHistoryItem, 0, len(rows))
+ for _, row := range rows {
+ items = append(items, &proto.PlayletHistoryItem{
+ PlayletData: playletToAppData(&row.Playlet, cdn, tagNames, themeNames),
+ ChapterID: row.ChapterID, Episode: row.Episode, WatchedAt: row.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// ClearPlayletHistory 清空当前用户的短剧观看历史
+func ClearPlayletHistory(ctx context.Context, userID int64) (*proto.PlayletHistoryClearResp, *srvdef.SrvErr) {
+ if userID <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "用户无效", "")
+ }
+ count, err := models.DeleteWatchHistory(ctx, userID, models.WatchHistoryTypePlaylet)
+ if err != nil {
+ return nil, err
+ }
+ return &proto.PlayletHistoryClearResp{DeletedCount: count}, nil
+}
+
+// buildPlayletTagThemeNameMaps 批量查询当前页短剧标签和题材名称,避免列表循环内多次访问数据库
+func buildPlayletTagThemeNameMaps(ctx context.Context, list playletmod.PlayletList) (contenttagmod.ContentTagMap, map[int64]string, *srvdef.SrvErr) {
+ tagIds := uniquePlayletRelationIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.TagIds
+ })
+ themeIds := uniquePlayletRelationIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.ThemeIds
+ })
+ tagNameMap, err := buildPlayletTagNameMap(ctx, tagIds)
+ if err != nil {
+ return nil, nil, err
+ }
+ themeNameMap, err := buildPlayletThemeNameMap(ctx, themeIds)
+ if err != nil {
+ return nil, nil, err
+ }
+ return tagNameMap, themeNameMap, nil
+}
+
+// uniquePlayletRelationIds 按短剧关联字段收集去重后的ID
+func uniquePlayletRelationIds(list playletmod.PlayletList, values func(item *playletmod.Playlet) []int64) []int64 {
+ ids := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, id := range values(item) {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ }
+ return ids
+}
+
+// buildPlayletTagNameMap 批量查询当前页短剧标签名称
+func buildPlayletTagNameMap(ctx context.Context, tagIds []int64) (contenttagmod.ContentTagMap, *srvdef.SrvErr) {
+ result := make(contenttagmod.ContentTagMap, len(tagIds))
+ if len(tagIds) == 0 {
+ return result, nil
+ }
+ return contenttagmod.GetMapTags(ctx, tagIds)
+}
+
+// buildPlayletThemeNameMap 批量查询当前页短剧题材名称
+func buildPlayletThemeNameMap(ctx context.Context, themeIds []int64) (map[int64]string, *srvdef.SrvErr) {
+ result := make(map[int64]string, len(themeIds))
+ if len(themeIds) == 0 {
+ return result, nil
+ }
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds},
+ }, options.Find().SetProjection(bson.M{
+ playletthememod.FN.PlayletTheme.Id: 1,
+ playletthememod.FN.PlayletTheme.Name: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, theme := range themes {
+ if theme != nil {
+ result[theme.Id] = theme.Name
+ }
+ }
+ return result, nil
+}
+
+// playletTagNamesFromMap 按短剧标签ID原始顺序组装标签名称
+func playletTagNamesFromMap(tagIds []int64, tagNameMap contenttagmod.ContentTagMap) []string {
+ result := make([]string, 0, len(tagIds))
+ for _, tagId := range tagIds {
+ if tag := tagNameMap[tagId]; tag != nil && tag.Name != "" {
+ result = append(result, tag.Name)
+ }
+ }
+ return result
+}
+
+// playletTagSlugsFromMap 按标签名称的顺序组装slug
+func playletTagSlugsFromMap(tagIds []int64, tagNameMap contenttagmod.ContentTagMap) []string {
+ result := make([]string, 0, len(tagIds))
+ for _, tagId := range tagIds {
+ if tag := tagNameMap[tagId]; tag != nil && tag.Name != "" {
+ result = append(result, tag.Slug)
+ }
+ }
+ return result
+}
+
+// playletThemeNamesFromMap 按短剧题材ID原始顺序组装题材名称
+func playletThemeNamesFromMap(themeIds []int64, themeNameMap map[int64]string) []string {
+ result := make([]string, 0, len(themeIds))
+ for _, themeId := range themeIds {
+ if name := themeNameMap[themeId]; name != "" {
+ result = append(result, name)
+ }
+ }
+ return result
+}
+
+// isNewPlaylet 判断短剧是否为本周内上架
+func isNewPlaylet(onlineAt time.Time) bool {
+ if onlineAt.IsZero() {
+ return false
+ }
+ return onlineAt.After(utils.BeginOfWeekByTime(utils.Now()))
+}
+
+// normalizePlayletSeoValue 规范化 SEO 字段,避免 keywords 为 null
+func normalizePlayletSeoValue(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
+}
+
+// playletRequestPath 从上下文中提取当前请求路径,避免API层重复组装短剧响应字段
+func playletRequestPath(ctx context.Context) string {
+ if ginCtx, ok := ctx.(*gin.Context); ok && ginCtx.Request != nil && ginCtx.Request.URL != nil {
+ return ginCtx.Request.URL.Path
+ }
+ return ""
+}
+
+// emptyPlayletRecommendListPage 构造 app 推荐短剧空分页响应
+func emptyPlayletRecommendListPage(pageNum, pageSize int64) *proto.PlayletRecommendListResp {
+ return &proto.PlayletRecommendListResp{BasePageResp: response.BasePageResp{
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletRecommendListItem, 0),
+ }}
+}
+
+const (
+ playletHomeBannerLimit int64 = 5
+ playletHomeModuleTopicLimit int64 = 4
+ playletHomeTopicLimit int64 = 3
+ playletHomeTopicItems int64 = 3
+)
+
+// buildHomePlayletResponse 查询并组装短剧首页数据
+func buildHomePlayletResponse(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
+ cfg := configser.GetRequestConfig(ctx)
+ // 广告和 SEO 使用当前请求的站点配置
+ seo := pubdef.SeoValue{Title: cfg.WebTitle, Keyworld: cfg.WebKeyworld, Description: cfg.WebDescription}
+ resp := &proto.HomePlayletResponse{
+ HomeBannerData: make([]*proto.HomeBannerData, 0),
+ Modules: make([]*proto.HomeModuleData, 0),
+ Announcements: make([]*proto.AnnouncementItem, 0),
+ Advitise: configser.GetAdvitise(),
+ SeoInfo: &seo,
+ }
+ // Banner 只取已上架且开启首页展示的短剧,优先展示置顶权重高的内容
+ banners, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ playletmod.FN.Playlet.ShowOnBanner: true,
+ }, options.Find().SetSort(bson.D{
+ {Key: playletmod.FN.Playlet.TopRank, Value: -1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }).SetLimit(playletHomeBannerLimit))
+ if err != nil {
+ return nil, err
+ }
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, banners)
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range banners {
+ if item == nil {
+ continue
+ }
+ resp.HomeBannerData = append(resp.HomeBannerData, &proto.HomeBannerData{
+ PlayletData: playletToAppData(item, cfg.ImgCdn, tagNames, themeNames),
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: netutil.MergeURL(cfg.ImgCdn, item.Banner),
+ })
+ }
+ // 首页模块仅使用已启用的配置,排序值越大越靠前
+ configs, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx,
+ bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true},
+ options.Find().SetSort(bson.D{
+ {Key: playlethomeconfigmod.FN.PlayletHomeConfig.Rank, Value: -1},
+ {Key: playlethomeconfigmod.FN.PlayletHomeConfig.Id, Value: -1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range configs {
+ if item == nil {
+ continue
+ }
+ module, err := homeModule(ctx, item, cfg.ImgCdn)
+ if err != nil {
+ return nil, err
+ }
+ resp.Modules = append(resp.Modules, module)
+ }
+ // 公告只返回启用项,先按排序值再按修改时间倒序排列
+ announcements, err := announcementmod.FindManyAnnouncement(ctx,
+ bson.M{announcementmod.FN.Announcement.Status: true},
+ options.Find().SetSort(bson.D{
+ {Key: announcementmod.FN.Announcement.Sort, Value: -1},
+ {Key: announcementmod.FN.Announcement.UpdatedAt, Value: -1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range announcements {
+ if item != nil {
+ resp.Announcements = append(resp.Announcements, &proto.AnnouncementItem{Id: item.Id, Title: item.Title, Content: item.Content})
+ }
+ }
+ return resp, nil
+}
+
+// homeModule 按配置的数据类型和模块类型组装首页模块
+func homeModule(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string) (*proto.HomeModuleData, *srvdef.SrvErr) {
+ // 未设置条数时使用默认值,并限制最大查询量
+ limit := config.Limit
+ if limit <= 0 {
+ limit = playlethomeconfigmod.DefaultLimit
+ }
+ if limit > playlethomeconfigmod.MaxLimit {
+ limit = playlethomeconfigmod.MaxLimit
+ }
+ module := &proto.HomeModuleData{
+ Id: config.Id,
+ Slug: config.Slug,
+ Title: config.Title,
+ Desc: config.Desc,
+ Label: config.Label,
+ Style: config.Style,
+ CardBackgroundImage: netutil.MergeURL(imgCdn, config.CardBackgroundImage),
+ ModuleType: config.ModuleType,
+ DataType: config.DataType,
+ RecommendType: config.RecommendType,
+ CategoryId: config.CategoryId,
+ Rank: config.Rank,
+ List: make([]*proto.PlayletData, 0),
+ TopicList: make([]*proto.HomeTopicData, 0),
+ }
+ // 专题模块按数据类型分别使用视频分类或短剧题材
+ if config.DataType == playlethomeconfigmod.DataTypeVideo {
+ if config.ModuleType == playlethomeconfigmod.ModuleTypeTopic {
+ topics, err := homeVideoTopics(ctx, config, imgCdn)
+ module.TopicList = topics
+ return module, err
+ }
+ list, err := homeVideos(ctx, config, imgCdn, limit, 0, 0)
+ module.List = list
+ return module, err
+ }
+ if config.ModuleType == playlethomeconfigmod.ModuleTypeTopic {
+ topics, err := homeTopics(ctx, config, imgCdn)
+ module.TopicList = topics
+ return module, err
+ }
+ list, err := homePlaylets(ctx, config, 0, limit)
+ if err != nil {
+ return nil, err
+ }
+ module.List, err = homePlayletData(ctx, list, imgCdn)
+ return module, err
+}
+
+// homePlaylets 查询已上架短剧,按需叠加分类和专题筛选
+func homePlaylets(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, themeId, limit int64) (playletmod.PlayletList, *srvdef.SrvErr) {
+ filter, ok := homePlayletFilter(config, themeId)
+ if !ok {
+ return make(playletmod.PlayletList, 0), nil
+ }
+ if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
+ return playletmod.FindManyPlayletByCategoryRank(ctx, filter, config.CategoryId, 0, limit, playletmod.CategoryRankSort(), nil)
+ }
+ return findPlayletRecommendPage(ctx, filter, homePlayletSort(config.RecommendType), 0, limit)
+}
+
+// homePlayletSort 将推荐类型映射到短剧排序字段,ID 用于稳定同分排序
+func homePlayletSort(recommendType int64) bson.D {
+ var primary string
+ switch recommendType {
+ case playlethomeconfigmod.RecommendTypeUpdated:
+ primary = playletmod.FN.Playlet.UpdatedAt
+ case playlethomeconfigmod.RecommendTypeLike:
+ primary = playletmod.FN.Playlet.CountLike
+ case playlethomeconfigmod.RecommendTypeHeat:
+ primary = playletmod.FN.Playlet.HeatValue
+ case playlethomeconfigmod.RecommendTypeCategory:
+ primary = playletmod.FN.Playlet.RecommendRank
+ default:
+ primary = playletmod.FN.Playlet.CountBrowse
+ }
+ return bson.D{{Key: primary, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
+}
+
+// homePlayletData 补齐标签和专题名称并转换为 App 短剧结构
+func homePlayletData(ctx context.Context, list playletmod.PlayletList, imgCdn string) ([]*proto.PlayletData, *srvdef.SrvErr) {
+ result := make([]*proto.PlayletData, 0, len(list))
+ tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ data := playletToAppData(item, imgCdn, tagNames, themeNames)
+ result = append(result, &data)
+ }
+ return result, nil
+}
+
+// homeVideos 查询审核通过的视频,推荐类型决定排序或分类筛选
+func homeVideos(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string, limit, skip, categoryId int64) ([]*proto.HomeVideoData, *srvdef.SrvErr) {
+ result := make([]*proto.HomeVideoData, 0)
+ filter, ok := homeVideoFilter(config, categoryId)
+ if !ok {
+ return result, nil
+ }
+ sortKey := videoinformationmod.FN.VideoInformation.Real.WatchTimes
+ switch config.RecommendType {
+ case playlethomeconfigmod.RecommendTypeUpdated:
+ sortKey = videoinformationmod.FN.VideoInformation.UpdatedAt
+ case playlethomeconfigmod.RecommendTypeLike:
+ sortKey = videoinformationmod.FN.VideoInformation.Real.Likes
+ case playlethomeconfigmod.RecommendTypeHeat:
+ sortKey = videoinformationmod.FN.VideoInformation.HeatValue
+ case playlethomeconfigmod.RecommendTypeCategory:
+ sortKey = videoinformationmod.FN.VideoInformation.Sort
+ }
+ sort := bson.D{{Key: sortKey, Value: -1}}
+ if config.RecommendType == playlethomeconfigmod.RecommendTypeHeat {
+ sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1})
+ }
+ sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1})
+ projection := bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Desc, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Fake.Likes, Value: 1},
+ }
+ var list videoinformationmod.VideoInformationList
+ var err *srvdef.SrvErr
+ if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
+ selectedCategoryId := config.CategoryId
+ if categoryId > 0 {
+ selectedCategoryId = categoryId
+ }
+ project := bson.M{}
+ for _, field := range projection {
+ project[field.Key] = field.Value
+ }
+ list, err = videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, selectedCategoryId, skip, limit, project)
+ } else {
+ list, err = videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().SetProjection(projection).SetSort(sort).SetSkip(skip).SetLimit(limit))
+ }
+ if err != nil {
+ return nil, err
+ }
+ // 展示数值为真实统计与后台设置的虚拟统计之和
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, &proto.HomeVideoData{
+ Id: item.Id, Title: item.Title, Slug: item.Slug,
+ Cover: netutil.MergeURL(imgCdn, item.CoverImg), Desc: item.Desc,
+ Tags: item.Tags, WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ })
+ }
+ return result, nil
+}
+
+// homeVideoTopics 从配置的视频分类中按视频数量选取前四个有内容的专题
+func homeVideoTopics(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string) ([]*proto.HomeTopicData, *srvdef.SrvErr) {
+ topics, _, err := homeTopicPage(ctx, config, imgCdn, 0, playletHomeModuleTopicLimit, 0, true, true)
+ return topics, err
+}
+
+// homeTopics 从配置的题材中按上架短剧数量选取前四个专题,每个专题最多取三部短剧
+func homeTopics(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string) ([]*proto.HomeTopicData, *srvdef.SrvErr) {
+ topics, _, err := homeTopicPage(ctx, config, imgCdn, 0, playletHomeModuleTopicLimit, 0, true, true)
+ return topics, err
+}
+
+const (
+ homeConfigPageSize int64 = 20
+ homeTopicPageSize int64 = 12
+ homePageMaxSize int64 = 100
+ homePlayletCacheTTL = 5 * time.Minute
+)
+
+// HomeConfigList 按首页配置分页查询短剧、视频或专题
+func HomeConfigList(ctx context.Context, req *proto.PlayletHomeConfigListReq) (*proto.PlayletHomeConfigListResp, *srvdef.SrvErr) {
+ if req == nil || (req.Id <= 0 && strings.TrimSpace(req.Slug) == "") {
+ return nil, srvdef.New(srvdef.ErrParamError, "首页配置ID或slug不能为空", "")
+ }
+ filter := bson.M{}
+ if slug := strings.TrimSpace(req.Slug); slug != "" {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Slug] = slug
+ } else {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Id] = req.Id
+ }
+ config, err := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ pageNum, pageSize := normalizeHomePage(req.PageNum, req.PageSize, homeConfigPageSize)
+ resp := &proto.PlayletHomeConfigListResp{
+ Config: &proto.HomeModuleData{
+ Id: config.Id, Slug: config.Slug, Title: config.Title, Desc: config.Desc, Label: config.Label,
+ Style: config.Style, CardBackgroundImage: netutil.MergeURL(configser.GetRequestConfig(ctx).ImgCdn, config.CardBackgroundImage),
+ ModuleType: config.ModuleType, DataType: config.DataType, RecommendType: config.RecommendType,
+ CategoryId: config.CategoryId, Rank: config.Rank,
+ List: make([]*proto.PlayletData, 0), TopicList: make([]*proto.HomeTopicData, 0),
+ },
+ PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletData, 0),
+ TopicList: make([]*proto.HomeTopicData, 0), Advitise: configser.GetAdvitise(),
+ }
+ if !config.Enabled {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ imgCdn := configser.GetRequestConfig(ctx).ImgCdn
+ if config.ModuleType == playlethomeconfigmod.ModuleTypeTopic {
+ resp.TopicList, resp.TotalCount, err = homeTopicPage(ctx, config, imgCdn, skip, pageSize, 0, true, true)
+ return resp, err
+ }
+ if config.DataType == playlethomeconfigmod.DataTypeVideo {
+ filter, ok := homeVideoFilter(config, 0)
+ if !ok {
+ resp.List = make([]*proto.HomeVideoData, 0)
+ return resp, nil
+ }
+ resp.TotalCount, err = videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp.List, err = homeVideos(ctx, config, imgCdn, pageSize, skip, 0)
+ return resp, err
+ }
+ filter, ok := homePlayletFilter(config, 0)
+ if !ok {
+ return resp, nil
+ }
+ resp.TotalCount, err = playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ var list playletmod.PlayletList
+ if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
+ list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, config.CategoryId, skip, pageSize, playletmod.CategoryRankSort(), nil)
+ } else {
+ list, err = findPlayletRecommendPage(ctx, filter, homePlayletSort(config.RecommendType), skip, pageSize)
+ }
+ if err != nil {
+ return nil, err
+ }
+ resp.List, err = homePlayletData(ctx, list, imgCdn)
+ return resp, err
+}
+
+// TopicDetail 根据数据类型查询短剧题材或视频分类专题详情
+func TopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq) (*proto.PlayletTopicDetailResp, *srvdef.SrvErr) {
+ if req == nil || (req.Id <= 0 && strings.TrimSpace(req.TopicSlug) == "") {
+ return nil, srvdef.New(srvdef.ErrParamError, "专题ID或slug不能为空", "")
+ }
+ dataType := req.DataType
+ if dataType == 0 {
+ dataType = playlethomeconfigmod.DataTypeAll
+ }
+ if dataType != playlethomeconfigmod.DataTypeAll && dataType != playlethomeconfigmod.DataTypeVideo {
+ return nil, srvdef.New(srvdef.ErrParamError, "数据类型无效", "")
+ }
+ pageNum, pageSize := normalizeHomePage(req.PageNum, req.PageSize, homeTopicPageSize)
+ imgCdn := configser.GetRequestConfig(ctx).ImgCdn
+ resp := &proto.PlayletTopicDetailResp{
+ DataType: dataType, PageNum: int(pageNum), PageSize: int(pageSize),
+ Covers: make([]string, 0), List: make([]*proto.PlayletData, 0),
+ MoreTopics: make([]*proto.HomeTopicData, 0), Advitise: configser.GetAdvitise(),
+ }
+ skip := (pageNum - 1) * pageSize
+ var err *srvdef.SrvErr
+ if dataType == playlethomeconfigmod.DataTypeVideo {
+ err = fillVideoTopicDetail(ctx, req, resp, imgCdn, skip, pageSize)
+ } else {
+ err = fillPlayletTopicDetail(ctx, req, resp, imgCdn, skip, pageSize)
+ }
+ if err != nil {
+ return nil, err
+ }
+ config := &playlethomeconfigmod.PlayletHomeConfig{DataType: dataType, RecommendType: playlethomeconfigmod.RecommendTypeHeat}
+ resp.MoreTopics, _, err = homeTopicPage(ctx, config, imgCdn, 0, playletHomeTopicLimit, resp.Id, true, false)
+ return resp, err
+}
+
+func fillPlayletTopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq, resp *proto.PlayletTopicDetailResp, imgCdn string, skip, pageSize int64) *srvdef.SrvErr {
+ filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
+ if slug := strings.TrimSpace(req.TopicSlug); slug != "" {
+ filter[playletthememod.FN.PlayletTheme.Slug] = slug
+ } else {
+ filter[playletthememod.FN.PlayletTheme.Id] = req.Id
+ }
+ theme, err := playletthememod.FindOnePlayletTheme(ctx, filter)
+ if err != nil {
+ return err
+ }
+ resp.Id, resp.Name, resp.Slug, resp.Rank = theme.Id, theme.Name, theme.Slug, theme.Rank
+ resp.Desc, resp.Icon = theme.SeoInfo.Description, netutil.MergeURL(imgCdn, theme.Icon)
+ seo := theme.SeoInfo
+ resp.SeoInfo = &seo
+ playletFilter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp, playletmod.FN.Playlet.ThemeIds: theme.Id}
+ resp.TotalCount, err = playletmod.CountPlaylet(ctx, playletFilter)
+ if err != nil {
+ return err
+ }
+ resp.PlayletCount = resp.TotalCount
+ sortOrder := homeTopicDetailPlayletSort(req.SortType)
+ covers, err := findPlayletRecommendPage(ctx, playletFilter, homeTopicDetailPlayletSort(1), 0, playletHomeTopicItems)
+ if err != nil {
+ return err
+ }
+ for _, item := range covers {
+ if item != nil && item.Cover != "" {
+ resp.Covers = append(resp.Covers, netutil.MergeURL(imgCdn, item.Cover))
+ }
+ }
+ list, err := findPlayletRecommendPage(ctx, playletFilter, sortOrder, skip, pageSize)
+ if err != nil {
+ return err
+ }
+ resp.List, err = homePlayletData(ctx, list, imgCdn)
+ return err
+}
+
+func fillVideoTopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq, resp *proto.PlayletTopicDetailResp, imgCdn string, skip, pageSize int64) *srvdef.SrvErr {
+ filter := bson.M{videocategorymod.FN.VideoCategory.Status: true}
+ if slug := strings.TrimSpace(req.TopicSlug); slug != "" {
+ filter[videocategorymod.FN.VideoCategory.Slug] = slug
+ } else {
+ filter[videocategorymod.FN.VideoCategory.Id] = req.Id
+ }
+ category, err := videocategorymod.FindOneVideoCategory(ctx, filter)
+ if err != nil {
+ return err
+ }
+ resp.Id, resp.Name, resp.Slug, resp.Rank = category.Id, category.Name, category.Slug, category.Rank
+ resp.Desc = category.Desc
+ seo := category.SeoInfo
+ resp.SeoInfo = &seo
+ videoFilter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.CategoryId: category.Id}
+ resp.TotalCount, err = videoinformationmod.CountVideoInformations(ctx, videoFilter)
+ if err != nil {
+ return err
+ }
+ resp.VideoCount = resp.TotalCount
+ config := &playlethomeconfigmod.PlayletHomeConfig{RecommendType: homeTopicDetailVideoRecommendType(req.SortType)}
+ covers, err := homeVideos(ctx, &playlethomeconfigmod.PlayletHomeConfig{RecommendType: playlethomeconfigmod.RecommendTypeHeat}, imgCdn, playletHomeTopicItems, 0, category.Id)
+ if err != nil {
+ return err
+ }
+ for _, item := range covers {
+ if item != nil && item.Cover != "" {
+ resp.Covers = append(resp.Covers, item.Cover)
+ }
+ }
+ resp.List, err = homeVideos(ctx, config, imgCdn, pageSize, skip, category.Id)
+ return err
+}
+
+func homeTopicDetailPlayletSort(sortType int64) bson.D {
+ switch sortType {
+ case 2:
+ return bson.D{{Key: playletmod.FN.Playlet.OnlineAt, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
+ case 3:
+ return bson.D{{Key: playletmod.FN.Playlet.HeatValue, Value: -1}, {Key: playletmod.FN.Playlet.CountBrowse, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
+ default:
+ return bson.D{{Key: playletmod.FN.Playlet.RecommendRank, Value: -1}, {Key: playletmod.FN.Playlet.Id, Value: -1}}
+ }
+}
+
+func homeTopicDetailVideoRecommendType(sortType int64) int64 {
+ switch sortType {
+ case 2:
+ return playlethomeconfigmod.RecommendTypeUpdated
+ case 3:
+ return playlethomeconfigmod.RecommendTypeBrowse
+ default:
+ return playlethomeconfigmod.RecommendTypeCategory
+ }
+}
+
+func normalizeHomePage(pageNum, pageSize, defaultSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultSize
+ }
+ if pageSize > homePageMaxSize {
+ pageSize = homePageMaxSize
+ }
+ return pageNum, pageSize
+}
+
+func homePlayletFilter(config *playlethomeconfigmod.PlayletHomeConfig, themeId int64) (bson.M, bool) {
+ filter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
+ if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
+ if config.CategoryId <= 0 {
+ return nil, false
+ }
+ filter[playletmod.FN.Playlet.CategoryIds] = config.CategoryId
+ }
+ if themeId > 0 {
+ filter[playletmod.FN.Playlet.ThemeIds] = themeId
+ }
+ return filter, true
+}
+
+func homeVideoFilter(config *playlethomeconfigmod.PlayletHomeConfig, categoryId int64) (bson.M, bool) {
+ filter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}
+ if categoryId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CategoryId] = categoryId
+ } else if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
+ if config.CategoryId <= 0 {
+ return nil, false
+ }
+ filter[videoinformationmod.FN.VideoInformation.CategoryId] = config.CategoryId
+ }
+ return filter, true
+}
+
+// homeTopicPage 按专题排序分页,可分别限定有上架内容和首页配置关联的专题
+func homeTopicPage(ctx context.Context, config *playlethomeconfigmod.PlayletHomeConfig, imgCdn string, skip, limit, excludeId int64, onlyWithContent, onlyConfigured bool) ([]*proto.HomeTopicData, int64, *srvdef.SrvErr) {
+ type topicStat struct {
+ id, rank, count int64
+ name, slug, icon string
+ seo pubdef.SeoValue
+ }
+ stats := make([]topicStat, 0)
+ selectedTopics := make(map[int64]struct{}, len(config.TopicIds))
+ if onlyConfigured {
+ for _, id := range config.TopicIds {
+ selectedTopics[id] = struct{}{}
+ }
+ if len(selectedTopics) == 0 {
+ return make([]*proto.HomeTopicData, 0), 0, nil
+ }
+ }
+ if config.DataType == playlethomeconfigmod.DataTypeVideo {
+ filter := bson.M{videocategorymod.FN.VideoCategory.Status: true}
+ if onlyConfigured {
+ filter[videocategorymod.FN.VideoCategory.Id] = bson.M{"$in": config.TopicIds}
+ }
+ if config.RecommendType == playlethomeconfigmod.RecommendTypeCategory {
+ if config.CategoryId <= 0 {
+ return make([]*proto.HomeTopicData, 0), 0, nil
+ }
+ filter[videocategorymod.FN.VideoCategory.Id] = config.CategoryId
+ }
+ categories, err := videocategorymod.FindManyVideoCategorys(ctx, filter)
+ if err != nil {
+ return nil, 0, err
+ }
+ for _, category := range categories {
+ if category == nil || category.Id == excludeId {
+ continue
+ }
+ if onlyConfigured {
+ if _, ok := selectedTopics[category.Id]; !ok {
+ continue
+ }
+ }
+ count, err := videoinformationmod.CountVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.CategoryId: category.Id,
+ })
+ if err != nil {
+ return nil, 0, err
+ }
+ if onlyWithContent && count == 0 {
+ continue
+ }
+ stats = append(stats, topicStat{id: category.Id, rank: category.Rank, count: count,
+ name: category.Name, slug: category.Slug, seo: category.SeoInfo})
+ }
+ } else {
+ filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
+ if onlyConfigured {
+ filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$in": config.TopicIds}
+ }
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, filter)
+ if err != nil {
+ return nil, 0, err
+ }
+ for _, theme := range themes {
+ if theme == nil || theme.Id == excludeId {
+ continue
+ }
+ if onlyConfigured {
+ if _, ok := selectedTopics[theme.Id]; !ok {
+ continue
+ }
+ }
+ filter, ok := homePlayletFilter(config, theme.Id)
+ if !ok {
+ continue
+ }
+ count, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, 0, err
+ }
+ if onlyWithContent && count == 0 {
+ continue
+ }
+ stats = append(stats, topicStat{id: theme.Id, rank: theme.Rank, count: count,
+ name: theme.Name, slug: theme.Slug, icon: theme.Icon, seo: theme.SeoInfo})
+ }
+ }
+ sort.Slice(stats, func(i, j int) bool {
+ if stats[i].count != stats[j].count {
+ return stats[i].count > stats[j].count
+ }
+ if stats[i].rank != stats[j].rank {
+ return stats[i].rank > stats[j].rank
+ }
+ return stats[i].id > stats[j].id
+ })
+ total := int64(len(stats))
+ result := make([]*proto.HomeTopicData, 0)
+ if skip >= total || limit <= 0 {
+ return result, total, nil
+ }
+ end := skip + limit
+ if end > total {
+ end = total
+ }
+ if onlyWithContent {
+ end = total // 被排除的内容可能让专题变空,继续查后续专题补足展示数量
+ }
+ for _, stat := range stats[int(skip):int(end)] {
+ if onlyWithContent && int64(len(result)) >= limit {
+ break
+ }
+ item := &proto.HomeTopicData{Id: stat.id, Name: stat.name, Slug: stat.slug,
+ Icon: netutil.MergeURL(imgCdn, stat.icon), Rank: stat.rank, SeoInfo: stat.seo}
+ if config.DataType == playlethomeconfigmod.DataTypeVideo {
+ item.VideoCount = stat.count
+ list, err := homeVideos(ctx, config, imgCdn, playletHomeTopicItems, 0, stat.id)
+ if err != nil {
+ return nil, 0, err
+ }
+ item.List = list
+ if onlyWithContent && len(list) == 0 {
+ continue
+ }
+ } else {
+ item.PlayletCount = stat.count
+ list, err := homePlaylets(ctx, config, stat.id, playletHomeTopicItems)
+ if err != nil {
+ return nil, 0, err
+ }
+ if onlyWithContent && len(list) == 0 {
+ continue
+ }
+ item.List, err = homePlayletData(ctx, list, imgCdn)
+ if err != nil {
+ return nil, 0, err
+ }
+ }
+ result = append(result, item)
+ }
+ return result, total, nil
+}
+
+// Home 优先返回 Redis 缓存,未命中或缓存不可用时查询数据库
+func Home(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
+ key := homePlayletCacheKey(configser.GetRequestConfig(ctx).ImgCdn)
+ if resp := getHomePlayletCache(key); resp != nil {
+ return resp, nil
+ }
+ resp, err := buildHomePlayletResponse(ctx)
+ if err != nil {
+ return nil, err
+ }
+ setHomePlayletCache(key, resp)
+ return resp, nil
+}
+
+// homePlayletCacheKey 包含缓存版本和图片域名,后台变更或域名变更后使用新键
+func homePlayletCacheKey(imgCdn string) string {
+ if redis.Client() == nil {
+ return ""
+ }
+ version := "0"
+ value, err := redis.Get(redisconst.AppPlayletHomeCacheVersionKey())
+ if err != nil {
+ log.Warn("读取短剧首页缓存版本失败", log.E(err))
+ return ""
+ }
+ if value != nil && strings.TrimSpace(*value) != "" {
+ version = strings.TrimSpace(*value)
+ }
+ cdnHash := sha256.Sum256([]byte(imgCdn))
+ return fmt.Sprintf("app:playlet:home:data:%d:v7:%s:%x", pubdef.AppId, version, cdnHash[:8])
+}
+
+// getHomePlayletCache 读取并反序列化完整首页响应
+func getHomePlayletCache(key string) *proto.HomePlayletResponse {
+ if key == "" {
+ return nil
+ }
+ value, err := redis.Get(key)
+ if err != nil {
+ log.Warn("读取短剧首页缓存失败", log.E(err))
+ return nil
+ }
+ if value == nil || *value == "" {
+ return nil
+ }
+ var resp proto.HomePlayletResponse
+ if err := json.Unmarshal([]byte(*value), &resp); err != nil {
+ log.Warn("解析短剧首页缓存失败", log.E(err))
+ return nil
+ }
+ return &resp
+}
+
+// setHomePlayletCache 缓存完整首页响应五分钟
+func setHomePlayletCache(key string, resp *proto.HomePlayletResponse) {
+ if key == "" || resp == nil {
+ return
+ }
+ data, err := json.Marshal(resp)
+ if err != nil {
+ log.Warn("序列化短剧首页缓存失败", log.E(err))
+ return
+ }
+ if err := redis.Set(key, string(data), homePlayletCacheTTL); err != nil {
+ log.Warn("写入短剧首页缓存失败", log.E(err))
+ }
+}
diff --git a/app/service/playletser/playlet_content_list_test.go b/app/service/playletser/playlet_content_list_test.go
new file mode 100644
index 0000000..6add609
--- /dev/null
+++ b/app/service/playletser/playlet_content_list_test.go
@@ -0,0 +1,137 @@
+package playletser
+
+import (
+ "context"
+ "testing"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/moreser"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func TestPlayletContentListRejectsInvalidDataType(t *testing.T) {
+ for _, dataType := range []int64{0, 3, -1} {
+ if _, err := ContentList(context.Background(), &proto.PlayletContentListReq{DataType: dataType}); err == nil {
+ t.Errorf("dataType %d 未返回参数错误", dataType)
+ }
+ }
+ if _, err := ContentList(context.Background(), nil); err == nil {
+ t.Fatal("空请求未返回参数错误")
+ }
+}
+
+func TestPlayletContentWatchSort(t *testing.T) {
+ playletSort := playletContentPlayletSort()
+ if playletSort[0].Key != playletmod.FN.Playlet.CountBrowse || playletSort[0].Value != -1 {
+ t.Fatalf("短剧未优先按观看量排序: %v", playletSort)
+ }
+ pipeline := playletContentVideoPipeline(7, 10)
+ if pipeline[0]["$match"].(bson.M)[videoinformationmod.FN.VideoInformation.Status] == nil {
+ t.Fatalf("视频未限制上架状态: %v", pipeline[0])
+ }
+ fields := pipeline[1]["$addFields"].(bson.M)
+ watchSum := fields["watchScore"].(bson.M)["$add"].([]interface{})
+ if len(watchSum) != 2 {
+ t.Fatalf("视频观看量未包含真实数和后台配置数: %v", fields)
+ }
+ for i, field := range []string{
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes,
+ } {
+ value := watchSum[i].(bson.M)["$ifNull"].([]interface{})
+ if value[0] != "$"+field || value[1] != 0 {
+ t.Fatalf("视频观看量字段 %d 错误: %v", i, value)
+ }
+ }
+ sort := pipeline[2]["$sort"].(bson.D)
+ if sort[0].Key != "watchScore" || sort[0].Value != -1 ||
+ sort[1].Key != videoinformationmod.FN.VideoInformation.UpdatedAt ||
+ sort[2].Key != videoinformationmod.FN.VideoInformation.Id {
+ t.Fatalf("视频未优先按展示观看量排序: %v", sort)
+ }
+ if pipeline[3]["$skip"] != int64(7) || pipeline[4]["$limit"] != int64(10) {
+ t.Fatalf("视频分页位置错误: %v", pipeline)
+ }
+}
+
+func TestPlayletContentListMixedPagination(t *testing.T) {
+ for _, dataType := range []int64{1, 2} {
+ const contentCount int64 = 14
+ ads := []proxy.AdvitiseInfo{{Id: "ad-1"}, {Id: "ad-2"}}
+ interval := playletContentAdInterval(dataType)
+ adCount := playletContentInsertedAdsCount(contentCount, len(ads), interval)
+ wantAdCount := int64(3)
+ if dataType == 2 {
+ wantAdCount = 2
+ }
+ if adCount != wantAdCount {
+ t.Fatalf("dataType %d 广告位数量 = %d, want %d", dataType, adCount, wantAdCount)
+ }
+ content := make([]*proto.PlayletContentListItem, 0, contentCount)
+ for id := int64(1); id <= contentCount; id++ {
+ item := &proto.PlayletContentListItem{Type: 0}
+ if dataType == 1 {
+ item.Playlet = &proto.PlayletData{Id: id}
+ } else {
+ item.Video = &proto.VideoInfo{ContentData: proto.ContentData{Id: id}}
+ }
+ content = append(content, item)
+ }
+ var got []*proto.PlayletContentListItem
+ for start := int64(0); start < contentCount+adCount; start += 6 {
+ end := start + 6
+ if end > contentCount+adCount {
+ end = contentCount + adCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, contentCount, adCount, interval)
+ page := buildPlayletContentMixedPage(content[contentStart:contentStart+contentLimit], ads, "",
+ start, end, contentStart, adCount, interval)
+ if int64(len(page)) != end-start {
+ t.Fatalf("dataType %d 分页 [%d,%d) 返回 %d 条", dataType, start, end, len(page))
+ }
+ got = append(got, page...)
+ }
+ var nextID int64 = 1
+ for pos, item := range got {
+ cycle := int(interval + 1)
+ if pos%cycle == int(interval) && int64(pos/cycle) < adCount {
+ if item.Type != 2 || item.Advertise == nil || item.Advertise.Id != ads[(pos/cycle)%len(ads)].Id {
+ t.Fatalf("dataType %d 位置 %d 广告错误: %+v", dataType, pos, item)
+ }
+ continue
+ }
+ if item.Type != 0 || item.Advertise != nil {
+ t.Fatalf("dataType %d 位置 %d 内容类型错误: %+v", dataType, pos, item)
+ }
+ if dataType == 1 && (item.Playlet == nil || item.Playlet.Id != nextID || item.Video != nil) {
+ t.Fatalf("位置 %d 短剧错误: %+v", pos, item)
+ }
+ if dataType == 2 && (item.Video == nil || item.Video.Id != nextID || item.Playlet != nil) {
+ t.Fatalf("位置 %d 视频错误: %+v", pos, item)
+ }
+ nextID++
+ }
+ if nextID != contentCount+1 {
+ t.Fatalf("dataType %d 内容末尾 ID = %d", dataType, nextID)
+ }
+ }
+}
+
+func TestPlayletContentAdSlots(t *testing.T) {
+ for _, tc := range []struct {
+ dataType int64
+ content int64
+ ads int
+ want int64
+ }{
+ {1, 0, 2, 0}, {1, 4, 2, 0}, {1, 5, 2, 1}, {1, 8, 2, 1}, {1, 9, 2, 2}, {1, 13, 1, 3}, {1, 13, 0, 0},
+ {2, 0, 2, 0}, {2, 5, 2, 0}, {2, 6, 2, 1}, {2, 10, 2, 1}, {2, 11, 2, 2}, {2, 16, 1, 3}, {2, 16, 0, 0},
+ } {
+ if got := playletContentInsertedAdsCount(tc.content, tc.ads, playletContentAdInterval(tc.dataType)); got != tc.want {
+ t.Errorf("dataType %d 内容 %d 条、广告 %d 条得到 %d 个广告位,want %d", tc.dataType, tc.content, tc.ads, got, tc.want)
+ }
+ }
+}
diff --git a/app/service/playletser/playlet_list_test.go b/app/service/playletser/playlet_list_test.go
new file mode 100644
index 0000000..fc93bf7
--- /dev/null
+++ b/app/service/playletser/playlet_list_test.go
@@ -0,0 +1,119 @@
+package playletser
+
+import (
+ "context"
+ "testing"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/moreser"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/storage/proxy"
+)
+
+func TestPlayletListSortOptions(t *testing.T) {
+ for _, sortType := range []int64{0, 1} {
+ sort := playletListSort(sortType)
+ if len(sort) == 0 || sort[0].Key != playletmod.FN.Playlet.HeatValue {
+ t.Fatalf("sortType %d 未按热度优先排序: %v", sortType, sort)
+ }
+ }
+ sort := playletListSort(2)
+ if len(sort) == 0 || sort[0].Key != playletmod.FN.Playlet.UpdatedAt || sort[0].Value != -1 {
+ t.Fatalf("sortType 2 未按最近更新降序排序: %v", sort)
+ }
+ filter := playletListFilter()
+ if filter[playletmod.FN.Playlet.Status] != playletmod.StatusUp {
+ t.Fatalf("未限制上架状态: %v", filter)
+ }
+ if _, ok := filter[playletmod.FN.Playlet.UpdatedAt]; ok {
+ t.Fatalf("最近更新不应限制更新时间: %v", filter)
+ }
+ if _, err := PlayletList(context.Background(), &proto.PlayletListReq{SortType: 3}); err == nil {
+ t.Fatal("未拒绝无效排序方式")
+ }
+}
+
+func TestOfficialEpisodeRange(t *testing.T) {
+ tests := []struct {
+ count int64
+ open []int64
+ closed []int64
+ }{
+ {0, []int64{1, 2}, []int64{0, 3}},
+ {1, []int64{1}, []int64{2}},
+ {3, []int64{1, 2, 3}, []int64{4}},
+ {-1, []int64{1, 1001}, []int64{0}},
+ }
+ for _, tt := range tests {
+ playlet := &playletmod.Playlet{Status: playletmod.StatusUp, FreeEpisodeCount: tt.count}
+ for _, episode := range tt.open {
+ if !playlet.IsOfficialEpisodeOpen(episode) {
+ t.Fatalf("开放范围 %d 应允许第 %d 集", tt.count, episode)
+ }
+ }
+ for _, episode := range tt.closed {
+ if playlet.IsOfficialEpisodeOpen(episode) {
+ t.Fatalf("开放范围 %d 不应允许第 %d 集", tt.count, episode)
+ }
+ }
+ }
+ if (&playletmod.Playlet{Status: playletmod.StatusDown, FreeEpisodeCount: -1}).IsOfficialEpisodeOpen(1) {
+ t.Fatal("下架短剧不应开放")
+ }
+}
+
+func TestPlayletListMixedPagination(t *testing.T) {
+ const playletCount int64 = 14
+ ads := []proxy.AdvitiseInfo{{Id: "ad-1"}, {Id: "ad-2"}}
+ adCount := playletListInsertedAdsCount(playletCount, len(ads))
+ if adCount != 3 {
+ t.Fatalf("广告位数量 = %d, want 3", adCount)
+ }
+ all := make(playletmod.PlayletList, 0, playletCount)
+ for id := int64(1); id <= playletCount; id++ {
+ all = append(all, &playletmod.Playlet{Id: id})
+ }
+ var got []*proto.PlayletRecommendListItem
+ for start := int64(0); start < playletCount+adCount; start += 6 {
+ end := start + 6
+ if end > playletCount+adCount {
+ end = playletCount + adCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, playletCount, adCount, playletListAdInterval)
+ page := buildPlayletListMixedPage(all[contentStart:contentStart+contentLimit], ads, "", nil, nil,
+ start, end, contentStart, adCount)
+ if int64(len(page)) != end-start {
+ t.Fatalf("分页 [%d,%d) 返回 %d 条", start, end, len(page))
+ }
+ got = append(got, page...)
+ }
+ var playletID int64 = 1
+ for pos, item := range got {
+ if pos%5 == 4 && pos < 15 {
+ if item.Type != proto.PlayletRecommendItemTypeAdvertise || item.AdvertiseId != ads[(pos/5)%len(ads)].Id {
+ t.Fatalf("位置 %d 广告错误: %+v", pos, item)
+ }
+ continue
+ }
+ if item.Type != proto.PlayletRecommendItemTypePlaylet || item.Id != playletID {
+ t.Fatalf("位置 %d 短剧错误: %+v, want ID %d", pos, item, playletID)
+ }
+ playletID++
+ }
+ if playletID != playletCount+1 {
+ t.Fatalf("短剧末尾 ID = %d, want %d", playletID, playletCount+1)
+ }
+}
+
+func TestPlayletListAdSlots(t *testing.T) {
+ for _, tc := range []struct {
+ playlets int64
+ ads int
+ want int64
+ }{
+ {0, 2, 0}, {4, 2, 0}, {5, 2, 1}, {8, 2, 1}, {9, 2, 2}, {13, 1, 3}, {13, 0, 0},
+ } {
+ if got := playletListInsertedAdsCount(tc.playlets, tc.ads); got != tc.want {
+ t.Errorf("短剧 %d 条、广告 %d 条得到 %d 个广告位,want %d", tc.playlets, tc.ads, got, tc.want)
+ }
+ }
+}
diff --git a/app/service/questionnaireser/questionnaire.go b/app/service/questionnaireser/questionnaire.go
new file mode 100644
index 0000000..9624e3f
--- /dev/null
+++ b/app/service/questionnaireser/questionnaire.go
@@ -0,0 +1,79 @@
+package questionnaireser
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/questionnairemod"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+const (
+ maxFiles = 3
+)
+
+// Submit 提交问卷(App端)
+// ip:客户端真实 IP;req.UserId 为 0 表示匿名用户,仅按 IP 限流
+func Submit(ctx context.Context, ip string, req *proto.QuestionnaireSubmitReq) *srvdef.SrvErr {
+ // IP 维度:同一 IP 每分钟只能提交一次
+ ipCount, err := redis.Incr(redisconst.QuestionnaireLimitIpKey(ip), redisconst.QuestionnaireLimitExpire)
+ if err != nil {
+ log.Error("问卷提交IP限频Redis异常", log.E(err))
+ }
+ if ipCount > 1 {
+ return srvdef.New(srvdef.ErrOperationTooFrequently, "user operation is too frequent", "提交太频繁,请稍后再试")
+ }
+
+ // 用户维度:登录用户(userId > 0)每分钟只能提交一次
+ if req.UserId > 0 {
+ userCount, err := redis.Incr(redisconst.QuestionnaireLimitUserKey(req.UserId), redisconst.QuestionnaireLimitExpire)
+ if err != nil {
+ log.Error("问卷提交用户限频Redis异常", log.E(err))
+ }
+ if userCount > 1 {
+ return srvdef.New(srvdef.ErrOperationTooFrequently, "user operation is too frequent", "提交太频繁,请稍后再试")
+ }
+ }
+
+ // 校验附件数量限制
+ fileCount := 0
+ if req.Attachments != nil && len(req.Attachments) > 0 {
+ fileCount = len(req.Attachments)
+ }
+ if fileCount > maxFiles {
+ return srvdef.New(srvdef.ErrParamError, fmt.Sprintf("文件不超过%d个", fileCount), "")
+ }
+ attachments := req.Attachments
+ if attachments == nil {
+ attachments = []questionnairemod.Attachment{}
+ }
+
+ now := time.Now()
+ doc := &questionnairemod.QuestionnaireModel{
+ Id: primitive.NewObjectID(),
+ UserId: req.UserId,
+ QuestionType: req.QuestionType,
+ Frequency: req.Frequency,
+ Province: req.Province,
+ City: req.City,
+ Operator: req.Operator,
+ NetworkType: req.NetworkType,
+ DeviceType: req.DeviceType,
+ DeviceModel: req.DeviceModel,
+ Description: req.Description,
+ Contact: req.Contact,
+ Status: questionnairemod.StatusPending,
+ Attachments: attachments,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ return questionnairemod.InsertOne(ctx, doc)
+}
diff --git a/app/service/reportser/report.go b/app/service/reportser/report.go
new file mode 100644
index 0000000..51df48c
--- /dev/null
+++ b/app/service/reportser/report.go
@@ -0,0 +1,279 @@
+package reportser
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func ReportVideoComment(userId int64, ua useragent.UA, ip string, media *videoinformationmod.VideoInformation, content string) error {
+ if media == nil {
+ return srvdef.New(srvdef.Failure, "invalid user or media")
+ }
+ typeID, typeName := resolveMediaType(media)
+ event := dataReport.NewCommonField(dataReport.EventTypeVideoComment, "", ua, ip, userId)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoCommentEvent{
+ VideoEventPub: dataReport.VideoEventPub{
+ MediaId: "",
+ VideoID: fmt.Sprintf("%v", media.Id),
+ VideoTitle: media.Title,
+ VideoTypeID: typeID,
+ VideoTypeName: typeName,
+ VideoContentType: videoContentType(media),
+ RecommendTraceId: "",
+ },
+ CommentContent: content,
+ })
+ return insertEvent(event)
+}
+
+func ReportVideoCollect(userId int64, ua useragent.UA, ip string, media *videoinformationmod.VideoInformation, flag int64) error {
+ if media == nil {
+ return srvdef.New(srvdef.Failure, "invalid user or media")
+ }
+ typeID, typeName := resolveMediaType(media)
+ event := dataReport.NewCommonField(dataReport.EventTypeVideoCollect, "", ua, ip, userId)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoCollectEvent{
+ VideoEventPub: dataReport.VideoEventPub{
+ MediaId: "",
+ VideoID: fmt.Sprintf("%v", media.Id),
+ VideoTitle: media.Title,
+ VideoTypeID: typeID,
+ VideoTypeName: typeName,
+ VideoContentType: videoContentType(media),
+ RecommendTraceId: "",
+ },
+ Flag: dataReport.JsonNumber(flag),
+ })
+ return insertEvent(event)
+}
+
+func ReportVideoLike(userId int64, ua useragent.UA, ip string, media *videoinformationmod.VideoInformation, flag int64) error {
+ if media == nil {
+ return srvdef.New(srvdef.Failure, "invalid user or media")
+ }
+ typeID, typeName := resolveMediaType(media)
+ event := dataReport.NewCommonField(dataReport.EventTypeVideoLike, "", ua, ip, userId)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoLikeEvent{
+ VideoEventPub: dataReport.VideoEventPub{
+ MediaId: "",
+ VideoID: fmt.Sprintf("%v", media.Id),
+ VideoTitle: media.Title,
+ VideoTypeID: typeID,
+ VideoTypeName: typeName,
+ VideoContentType: videoContentType(media),
+ RecommendTraceId: "",
+ },
+ Flag: dataReport.JsonNumber(flag),
+ })
+ return insertEvent(event)
+}
+
+func ReportPlayletLike(userId int64, ua useragent.UA, ip string, playlet *playletmod.Playlet, flag int64) error {
+ if playlet == nil {
+ return srvdef.New(srvdef.Failure, "invalid user or playlet")
+ }
+ pub := playletToDramaPub(context.Background(), playlet, 0)
+ if pub.DramaID == "" {
+ return nil
+ }
+ event := dataReport.NewCommonField(dataReport.EventTypeDramaLike, "", ua, ip, userId)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.DramaLikeEvent{
+ DramaEventPub: pub,
+ Flag: dataReport.JsonNumber(flag),
+ })
+ return insertEvent(event)
+}
+
+// ReportPlayletCollect 上报短剧收藏状态变化
+func ReportPlayletCollect(userId int64, ua useragent.UA, ip string, playlet *playletmod.Playlet, flag int64) error {
+ if playlet == nil {
+ return srvdef.New(srvdef.Failure, "invalid user or playlet")
+ }
+ pub := playletToDramaPub(context.Background(), playlet, 0)
+ if pub.DramaID == "" {
+ return nil
+ }
+ event := dataReport.NewCommonField(dataReport.EventTypeDramaCollect, "", ua, ip, userId)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.DramaCollectEvent{
+ DramaEventPub: pub,
+ Flag: dataReport.JsonNumber(flag),
+ })
+ return insertEvent(event)
+}
+
+func playletToDramaPub(ctx context.Context, playlet *playletmod.Playlet, chapterEpisode int64) dataReport.DramaEventPub {
+ if playlet == nil {
+ return dataReport.DramaEventPub{}
+ }
+ return playletChapterToDramaPub(ctx, playlet, findDramaReportChapter(ctx, playlet.Id, chapterEpisode))
+}
+
+func playletChapterToDramaPub(ctx context.Context, playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) dataReport.DramaEventPub {
+ if playlet == nil {
+ return dataReport.DramaEventPub{}
+ }
+ typeID, typeName := resolvePlayletType(ctx, playlet)
+ tagKey, tagName := resolvePlayletTags(ctx, playlet.TagIds)
+ episode := int64(1)
+ dramaID := ""
+ videoID := ""
+ if chapter != nil && chapter.Id > 0 {
+ if chapter.CurrentEpisode > 0 {
+ episode = chapter.CurrentEpisode
+ }
+ dramaID = strconv.FormatInt(chapter.Id, 10)
+ if chapter.MediaContentId > 0 {
+ videoID = strconv.FormatUint(uint64(chapter.MediaContentId), 10)
+ }
+ }
+ return dataReport.DramaEventPub{
+ DramaID: dramaID,
+ DramaTitle: playlet.Title,
+ Episode: dataReport.JsonNumber(episode),
+ VideoID: videoID,
+ TotalEpisode: dataReport.JsonNumber(playlet.TotalEpisode),
+ DramaTypeID: typeID,
+ DramaTypeName: typeName,
+ DramaContentType: dataReport.DramaContentTypeShortDrama,
+ DramaTagKey: tagKey,
+ DramaTagName: tagName,
+ }
+}
+
+// findDramaReportChapter 取上报用剧集。指定集数就用该集,未指定时用第一集。
+func findDramaReportChapter(ctx context.Context, playletId, chapterEpisode int64) *playletchaptermod.PlayletChapter {
+ if playletId <= 0 {
+ return nil
+ }
+ projection := options.FindOne().SetProjection(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.MediaContentId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ })
+ filter := bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: playletId}
+ if chapterEpisode > 0 {
+ filter[playletchaptermod.FN.PlayletChapter.CurrentEpisode] = chapterEpisode
+ chapter, _ := playletchaptermod.FindOnePlayletChapter(ctx, filter, projection)
+ return chapter
+ }
+ chapter, _ := playletchaptermod.FindOnePlayletChapter(ctx, filter, projection.SetSort(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ }))
+ return chapter
+}
+
+func resolvePlayletType(ctx context.Context, playlet *playletmod.Playlet) (string, string) {
+ if playlet == nil || len(playlet.ThemeIds) == 0 {
+ return "default", "默认分类"
+ }
+ id := playlet.ThemeIds[0]
+ if id <= 0 {
+ return "default", "默认分类"
+ }
+ name := "默认分类"
+ queryCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ themes, err := playletthememod.FindManyPlayletTheme(queryCtx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: id,
+ }, options.Find().SetProjection(bson.M{
+ playletthememod.FN.PlayletTheme.Id: 1,
+ playletthememod.FN.PlayletTheme.Name: 1,
+ }))
+ if err == nil {
+ for _, theme := range themes {
+ if theme != nil && theme.Id == id && strings.TrimSpace(theme.Name) != "" {
+ name = strings.TrimSpace(theme.Name)
+ break
+ }
+ }
+ }
+ return strconv.FormatInt(id, 10), name
+}
+
+func resolvePlayletTags(ctx context.Context, ids []int64) (string, string) {
+ keys := make([]string, 0, len(ids))
+ validIDs := make([]int64, 0, len(ids))
+ for _, id := range ids {
+ if id > 0 {
+ keys = append(keys, strconv.FormatInt(id, 10))
+ validIDs = append(validIDs, id)
+ }
+ }
+ if len(keys) == 0 {
+ return "default", "默认标签"
+ }
+ nameMap := map[int64]string{}
+ queryCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ if tags, err := contenttagmod.GetMapTags(queryCtx, validIDs); err == nil {
+ for _, tag := range tags {
+ if tag != nil {
+ nameMap[tag.Id] = tag.Name
+ }
+ }
+ }
+ names := make([]string, 0, len(keys))
+ for _, id := range validIDs {
+ name := strings.TrimSpace(nameMap[id])
+ if name == "" {
+ name = strconv.FormatInt(id, 10)
+ }
+ names = append(names, name)
+ }
+ return strings.Join(keys, ","), strings.Join(names, ",")
+}
+
+func insertEvent(event dataReport.CommonField) error {
+ b, _ := json.Marshal(event)
+ return eventmod.InsertOneEvent(context.Background(), &eventmod.Event{
+ EventId: event.EventID,
+ EventType: event.Event,
+ Data: string(b),
+ Status: eventmod.Default,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ })
+}
+
+func resolveMediaType(media *videoinformationmod.VideoInformation) (string, string) {
+ var videoTypeID = "default"
+ var videoTypeName = "默认分类"
+ if media.CategoryId > 0 {
+ // 复用 reportser 中的分类解析逻辑
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ category, _ := videocategorymod.FindOneVideoCategory(ctx, bson.M{
+ videocategorymod.FN.VideoCategory.Id: media.CategoryId,
+ })
+ if category != nil {
+ videoTypeID = fmt.Sprintf("%v", category.Id)
+ videoTypeName = category.Name
+ }
+ }
+ return videoTypeID, videoTypeName
+}
+
+func videoContentType(media *videoinformationmod.VideoInformation) string {
+ //if media != nil && media.ContentType == pubdef.ContentTypeVideoDsp {
+ // return "short_video"
+ //}
+ return "video"
+}
diff --git a/app/service/richtextser/cache.go b/app/service/richtextser/cache.go
new file mode 100644
index 0000000..6f7db25
--- /dev/null
+++ b/app/service/richtextser/cache.go
@@ -0,0 +1,90 @@
+package richtextser
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/richtextcontentmod"
+
+ "github.com/robfig/cron/v3"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ richTextCron *cron.Cron
+ richTextCache = struct {
+ sync.RWMutex
+ byTitle map[string]proto.RichTextInfo
+ }{
+ byTitle: make(map[string]proto.RichTextInfo),
+ }
+)
+
+// Start 启动富文本缓存,每10分钟刷新一次。
+func Start() {
+ richTextCron = cron.New(cron.WithSeconds())
+ UpdateCache(context.Background())
+ _, _ = richTextCron.AddFunc("0 */10 * * * ?", func() {
+ UpdateCache(context.Background())
+ })
+ richTextCron.Start()
+}
+
+// Stop 停止富文本缓存刷新任务。
+func Stop() {
+ if richTextCron != nil {
+ richTextCron.Stop()
+ }
+}
+
+// UpdateCache 更新全部启用的富文本;查询失败时保留原缓存。
+func UpdateCache(ctx context.Context) {
+ list, serr := richtextcontentmod.FindManyRichTextContentModelModel(ctx,
+ bson.M{richtextcontentmod.FN.RichTextContentModel.Status: true},
+ options.Find().SetSort(bson.D{{Key: richtextcontentmod.FN.RichTextContentModel.UpdatedAt, Value: 1}}))
+ if serr != nil {
+ log.Warn("richtextser.UpdateCache failed, keep old cache", log.E(serr))
+ return
+ }
+
+ next := make(map[string]proto.RichTextInfo, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ title := strings.TrimSpace(item.Title)
+ if title == "" {
+ continue
+ }
+ next[title] = proto.RichTextInfo{
+ Id: item.Id,
+ Title: item.Title,
+ Content: item.Content,
+ Desc: item.Desc,
+ SeoInfo: item.SeoInfo,
+ }
+ }
+
+ richTextCache.Lock()
+ richTextCache.byTitle = next
+ richTextCache.Unlock()
+}
+
+// GetByTitle 从缓存按 title 获取富文本。
+func GetByTitle(title string) (proto.RichTextInfo, *srvdef.SrvErr) {
+ title = strings.TrimSpace(title)
+ if title == "" {
+ return proto.RichTextInfo{}, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ richTextCache.RLock()
+ data, ok := richTextCache.byTitle[title]
+ richTextCache.RUnlock()
+ if !ok {
+ return proto.RichTextInfo{}, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return data, nil
+}
diff --git a/app/service/searchser/cache.go b/app/service/searchser/cache.go
new file mode 100644
index 0000000..54cfe0e
--- /dev/null
+++ b/app/service/searchser/cache.go
@@ -0,0 +1 @@
+package searchser
\ No newline at end of file
diff --git a/app/service/searchser/search.go b/app/service/searchser/search.go
new file mode 100644
index 0000000..756b999
--- /dev/null
+++ b/app/service/searchser/search.go
@@ -0,0 +1,487 @@
+package searchser
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "github.com/patrickmn/go-cache"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ Cache *cache.Cache
+)
+
+func init() {
+ Cache = cache.New(5*time.Minute, 5*time.Minute)
+}
+func GetHotSearchItem(ctx context.Context) []*proto.HotSearchItem {
+ cacheKey := fmt.Sprintf("mdnet_hotsearchitem")
+ var resp []*proto.HotSearchItem
+ if cacheData, exists := Cache.Get(cacheKey); exists {
+ if item, ok := cacheData.([]*proto.HotSearchItem); ok {
+ resp = item
+ }
+ }
+ if resp == nil || len(resp) == 0 {
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{
+ videoinformationmod.FN.VideoInformation.Sort: -1,
+ })
+ opts.SetLimit(15)
+ list, _ := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.AddedTime: bson.M{
+ "$gte": time.Now().AddDate(0, 0, -7),
+ },
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }, opts)
+ data := make([]*proto.HotSearchItem, 0)
+ if len(list) > 0 {
+ for _, item := range list {
+ data = append(data, &proto.HotSearchItem{
+ Id: item.Id,
+ Title: item.Title,
+ HotValue: item.Real.WatchTimes * 100,
+ })
+ }
+ }
+ resp = data
+ Cache.Set(cacheKey, data, 5*time.Minute)
+ }
+ return resp
+}
+
+// Search 查询搜索结果
+func Search(ctx context.Context, title string, mediaType int64, page int64, sort string) *proto.SearchResp {
+ resp := &proto.SearchResp{
+ TotalSize: 0,
+ Sort: make([]proto.SearchSortItem, 0),
+ SearchRecommend: make([]string, 0),
+ Media: make([]proto.VideoInfoData, 0),
+ Playlets: make([]proto.PlayletData, 0),
+ PageNum: page,
+ PageSize: pubdef.PageSize,
+ }
+ resp.InTitle = title
+ resp.InType = mediaType
+ resp.Sort = getSearchSort()
+ resp.SearchRecommend = getSearchRecommend(title)
+ resp.Config = configser.GetRequestConfig(ctx)
+ var err error
+ if len(title) > 0 {
+ if mediaType < 0 {
+ searchAll(ctx, resp, title, page, sort)
+ } else if mediaType == 0 {
+ resp.TotalSize, resp.Media, err = searchVideo(ctx, title, page, sort)
+ if err != nil {
+ log.Warn("Search searchVideo Failed", log.Any("mediaType", mediaType), log.Any("title", title), log.E(err))
+ return resp
+ }
+ } else if mediaType == 6 {
+ resp.TotalSize, resp.Playlets, err = searchPlaylet(ctx, title, page, sort)
+ }
+ }
+ resp.TotalSize = pubdef.GetTotalPage(resp.TotalSize, pubdef.PageSize)
+ return resp
+}
+
+// searchAll 并发搜索全部资源类型
+func searchAll(ctx context.Context, resp *proto.SearchResp, title string, page int64, sort string) {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var (
+ mediaList []proto.VideoInfoData
+ playletList []proto.PlayletData
+ )
+ addTotal := func(total int64) {
+ mu.Lock()
+ resp.TotalSize += total
+ mu.Unlock()
+ }
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ total, data, err := searchVideo(ctx, title, page, sort)
+ if err != nil {
+ log.Warn("SearchAll searchVideo Failed", log.Any("title", title), log.E(err))
+ return
+ }
+ mu.Lock()
+ mediaList = data
+ mu.Unlock()
+ addTotal(total)
+ }()
+ go func() {
+ defer wg.Done()
+ total, data, err := searchPlaylet(ctx, title, page, sort)
+ if err != nil {
+ log.Warn("SearchAll searchPlaylet Failed", log.Any("title", title), log.E(err))
+ return
+ }
+ mu.Lock()
+ playletList = data
+ mu.Unlock()
+ addTotal(total)
+ }()
+ wg.Wait()
+ resp.Media = mediaList
+ resp.Playlets = playletList
+}
+
+// searchVideo 搜索视频数据
+func searchVideo(ctx context.Context, title string, page int64, sort string) (int64, []proto.VideoInfoData, error) {
+ data := make([]proto.VideoInfoData, 0)
+ total := int64(0)
+ skip := (page - 1) * pubdef.PageSize
+ total, list, err := esmod.SearchVideo(title, skip, pubdef.PageSize, sort, false)
+ if err != nil {
+ log.Warn("Search SearchVideo", log.Any("title", title), log.Any("sort", sort), log.E(err))
+ return total, data, err
+ }
+ if list != nil && len(list) > 0 {
+ config := configser.GetRequestConfig(ctx)
+ ids := make([]int64, len(list))
+ for i, v := range list {
+ ids[i] = v.Id
+ }
+ medias, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }, options.Find().SetProjection(searchVideoProjection()))
+ if err != nil {
+ log.Warn("Search FindManyVideoInformations", log.Any("title", title), log.E(err))
+ return total, data, err
+ }
+ mediaMap := make(map[int64]*videoinformationmod.VideoInformation, len(medias))
+ for _, media := range medias {
+ mediaMap[media.Id] = media
+ }
+ for _, id := range ids {
+ media := mediaMap[id]
+ if media == nil {
+ continue
+ }
+ info := proto.VideoInfoToData(media, config.ImgCdn, configser.GetCreateInfo(media.CreatorId, config.ImgCdn))
+ for _, tagId := range media.Tags {
+ tagInfo := configser.GetVideoTag(tagId)
+ if tagInfo != nil {
+ info.Tags = append(info.Tags, proto.TagsInfoData{
+ Id: tagInfo.Id,
+ Name: tagInfo.Name,
+ Slug: tagInfo.Slug,
+ })
+ }
+ }
+ data = append(data, info)
+ }
+
+ }
+ return total, data, nil
+}
+
+// searchPlaylet 搜索短剧数据
+func searchPlaylet(ctx context.Context, title string, page int64, sort string) (int64, []proto.PlayletData, error) {
+ data := make([]proto.PlayletData, 0)
+ total := int64(0)
+ skip := (page - 1) * pubdef.PageSize
+ total, list, err := esmod.SearchPlaylet(title, skip, pubdef.PageSize, sort)
+ if err != nil {
+ log.Warn("Search SearchPlaylet", log.Any("title", title), log.Any("sort", sort), log.E(err))
+ return total, data, err
+ }
+ if len(list) == 0 {
+ return total, data, nil
+ }
+ config := configser.GetRequestConfig(ctx)
+ ids := make([]int64, 0, len(list))
+ for _, item := range list {
+ if item != nil && item.Id > 0 {
+ ids = append(ids, item.Id)
+ }
+ }
+ if len(ids) == 0 {
+ return total, data, nil
+ }
+ playlets, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": ids},
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.Find().SetProjection(searchPlayletProjection()))
+ if err != nil {
+ log.Warn("Search FindManyPlaylet", log.Any("title", title), log.E(err))
+ return total, data, err
+ }
+ playletMap := make(map[int64]*playletmod.Playlet, len(playlets))
+ for _, item := range playlets {
+ if item != nil {
+ playletMap[item.Id] = item
+ }
+ }
+ tagNameMap, themeNameMap, err := buildSearchPlayletTagThemeNameMaps(ctx, playlets)
+ if err != nil {
+ return total, data, err
+ }
+ data = make([]proto.PlayletData, 0, len(ids))
+ for _, id := range ids {
+ item := playletMap[id]
+ if item == nil {
+ continue
+ }
+ data = append(data, searchPlayletToData(item, config.ImgCdn, tagNameMap, themeNameMap))
+ }
+ return total, data, nil
+}
+
+// searchVideoProjection 返回搜索视频列表需要的Mongo字段
+func searchVideoProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.MediaType: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.Keyworld: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.CreatorId: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Video.Cover: 1,
+ videoinformationmod.FN.VideoInformation.Video.Url: 1,
+ videoinformationmod.FN.VideoInformation.Video.H265Url: 1,
+ videoinformationmod.FN.VideoInformation.Video.VideoTime: 1,
+ videoinformationmod.FN.VideoInformation.IsHot: 1,
+ videoinformationmod.FN.VideoInformation.Real.ShowTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Desc: 1,
+ videoinformationmod.FN.VideoInformation.Bango: 1,
+ videoinformationmod.FN.VideoInformation.IssuedBrand: 1,
+ videoinformationmod.FN.VideoInformation.IssuedAt: 1,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: 1,
+ videoinformationmod.FN.VideoInformation.PageUpdatedAt: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ }
+}
+
+// searchPlayletProjection 返回搜索短剧列表需要的Mongo字段
+func searchPlayletProjection() bson.M {
+ return bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.Status: 1,
+ playletmod.FN.Playlet.IsRecommend: 1,
+ playletmod.FN.Playlet.IsFeatured: 1,
+ playletmod.FN.Playlet.IsOriginal: 1,
+ playletmod.FN.Playlet.ShowOriginal: 1,
+ playletmod.FN.Playlet.MediaCenterId: 1,
+ playletmod.FN.Playlet.OnlineAt: 1,
+ playletmod.FN.Playlet.MediaCreatedAt: 1,
+ playletmod.FN.Playlet.MediaUpdatedAt: 1,
+ playletmod.FN.Playlet.Title: 1,
+ playletmod.FN.Playlet.Slug: 1,
+ playletmod.FN.Playlet.Desc: 1,
+ playletmod.FN.Playlet.Anchor: 1,
+ playletmod.FN.Playlet.Cover: 1,
+ playletmod.FN.Playlet.CompressedCover: 1,
+ playletmod.FN.Playlet.Preview: 1,
+ playletmod.FN.Playlet.TagIds: 1,
+ playletmod.FN.Playlet.ThemeIds: 1,
+ playletmod.FN.Playlet.ContentType: 1,
+ playletmod.FN.Playlet.PayType: 1,
+ playletmod.FN.Playlet.SingleCoins: 1,
+ playletmod.FN.Playlet.TotalEpisode: 1,
+ playletmod.FN.Playlet.UpdateStatus: 1,
+ playletmod.FN.Playlet.CountBrowse: 1,
+ playletmod.FN.Playlet.VirtualCountBrowse: 1,
+ playletmod.FN.Playlet.HeatValue: 1,
+ playletmod.FN.Playlet.VirtualHeatValue: 1,
+ playletmod.FN.Playlet.CountCollect: 1,
+ playletmod.FN.Playlet.CountLike: 1,
+ playletmod.FN.Playlet.VirtualCountLike: 1,
+ playletmod.FN.Playlet.SeoInfo.Title: 1,
+ playletmod.FN.Playlet.SeoInfo.Description: 1,
+ playletmod.FN.Playlet.SeoInfo.Keyworld: 1,
+ playletmod.FN.Playlet.CreatedAt: 1,
+ playletmod.FN.Playlet.UpdatedAt: 1,
+ }
+}
+
+func searchPlayletToData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string) proto.PlayletData {
+ if item == nil {
+ return proto.PlayletData{}
+ }
+ return proto.PlayletData{
+ Id: item.Id,
+ Status: item.Status,
+ IsRecommend: item.IsRecommend,
+ IsNew: searchPlayletIsNew(item.OnlineAt),
+ IsFeatured: item.IsFeatured,
+ IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
+ ShowOriginal: item.ShowOriginal,
+ OnlineAt: item.OnlineAt,
+ MediaCreatedAt: item.MediaCreatedAt,
+ MediaUpdatedAt: item.MediaUpdatedAt,
+ Title: item.Title,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: netutil.MergeURL(imgCdn, item.DisplayCover()),
+ Preview: netutil.MergeURL(imgCdn, item.Preview),
+ TagIds: item.TagIds,
+ TagNames: searchPlayletTagNamesFromMap(item.TagIds, tagNameMap),
+ TagSlugs: searchPlayletTagSlugsFromMap(item.TagIds, tagNameMap),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: searchPlayletNamesFromMap(item.ThemeIds, themeNameMap),
+ ContentType: item.ContentType,
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.DisplayCountBrowse(),
+ HeatValue: item.DisplayHeatValue(),
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ VirtualCountLike: item.VirtualCountLike,
+ DisplayCountLike: item.CountLike + item.VirtualCountLike,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: item.SeoInfo,
+ }
+}
+
+func buildSearchPlayletTagThemeNameMaps(ctx context.Context, list playletmod.PlayletList) (contenttagmod.ContentTagMap, map[int64]string, *srvdef.SrvErr) {
+ tagIds := searchPlayletRelationIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.TagIds
+ })
+ themeIds := searchPlayletRelationIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.ThemeIds
+ })
+ tagNameMap, err := buildSearchPlayletTagNameMap(ctx, tagIds)
+ if err != nil {
+ return nil, nil, err
+ }
+ themeNameMap, err := buildSearchPlayletThemeNameMap(ctx, themeIds)
+ if err != nil {
+ return nil, nil, err
+ }
+ return tagNameMap, themeNameMap, nil
+}
+
+func buildSearchPlayletTagNameMap(ctx context.Context, ids []int64) (contenttagmod.ContentTagMap, *srvdef.SrvErr) {
+ result := make(contenttagmod.ContentTagMap, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ return contenttagmod.GetMapTags(ctx, ids)
+}
+
+func searchPlayletTagNamesFromMap(ids []int64, tags contenttagmod.ContentTagMap) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if tag := tags[id]; tag != nil && tag.Name != "" {
+ result = append(result, tag.Name)
+ }
+ }
+ return result
+}
+
+func searchPlayletTagSlugsFromMap(ids []int64, tags contenttagmod.ContentTagMap) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if tag := tags[id]; tag != nil && tag.Name != "" {
+ result = append(result, tag.Slug)
+ }
+ }
+ return result
+}
+
+func buildSearchPlayletThemeNameMap(ctx context.Context, ids []int64) (map[int64]string, *srvdef.SrvErr) {
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ list, 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 _, item := range list {
+ if item != nil {
+ result[item.Id] = item.Name
+ }
+ }
+ return result, nil
+}
+
+func searchPlayletRelationIds(list playletmod.PlayletList, pick func(item *playletmod.Playlet) []int64) []int64 {
+ result := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, id := range pick(item) {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ }
+ return result
+}
+
+func searchPlayletNamesFromMap(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
+}
+
+func searchPlayletIsNew(onlineAt time.Time) bool {
+ if onlineAt.IsZero() {
+ return false
+ }
+ return onlineAt.After(time.Now().AddDate(0, 0, -7))
+}
+
+// getSearchRecommend 返回搜索推荐词
+func getSearchRecommend(title string) []string {
+ return make([]string, 0)
+}
+
+func getSearchSort() []proto.SearchSortItem {
+ resp := make([]proto.SearchSortItem, 0)
+ resp = append(resp, proto.SearchSortItem{
+ Key: string(proto.SearchSortTypeDefault),
+ Title: "相关度",
+ })
+ resp = append(resp, proto.SearchSortItem{
+ Key: string(proto.SearchSortTypeHot),
+ Title: "热度最高",
+ })
+ resp = append(resp, proto.SearchSortItem{
+ Key: string(proto.SearchSortTypeNew),
+ Title: "最新发布",
+ })
+ return resp
+}
diff --git a/app/service/slugser/resolve.go b/app/service/slugser/resolve.go
new file mode 100644
index 0000000..7ee7ac1
--- /dev/null
+++ b/app/service/slugser/resolve.go
@@ -0,0 +1,119 @@
+package slugser
+
+import (
+ "context"
+ "strconv"
+ "strings"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// Resolve 解析实体ID:slug 非空时先按 slug 查 ID,否则使用传入 ID。
+func Resolve(id int64, slug string, required bool, emptyMsg string, lookup func(string) (int64, *srvdef.SrvErr)) (int64, *srvdef.SrvErr) {
+ slug = strings.TrimSpace(slug)
+ if slug != "" {
+ if lookup == nil {
+ return 0, srvdef.New(srvdef.ErrParamError, emptyMsg, emptyMsg)
+ }
+ return lookup(slug)
+ }
+ if required && id <= 0 {
+ if emptyMsg == "" {
+ emptyMsg = "参数错误"
+ }
+ return 0, srvdef.New(srvdef.ErrParamError, emptyMsg, emptyMsg)
+ }
+ return id, nil
+}
+
+// ParseIDOrSlug 解析路径参数:纯正整数当作ID,否则当作slug。
+func ParseIDOrSlug(raw string) (int64, string) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return 0, ""
+ }
+ if id, err := strconv.ParseInt(raw, 10, 64); err == nil && id > 0 {
+ return id, ""
+ }
+ return 0, raw
+}
+
+func VideoID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ return Resolve(id, slug, required, "视频ID不能为空", func(s string) (int64, *srvdef.SrvErr) {
+ item, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Slug: s,
+ }, options.FindOne().SetProjection(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}))
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ })
+}
+
+func VideoCategoryID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ return Resolve(id, slug, required, "视频分类ID不能为空", func(s string) (int64, *srvdef.SrvErr) {
+ item, err := videocategorymod.FindOneVideoCategory(ctx, bson.M{
+ videocategorymod.FN.VideoCategory.Slug: s,
+ })
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ })
+}
+
+func PlayletID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ return Resolve(id, slug, required, "短剧ID不能为空", func(s string) (int64, *srvdef.SrvErr) {
+ item, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Slug: s,
+ }, options.FindOne().SetProjection(bson.M{playletmod.FN.Playlet.Id: 1}))
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ })
+}
+
+func PlayletCategoryID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ return Resolve(id, slug, required, "短剧分类ID不能为空", func(s string) (int64, *srvdef.SrvErr) {
+ item, err := playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Slug: s,
+ }, options.FindOne().SetProjection(bson.M{playletcategorymod.FN.PlayletCategory.Id: 1}))
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ })
+}
+
+func ActressID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ return Resolve(id, slug, required, "女优ID不能为空", func(s string) (int64, *srvdef.SrvErr) {
+ item, err := actress.FindOneActress(ctx, bson.M{
+ actress.FN.Actress.Slug: s,
+ })
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ })
+}
+
+func ContentTagID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ return Resolve(id, slug, required, "标签ID不能为空", func(s string) (int64, *srvdef.SrvErr) {
+ item, err := contenttagmod.FindOneContentTag(ctx, bson.M{
+ contenttagmod.FN.ContentTag.Slug: s,
+ }, options.FindOne().SetProjection(bson.M{contenttagmod.FN.ContentTag.Id: 1}))
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ })
+}
diff --git a/app/service/translationser/catch.go b/app/service/translationser/catch.go
new file mode 100644
index 0000000..2fb7060
--- /dev/null
+++ b/app/service/translationser/catch.go
@@ -0,0 +1,260 @@
+package translationser
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strings"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/pubdef"
+
+ "github.com/robfig/cron/v3"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ CacheMgr CacheInfo
+ skdCron *cron.Cron
+ categoryCacheFill sync.Mutex
+)
+
+type CacheInfo struct {
+ Lock sync.RWMutex // 读写锁
+ TranslationData proto.AppTranslationInfo // 顶部导航缓存
+ BottomTranslationData proto.AppTranslationInfo // 底部导航缓存
+ TranslationMap map[int64]proto.TranslationInfo
+
+ // 视频分类缓存(按 ID 索引)
+ VideoCategoryMap map[int64]*videocategorymod.VideoCategory
+}
+
+func init() {
+ skdCron = cron.New(cron.WithSeconds())
+ CacheMgr.TranslationData = proto.AppTranslationInfo{}
+ CacheMgr.BottomTranslationData = proto.AppTranslationInfo{}
+ CacheMgr.TranslationMap = make(map[int64]proto.TranslationInfo)
+ CacheMgr.VideoCategoryMap = make(map[int64]*videocategorymod.VideoCategory)
+}
+
+func Start() {
+ ctx := context.Background()
+ UpdateTranslationInfo(ctx)
+
+ _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ UpdateTranslationInfo(ctx)
+ })
+
+ skdCron.Start()
+}
+
+func UpdateTranslationInfo(ctx context.Context) {
+ filter := bson.M{cfg.FN.SysCfg.Id: 1}
+ sysCfg, _ := cfg.FindOneSysCfg(context.Background(), filter)
+ if sysCfg == nil {
+ return
+ }
+
+ translation := proto.AppTranslationInfo{}
+ bottomTranslation := proto.AppTranslationInfo{}
+
+ wg := &sync.WaitGroup{}
+ wg.Add(3)
+ var translationCategoryList translationcategorymod.TranslationCategoryList
+ var translationList translationmod.TranslationInfoList
+ var videoCategoryList videocategorymod.VideoCategoryList
+
+ go func() {
+ defer wg.Done()
+ translationCategoryList, _ = translationcategorymod.FindManyTranslationCategorys(ctx,
+ bson.M{translationcategorymod.FN.TranslationCategory.Status: true},
+ options.Find().SetLimit(1000).SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1}),
+ )
+ }()
+ go func() {
+ defer wg.Done()
+ translationList, _ = translationmod.FindManyTranslationInfos(ctx,
+ bson.M{translationmod.FN.TranslationInfo.Status: true},
+ options.Find().SetLimit(1000).SetSort(bson.M{translationmod.FN.TranslationInfo.Rank: -1}),
+ )
+ }()
+ go func() {
+ defer wg.Done()
+ videoCategoryList, _ = videocategorymod.FindManyVideoCategorys(ctx,
+ bson.M{videocategorymod.FN.VideoCategory.Status: true},
+ options.Find().SetLimit(500).SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1}),
+ )
+ }()
+ wg.Wait()
+
+ // ---- 构建各分类 ID Map(用于缓存 & 导航分组) ----
+ newVideoCategoryMap := make(map[int64]*videocategorymod.VideoCategory, len(videoCategoryList))
+ videoCategoryByNavId := make(map[int64][]*videocategorymod.VideoCategory)
+ for _, c := range videoCategoryList {
+ newVideoCategoryMap[c.Id] = c
+ if c.TranslationCategoryId > 0 {
+ videoCategoryByNavId[c.TranslationCategoryId] = append(videoCategoryByNavId[c.TranslationCategoryId], c)
+ }
+ }
+
+ // ---- 构建导航树 ----
+ tempTranslationMap := make(map[int64]proto.TranslationInfo)
+
+ for _, categoryInfo := range translationCategoryList {
+ categoryTemp := proto.TranslationCategory{
+ ID: categoryInfo.Id,
+ Name: categoryInfo.Name,
+ Icon: netutil.MergeURL(sysCfg.ImgAppCdn, categoryInfo.Icon),
+ TranslationList: make([]proto.TranslationInfo, 0),
+ CategoryType: categoryInfo.CategoryType,
+ UrlValue: categoryInfo.UrlValue,
+ }
+
+ if categoryInfo.CategoryType == pubdef.TranslationTypeSub {
+ // 固定路径条目优先放在最前面(根据导航分类的 UrlValue 决定属于哪个内容域)
+ categoryTemp.TranslationList = append(categoryTemp.TranslationList,
+ buildFixedNavItems(categoryInfo.UrlValue)...,
+ )
+ // 手动配置的导航条目
+ for _, translationInfo := range translationList {
+ if categoryInfo.Id != translationInfo.TransCategoryId {
+ continue
+ }
+ translationTmp := proto.TranslationInfo{
+ ID: translationInfo.Id,
+ Name: translationInfo.Name,
+ TransCategoryId: translationInfo.TransCategoryId,
+ }
+ if translationInfo.IsFriendAds {
+ // 友链广告,顶部暂不支持
+ continue
+ } else if translationInfo.HtmlId > 0 {
+ translationTmp.UrlValue = "/richtext?title=" + url.QueryEscape(strings.TrimSpace(translationInfo.Name))
+ } else if len(translationInfo.Link) > 0 {
+ translationTmp.UrlValue = translationInfo.Link
+ }
+ if translationTmp.UrlValue == "" {
+ continue
+ }
+ tempTranslationMap[translationInfo.Id] = translationTmp
+ categoryTemp.TranslationList = append(categoryTemp.TranslationList, translationTmp)
+ }
+ // 挂载到该导航的视频分类
+ for _, c := range videoCategoryByNavId[categoryInfo.Id] {
+ if c.DisplayPosition == pubdef.CategoryDisplayPositionHomeOnly {
+ continue
+ }
+ categoryTemp.TranslationList = append(categoryTemp.TranslationList, proto.TranslationInfo{
+ ID: c.Id,
+ Name: c.Name,
+ DisplayMode: c.DisplayMode,
+ UrlValue: fmt.Sprintf(pubdef.TranslationPath_VideoMore, c.Id),
+ })
+ }
+ } else if categoryInfo.CategoryType == pubdef.TranslationTypeBottom {
+ for _, translationInfo := range translationList {
+ if categoryInfo.Id != translationInfo.TransCategoryId {
+ continue
+ }
+ translationTmp := proto.TranslationInfo{
+ ID: translationInfo.Id,
+ Name: translationInfo.Name,
+ TransCategoryId: translationInfo.TransCategoryId,
+ }
+ if translationInfo.IsFriendAds {
+ translationTmp.IsFriendAds = true
+ } else if translationInfo.HtmlId > 0 {
+ translationTmp.UrlValue = "/richtext?title=" + url.QueryEscape(strings.TrimSpace(translationInfo.Name))
+ } else if len(translationInfo.Link) > 0 {
+ translationTmp.UrlValue = translationInfo.Link
+ }
+ tempTranslationMap[translationInfo.Id] = translationTmp
+ categoryTemp.TranslationList = append(categoryTemp.TranslationList, translationTmp)
+ }
+ bottomTranslation.CategoryList = append(bottomTranslation.CategoryList, categoryTemp)
+ continue
+ } else {
+ continue
+ }
+ translation.CategoryList = append(translation.CategoryList, categoryTemp)
+ }
+
+ log.Info("UpdateTranslationInfo", log.Any("categoryList", translation.CategoryList))
+
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.TranslationData = translation
+ CacheMgr.BottomTranslationData = bottomTranslation
+ CacheMgr.TranslationMap = tempTranslationMap
+ CacheMgr.VideoCategoryMap = newVideoCategoryMap
+}
+
+// ---- 导航读取 ----
+
+func GetTranslationData() proto.AppTranslationInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return CacheMgr.TranslationData
+}
+
+func GetTranslationBottomList() proto.AppTranslationInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return CacheMgr.BottomTranslationData
+}
+
+// ---- 分类读取 ----
+
+func categoryCacheEmpty() bool {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return len(CacheMgr.VideoCategoryMap) == 0
+}
+
+// EnsureCategoryCaches 若任一分内容分类缓存为空则立即补全;常规定时 5 分钟刷新。
+func EnsureCategoryCaches(ctx context.Context) {
+ if !categoryCacheEmpty() {
+ return
+ }
+ categoryCacheFill.Lock()
+ defer categoryCacheFill.Unlock()
+ if !categoryCacheEmpty() {
+ return
+ }
+ UpdateTranslationInfo(ctx)
+}
+
+// EnsureVideoCategoryCache 兼容旧调用,等同 EnsureCategoryCaches。
+func EnsureVideoCategoryCache(ctx context.Context) {
+ EnsureCategoryCaches(ctx)
+}
+
+// GetVideoCategory 通过视频分类 ID 获取分类信息,不存在返回 nil
+func GetVideoCategory(id int64) *videocategorymod.VideoCategory {
+ EnsureCategoryCaches(context.Background())
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return CacheMgr.VideoCategoryMap[id]
+}
+
+// buildFixedNavItems 根据导航分类的 UrlValue 返回对应内容域的固定路径条目,
+// 这些条目始终排在动态分类和手动配置条目之前。
+func buildFixedNavItems(urlValue string) []proto.TranslationInfo {
+ switch urlValue {
+ case pubdef.TranslationPath_Video:
+ return []proto.TranslationInfo{
+ //{Name: pubdef.TranslationName_Video, UrlValue: pubdef.TranslationPath_Video},
+ //{Name: pubdef.TranslationName_VideoAll, UrlValue: pubdef.TranslationPath_VideoAll},
+ //{Name: pubdef.TranslationName_VideoRank, UrlValue: pubdef.TranslationPath_VideoRank},
+ //{Name: pubdef.TranslationName_ActressRank, UrlValue: pubdef.TranslationPath_ActressRank},
+ }
+ }
+ return nil
+}
diff --git a/app/service/uploadser/upload.go b/app/service/uploadser/upload.go
new file mode 100644
index 0000000..9951409
--- /dev/null
+++ b/app/service/uploadser/upload.go
@@ -0,0 +1,211 @@
+package uploadser
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "strconv"
+ "videoweb/pub/common/log"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+)
+
+const (
+ defaultUploadUserName = "questionnaires"
+ // 分片上传路径
+ videoBodyPath = "/media/web/autopublish/videoBody"
+ // 分片完成路径
+ videoOkPath = "/media/web/autopublish/videoOk"
+ // 图片上传路径
+ imagePath = "/media/web/autopublish/image"
+)
+
+// VideoBodyResp 视频分片上传响应
+type VideoBodyResp struct {
+ Code int `json:"code"`
+ Data VideoBodyData `json:"data"`
+ Hash bool `json:"hash"`
+ Msg string `json:"msg"`
+ Time string `json:"time"`
+ Tip string `json:"tip"`
+}
+
+// VideoBodyData 视频分片上传响应数据
+type VideoBodyData struct {
+ Id string `json:"id"`
+ TaskId string `json:"taskId"`
+ SourceNameFs string `json:"sourceNameFs"`
+}
+
+// VideoOkResp 视频上传完成响应
+type VideoOkResp struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Tip string `json:"tip"`
+}
+
+// ImageUploadResp 图片上传响应
+type ImageUploadResp struct {
+ Code int `json:"code"`
+ Data ImageUploadData `json:"data"`
+ Hash bool `json:"hash"`
+ Msg string `json:"msg"`
+ Time string `json:"time"`
+ Tip string `json:"tip"`
+}
+
+type ImageUploadData struct {
+ Id string `json:"id"`
+ SourceNameFs string `json:"sourceNameFs"`
+}
+
+// VideoBodyParam 视频分片上传入参
+type VideoBodyParam struct {
+ TaskId string
+ Pos string
+ AppId string
+ UploadUserName string
+ FileHeader *multipart.FileHeader // 分片文件
+}
+
+// ImageParam 图片上传入参
+type ImageParam struct {
+ AppId string
+ FileHeader *multipart.FileHeader
+}
+
+// targetDomain 获取转发目标域名
+func targetDomain() string {
+ return proxy.DomainAddressRepair("", proxy.BucketVideoAddressV2)
+}
+
+// ForwardVideoBody 转发视频分片上传
+func ForwardVideoBody(param *VideoBodyParam) ([]byte, error) {
+ if param.AppId == "" {
+ param.AppId = strconv.Itoa(pubdef.AppId)
+ }
+ if param.UploadUserName == "" {
+ param.UploadUserName = defaultUploadUserName
+ }
+
+ var buf bytes.Buffer
+ mw := multipart.NewWriter(&buf)
+
+ fields := map[string]string{
+ "taskId": param.TaskId,
+ "pos": param.Pos,
+ "appId": param.AppId,
+ "uploadUserName": param.UploadUserName,
+ }
+ for k, v := range fields {
+ if err := mw.WriteField(k, v); err != nil {
+ return nil, fmt.Errorf("write field %s: %w", k, err)
+ }
+ }
+
+ if param.FileHeader != nil {
+ if err := writeFileField(mw, "file", param.FileHeader); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := mw.Close(); err != nil {
+ return nil, err
+ }
+
+ url := targetDomain() + videoBodyPath
+ return doMultipartPost(url, mw.FormDataContentType(), &buf)
+}
+
+// ForwardVideoOk 转发视频分片完成通知
+func ForwardVideoOk(taskId string) ([]byte, error) {
+ body := map[string]string{"taskId": taskId}
+ jsonBytes, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ url := targetDomain() + videoOkPath
+ resp, err := http.Post(url, "application/json", bytes.NewReader(jsonBytes)) //nolint:noctx
+ if err != nil {
+ log.Error("uploadser:ForwardVideoOk failed", log.E(err))
+ return nil, err
+ }
+ defer resp.Body.Close()
+ return io.ReadAll(resp.Body)
+}
+
+// ForwardImage 转发图片上传
+func ForwardImage(param *ImageParam) ([]byte, error) {
+ if param.AppId == "" {
+ param.AppId = strconv.Itoa(pubdef.AppId)
+ }
+
+ var buf bytes.Buffer
+ mw := multipart.NewWriter(&buf)
+
+ if err := mw.WriteField("appId", param.AppId); err != nil {
+ return nil, err
+ }
+
+ if param.FileHeader != nil {
+ if err := writeFileField(mw, "file", param.FileHeader); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := mw.Close(); err != nil {
+ return nil, err
+ }
+
+ url := targetDomain() + imagePath
+ return doMultipartPost(url, mw.FormDataContentType(), &buf)
+}
+
+// writeFileField 将 multipart.FileHeader 写入目标 multipart.Writer
+func writeFileField(mw *multipart.Writer, fieldName string, fh *multipart.FileHeader) error {
+ src, err := fh.Open()
+ if err != nil {
+ return fmt.Errorf("open file: %w", err)
+ }
+ defer src.Close()
+
+ // 保留原始文件名和 Content-Type
+ h := make(textproto.MIMEHeader)
+ contentType := fh.Header.Get("Content-Type")
+ if contentType == "" {
+ contentType = "application/octet-stream"
+ }
+ h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fh.Filename))
+ h.Set("Content-Type", contentType)
+
+ part, err := mw.CreatePart(h)
+ if err != nil {
+ return fmt.Errorf("create part: %w", err)
+ }
+ if _, err = io.Copy(part, src); err != nil {
+ return fmt.Errorf("copy file data: %w", err)
+ }
+ return nil
+}
+
+// doMultipartPost 发送 multipart/form-data 请求并返回响应 body
+func doMultipartPost(url, contentType string, body io.Reader) ([]byte, error) {
+ req, err := http.NewRequest(http.MethodPost, url, body)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", contentType)
+
+ cli := &http.Client{}
+ resp, err := cli.Do(req)
+ if err != nil {
+ log.Error("uploadser:doMultipartPost failed", log.Any("url", url), log.E(err))
+ return nil, err
+ }
+ defer resp.Body.Close()
+ return io.ReadAll(resp.Body)
+}
diff --git a/app/service/videoser/h5_m3u8_ticket.go b/app/service/videoser/h5_m3u8_ticket.go
new file mode 100644
index 0000000..28288cf
--- /dev/null
+++ b/app/service/videoser/h5_m3u8_ticket.go
@@ -0,0 +1,258 @@
+package videoser
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/crypt"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ h5M3U8TicketTTLSeconds = int64(600)
+ h5M3U8TicketKeyVersion = "v1"
+ h5M3U8TicketBindIP = true
+ h5M3U8RoutePrefix = "/h5/m3u8/"
+ hardcodedH5M3U8TicketKey = "3d058714838683a97c04d7b6ab609fc196f29e55df36197341d87325dc63154a"
+ H5M3U8FallbackPath = "/v3/av/w0/w6/db/7j/029e2f2df53d4741ad39b471e2ad1806.m3u8"
+)
+
+// H5M3U8Ticket 记录业务接口已确认的正片/预览权限,供 m3u8 接口按 IP 可信计数。
+type H5M3U8Ticket struct {
+ MediaID int64 `json:"mediaId"`
+ Path string `json:"path"`
+ ExpireAt int64 `json:"expireAt"`
+ IPHash string `json:"ipHash,omitempty"`
+ Playable bool `json:"playable"`
+ Preview bool `json:"preview"`
+ UseNewKey bool `json:"useNewKey,omitempty"` // 短剧原片使用 newm3u8.key
+ KeyVersion string `json:"keyVersion,omitempty"`
+ OfficialPlayletID int64 `json:"officialPlayletId,omitempty"` // 官网短剧ID 非官网播放为0
+}
+
+func h5M3U8TicketKey() string {
+ return hardcodedH5M3U8TicketKey
+}
+
+// BuildH5M3U8LegacyURL 返回不带路由前缀的相对票据地址,兼容 H5 自行拼接 /h5/m3u8/。
+func BuildH5M3U8LegacyURL(mediaID int64, path, ip string, playable, preview bool) string {
+ return buildH5M3U8URL(mediaID, path, ip, playable, preview, false, 0)
+}
+
+// BuildH5M3U8PlayletURL 为短剧播放地址生成票据。原片 useNewKey 为 true。
+func BuildH5M3U8PlayletURL(chapterID int64, path, ip string, useNewKey bool) string {
+ return buildH5M3U8URL(chapterID, path, ip, true, false, useNewKey, 0)
+}
+
+// BuildH5M3U8OfficialURL 签发需要再次校验官网开放范围的剧集票据
+func BuildH5M3U8OfficialURL(chapterID, playletID int64, path, ip string, useNewKey bool) string {
+ return buildH5M3U8URL(chapterID, path, ip, true, false, useNewKey, playletID)
+}
+
+func buildH5M3U8URL(mediaID int64, path, ip string, playable, preview, useNewKey bool, officialPlayletID int64) string {
+ key := h5M3U8TicketKey()
+ if key == "" || mediaID <= 0 || strings.TrimSpace(ip) == "" {
+ return ""
+ }
+ normalizedPath, ok := normalizeH5M3U8Path(path)
+ if !ok {
+ return ""
+ }
+ expireAt := time.Now().Add(time.Duration(h5M3U8TicketTTLSeconds) * time.Second).Unix()
+ ticket := H5M3U8Ticket{
+ MediaID: mediaID,
+ Path: normalizedPath,
+ ExpireAt: expireAt,
+ Playable: playable,
+ Preview: preview,
+ UseNewKey: useNewKey,
+ KeyVersion: h5M3U8TicketKeyVersion,
+ OfficialPlayletID: officialPlayletID,
+ }
+ if h5M3U8TicketBindIP {
+ ticket.IPHash = h5M3U8Hash(ip)
+ }
+ encodedTicket, err := encodeH5M3U8Ticket(ticket, key)
+ if err != nil {
+ log.Error("【BuildH5M3U8LegacyURL】生成H5播放票据失败", log.E(err))
+ return ""
+ }
+ query := url.Values{}
+ query.Set("id", strconv.FormatInt(mediaID, 10))
+ query.Set("t", strconv.FormatInt(expireAt, 10))
+ query.Set("ticket", encodedTicket)
+ return normalizedPath + "?" + query.Encode()
+}
+
+// CheckH5M3U8Ticket 校验 IP、视频、路径、有效期,防止伪造正片计数语义。
+func CheckH5M3U8Ticket(mediaID, expireAt int64, path, ip, ticket string) (*H5M3U8Ticket, bool) {
+ data, reason := validateH5M3U8Ticket(mediaID, expireAt, path, ip, ticket, true)
+ if reason != "" {
+ return nil, false
+ }
+ return data, true
+}
+
+// CheckH5M3U8TicketForEnkey 校验 /enkey 请求,不校验 m3u8 路径。
+func CheckH5M3U8TicketForEnkey(mediaID, expireAt int64, ip, ticket string) (*H5M3U8Ticket, bool) {
+ data, reason := validateH5M3U8Ticket(mediaID, expireAt, "", ip, ticket, false)
+ if reason != "" {
+ return nil, false
+ }
+ return data, true
+}
+
+// CheckH5M3U8TicketReason 与 CheckH5M3U8Ticket 相同,额外返回失败原因便于排查保底视频。
+func CheckH5M3U8TicketReason(mediaID, expireAt int64, path, ip, ticket string) (*H5M3U8Ticket, bool, string) {
+ data, reason := validateH5M3U8Ticket(mediaID, expireAt, path, ip, ticket, true)
+ if reason != "" {
+ return nil, false, reason
+ }
+ return data, true, ""
+}
+
+func validateH5M3U8Ticket(mediaID, expireAt int64, path, ip, ticket string, checkPath bool) (*H5M3U8Ticket, string) {
+ data, reason := decodeAndValidateH5M3U8TicketReason(mediaID, expireAt, ip, ticket)
+ if reason != "" {
+ return nil, reason
+ }
+ if !checkPath {
+ return data, ""
+ }
+ normalizedPath, pathOK := normalizeH5M3U8Path(path)
+ if !pathOK {
+ return nil, "path_invalid"
+ }
+ payloadPath, valid := normalizeH5M3U8Path(data.Path)
+ if !valid || payloadPath != normalizedPath {
+ return nil, "path_mismatch"
+ }
+ return data, ""
+}
+
+func decodeAndValidateH5M3U8Ticket(mediaID, expireAt int64, ip, ticket string) (*H5M3U8Ticket, bool) {
+ data, reason := decodeAndValidateH5M3U8TicketReason(mediaID, expireAt, ip, ticket)
+ if reason != "" {
+ return nil, false
+ }
+ return data, true
+}
+
+func decodeAndValidateH5M3U8TicketReason(mediaID, expireAt int64, ip, ticket string) (*H5M3U8Ticket, string) {
+ key := h5M3U8TicketKey()
+ if key == "" || mediaID <= 0 || expireAt <= 0 || strings.TrimSpace(ticket) == "" {
+ return nil, "param_missing"
+ }
+ data, err := decodeH5M3U8Ticket(ticket, key)
+ if err != nil {
+ return nil, "ticket_decode_failed"
+ }
+ if data.MediaID != mediaID || data.ExpireAt != expireAt {
+ return nil, "id_or_expire_mismatch"
+ }
+ if data.KeyVersion != "" && data.KeyVersion != h5M3U8TicketKeyVersion {
+ return nil, "key_version_mismatch"
+ }
+ if time.Now().Unix() > data.ExpireAt {
+ return nil, "ticket_expired"
+ }
+ if h5M3U8TicketBindIP && data.IPHash != h5M3U8Hash(ip) {
+ return nil, "ip_mismatch"
+ }
+ return data, ""
+}
+
+func H5M3U8RequestIP(ctx context.Context) string {
+ if c, ok := ctx.(*gin.Context); ok {
+ return strings.TrimSpace(ipaddr.ResolveClientIP(c))
+ }
+ return ""
+}
+
+func normalizeH5M3U8TicketParam(ticket string) string {
+ ticket = strings.TrimSpace(ticket)
+ if idx := strings.Index(ticket, "?"); idx >= 0 {
+ ticket = ticket[:idx]
+ }
+ return ticket
+}
+
+func normalizeH5M3U8Path(path string) (string, bool) {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return "", false
+ }
+ lowerPath := strings.ToLower(path)
+ if strings.HasPrefix(lowerPath, "http://") || strings.HasPrefix(lowerPath, "https://") {
+ return "", false
+ }
+ if idx := strings.Index(path, "?"); idx >= 0 {
+ path = path[:idx]
+ }
+ path = strings.TrimLeft(path, "/")
+ path = strings.TrimPrefix(path, strings.TrimLeft(h5M3U8RoutePrefix, "/"))
+ lowerPath = strings.ToLower(path)
+ if path == "" || strings.Contains(path, `\`) || strings.Contains(path, "..") || strings.Contains(lowerPath, "%2e") {
+ return "", false
+ }
+ if !strings.HasSuffix(lowerPath, ".m3u8") {
+ return "", false
+ }
+ return path, true
+}
+
+func encodeH5M3U8Ticket(ticket H5M3U8Ticket, key string) (string, error) {
+ data, err := json.Marshal(ticket)
+ if err != nil {
+ return "", err
+ }
+ encrypted, err := crypt.CoreAesEncryptEx(data, 12, key)
+ if err != nil {
+ return "", err
+ }
+ return base64.URLEncoding.EncodeToString(encrypted), nil
+}
+
+func decodeH5M3U8Ticket(ticket, key string) (*H5M3U8Ticket, error) {
+ ticket = normalizeH5M3U8TicketParam(ticket)
+ encrypted, err := base64.URLEncoding.DecodeString(ticket)
+ if err != nil {
+ encrypted, err = base64.RawURLEncoding.DecodeString(ticket)
+ }
+ if err != nil {
+ return nil, err
+ }
+ decrypted, err := safeCoreAesDecryptEx(encrypted, 12, key)
+ if err != nil {
+ return nil, err
+ }
+ var data H5M3U8Ticket
+ if err = json.Unmarshal(decrypted, &data); err != nil {
+ return nil, err
+ }
+ return &data, nil
+}
+
+func safeCoreAesDecryptEx(encrypted []byte, nonceLen int, key string) (decrypted []byte, err error) {
+ defer func() {
+ if recovered := recover(); recovered != nil {
+ err = fmt.Errorf("decrypt h5 m3u8 ticket panic: %v", recovered)
+ }
+ }()
+ return crypt.CoreAesDecryptEx(encrypted, nonceLen, key)
+}
+
+func h5M3U8Hash(value string) string {
+ hash := sha256.Sum256([]byte(strings.TrimSpace(value)))
+ return hex.EncodeToString(hash[:])
+}
diff --git a/app/service/videoser/like_list.go b/app/service/videoser/like_list.go
new file mode 100644
index 0000000..bf47902
--- /dev/null
+++ b/app/service/videoser/like_list.go
@@ -0,0 +1,69 @@
+package videoser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videolikemod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// QueryLikeVideoList 按点赞时间倒序查询当前用户点赞的上架视频
+func QueryLikeVideoList(ctx context.Context, userID int64, req *proto.QueryLikeVideoListReq) (*proto.QueryLikeVideoListResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
+ pipeline := []bson.M{
+ {"$match": bson.M{videolikemod.FN.VideoLike.UserID: userID}},
+ {"$lookup": bson.M{
+ "from": videoinformationmod.TN.VideoInformation,
+ "localField": videolikemod.FN.VideoLike.VideoID,
+ "foreignField": videoinformationmod.FN.VideoInformation.Id,
+ "as": "video",
+ }},
+ {"$unwind": "$video"},
+ {"$match": bson.M{"video." + videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
+ }
+ countRows := make([]struct {
+ Total int64 `bson:"total"`
+ }, 0)
+ if err := videolikemod.AggregateVideoLike(ctx, &countRows, append(append([]bson.M(nil), pipeline...), bson.M{"$count": "total"})); err != nil {
+ return nil, err
+ }
+ total := int64(0)
+ if len(countRows) > 0 {
+ total = countRows[0].Total
+ }
+ resp := &proto.QueryLikeVideoListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.FavoriteVideoListItem, 0),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ pagePipeline := append([]bson.M{pipeline[0], {"$sort": bson.D{{Key: videolikemod.FN.VideoLike.CreatedAt, Value: -1}, {Key: videolikemod.FN.VideoLike.Id, Value: -1}}}}, pipeline[1:]...)
+ pagePipeline = append(pagePipeline,
+ bson.M{"$skip": (pageNum - 1) * pageSize},
+ bson.M{"$limit": pageSize},
+ bson.M{"$replaceRoot": bson.M{"newRoot": "$video"}},
+ bson.M{"$project": favoriteVideoProjection()},
+ )
+ list := make([]*videoinformationmod.VideoInformation, 0)
+ if err := videolikemod.AggregateVideoLike(ctx, &list, pagePipeline); err != nil {
+ return nil, err
+ }
+ cdn := configser.GetRequestConfig(ctx).ImgCdn
+ items := make([]*proto.FavoriteVideoListItem, 0, len(list))
+ for _, item := range list {
+ items = append(items, favoriteVideoListItem(item, cdn))
+ }
+ resp.List = items
+ return resp, nil
+}
diff --git a/app/service/videoser/m3u8.go b/app/service/videoser/m3u8.go
new file mode 100644
index 0000000..1b67943
--- /dev/null
+++ b/app/service/videoser/m3u8.go
@@ -0,0 +1,386 @@
+package videoser
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/m3u8"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/m3u8requestlogmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson"
+
+ "github.com/gin-gonic/gin"
+)
+
+const m3u8RateLimitFallback = H5M3U8FallbackPath
+
+type fileHandler func(ctx *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error)
+
+func Enkey(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ idStr := c.Query("id")
+ tStr := c.Query("t")
+ ticket := c.Query("ticket")
+
+ if idStr == "" || tStr == "" || ticket == "" {
+ err = errors.New("unauthorized")
+ return
+ }
+ videoId, parseErr := strconv.ParseInt(idStr, 10, 64)
+ if parseErr != nil {
+ err = errors.New("invalid id")
+ return
+ }
+ tVal, parseErr := strconv.ParseInt(tStr, 10, 64)
+ if parseErr != nil {
+ err = errors.New("invalid t")
+ return
+ }
+
+ ip := ipaddr.ResolveClientIP(c)
+ ticketInfo, ticketOK := CheckH5M3U8TicketForEnkey(videoId, tVal, ip, ticket)
+ if !ticketOK || ticketInfo.UseNewKey {
+ log.Warn("Enkey: ticket invalid", log.Any("ip", ip), log.Any("videoId", videoId))
+ err = errors.New("ticket check failed")
+ return
+ }
+ if ticketInfo.OfficialPlayletID > 0 && !officialChapterAllowed(c, ticketInfo.OfficialPlayletID, videoId) {
+ err = errors.New("本集暂未在官网开放")
+ return
+ }
+
+ buffer, err := m3u8.Enkey(c)
+ if err != nil {
+ return
+ }
+ contentLength = int64(buffer.Len())
+ contentType = "application/octet-stream"
+ reader = buffer
+ extraHeaders = map[string]string{"Cache-Control": "private, no-store"}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+func handlerFile(ctx *gin.Context, h fileHandler) {
+ contentLength, contentType, reader, extraHeaders, err := h(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.DownloadM3u8File, err)
+ return
+ }
+ ctx.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
+}
+
+func GetM3U8Ex(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ start := time.Now()
+ m3u8Name := ctx.Param("key")
+ origM3u8Name := m3u8Name
+ status := "success"
+ var mediaID int64
+ var expireAt int64
+ var ticketOK bool
+ var isOverseas bool
+ var cdnDomain string
+
+ ip := ipaddr.ResolveClientIP(c)
+ detector := appg.DetectorV6
+ if ipaddr.IsIPv4(ip) {
+ detector = appg.DetectorV4
+ }
+ if detector != nil {
+ isOverseas, _ = detector.IsOverseasIP(ip)
+ }
+
+ defer func() {
+ if err != nil && status == "success" {
+ status = "error"
+ }
+ recordM3U8RequestLog(c, m3u8Name, mediaID, expireAt, ticketOK, isOverseas, cdnDomain, status, err, start)
+ }()
+
+ ticket := ctx.Query("ticket")
+ mediaID = parseInt64(ctx.Query("id"))
+ expireAt = parseInt64(ctx.Query("t"))
+ ticketInfo, ticketOK, ticketReason := CheckH5M3U8TicketReason(
+ mediaID, expireAt, origM3u8Name, ip, ticket,
+ )
+
+ if !ticketOK {
+ m3u8Name = H5M3U8FallbackPath
+ log.Warn("【GetM3U8Ex】H5播放票据无效",
+ log.Any("reason", ticketReason),
+ log.Any("mediaId", mediaID),
+ log.Any("expireAt", expireAt),
+ log.Any("ip", ip),
+ log.Any("clientIP", c.ClientIP()),
+ log.Any("remoteIP", c.RemoteIP()),
+ log.Any("xff", c.GetHeader("X-Forwarded-For")),
+ log.Any("cfIP", c.GetHeader("CF-Connecting-IP")),
+ log.Any("path", origM3u8Name),
+ log.Any("hasTicket", ticket != ""))
+ }
+ if ticketOK && ticketInfo.OfficialPlayletID > 0 && !officialChapterAllowed(c, ticketInfo.OfficialPlayletID, mediaID) {
+ status = "official_closed"
+ err = errors.New("本集暂未在官网开放")
+ return
+ }
+
+ countTowardDailyLimit := ticketOK && ticketInfo.Playable && !ticketInfo.Preview
+ if countTowardDailyLimit && appg.Conf.RunMod == string(consts.RunModeProd) {
+ cnt, countErr := m3u8DailyMediaCountByIP(ip, mediaID)
+ if countErr != nil {
+ log.Error("【GetM3U8Ex】统计IP正片获取次数失败", log.Any("ip", ip), log.E(countErr))
+ } else if cnt > configser.GetM3U8DayRateLimitMax() {
+ m3u8Name = m3u8RateLimitFallback
+ status = "rate_limited"
+ log.Warn("GetM3U8Ex daily rate limit, use fallback m3u8",
+ log.Any("ip", ip),
+ log.Any("remoteIP", c.RemoteIP()),
+ log.Any("clientIP", c.ClientIP()),
+ log.Any("xff", c.GetHeader("X-Forwarded-For")),
+ log.Any("cnt", cnt),
+ log.Any("mediaId", mediaID),
+ log.Any("orig", origM3u8Name),
+ log.Any("fallback", m3u8Name))
+ }
+ }
+
+ var req m3u8.Req
+ if err = ctx.BindQuery(&req); err != nil {
+ status = "bind_query_failed"
+ return
+ }
+ req.Name = m3u8Name
+ cdnDomain = configser.GetRandCDN()
+ originDomain := appg.Conf.OrigDomain
+ if isOverseas && len(configser.GetConfigInfo().MediaAppUsCdn) > 0 {
+ cdnDomain = configser.GetConfigInfo().MediaAppUsCdn
+ }
+ log.Info("cdn信息", log.Any("ip:", ip), log.Any("isOverseas", isOverseas), log.Any("cdnDomain", cdnDomain))
+
+ enkeyUri := ""
+ playOriginal := ticketOK && ticketInfo.UseNewKey && m3u8Name != H5M3U8FallbackPath
+ if ticketOK {
+ enkeyPath := "/enkey"
+ if playOriginal {
+ enkeyPath = "/newenkey"
+ }
+ enkeyUri = fmt.Sprintf("%s?id=%d&t=%d&ticket=%s", enkeyPath, mediaID, expireAt, url.QueryEscape(ticket))
+ }
+
+ var buff *bytes.Buffer
+ if playOriginal {
+ _, buff, err = m3u8.TransformExOriginalPath(req.Name, cdnDomain, originDomain, 2, appg.Conf.TsAuthKey, appg.Conf.LsjMediaConf.M3u8Yuan, appg.Conf.CdnAuthKey, enkeyUri)
+ } else {
+ _, buff, err = m3u8.TransformEx(req.Name, cdnDomain, originDomain, 2, appg.Conf.TsAuthKey, appg.Conf.LsjMediaConf.M3u8Yuan, appg.Conf.CdnAuthKey, enkeyUri)
+ }
+ if err != nil {
+ status = "transform_failed"
+ return
+ }
+ contentType = "application/vnd.apple.mpegurl"
+ reader = bytes.NewReader(buff.Bytes())
+ contentLength = int64(buff.Len())
+ extraHeaders = map[string]string{"Cache-Control": "private, no-store"}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+func parseInt64(value string) int64 {
+ n, _ := strconv.ParseInt(value, 10, 64)
+ return n
+}
+
+func m3u8DailyMediaCountByIP(ip string, mediaID int64) (int64, error) {
+ expire := redisconst.M3U8DayRateLimitExpire()
+ first, err := redis.Setnx_NewOK(redisconst.M3U8DailyMediaIpKey(ip, mediaID), 1, expire)
+ if err != nil {
+ return 0, err
+ }
+ countKey := redisconst.M3U8DayRateLimitIpKey(ip)
+ if first {
+ return redis.Incr(countKey, expire)
+ }
+ value, err := redis.Get(countKey)
+ if err != nil || value == nil {
+ return 0, err
+ }
+ return strconv.ParseInt(*value, 10, 64)
+}
+
+func GetM3U8AuthParam(videoId int64, urlPath, ip, ua string) string {
+ _ = ua
+ if signed := BuildH5M3U8LegacyURL(videoId, urlPath, ip, true, false); signed != "" {
+ return signed
+ }
+ return urlPath
+}
+
+// GetPlayletM3U8AuthParam 为短剧剧集地址签发票据。useNewKey 表示当前地址是原片。
+func GetPlayletM3U8AuthParam(chapterID int64, urlPath, ip string, useNewKey bool) string {
+ if signed := BuildH5M3U8PlayletURL(chapterID, urlPath, ip, useNewKey); signed != "" {
+ return signed
+ }
+ return urlPath
+}
+
+// GetM3U8OfficialParam 返回带官网开放范围校验的剧集播放地址
+func GetM3U8OfficialParam(chapterID, playletID int64, urlPath, ip string, useNewKey bool) string {
+ return BuildH5M3U8OfficialURL(chapterID, playletID, urlPath, ip, useNewKey)
+}
+
+// NewEnkey 返回短剧原片解密密钥。
+func NewEnkey(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ idStr := c.Query("id")
+ tStr := c.Query("t")
+ ticket := c.Query("ticket")
+ if idStr == "" || tStr == "" || ticket == "" {
+ err = errors.New("unauthorized")
+ return
+ }
+ videoId, parseErr := strconv.ParseInt(idStr, 10, 64)
+ if parseErr != nil {
+ err = errors.New("invalid id")
+ return
+ }
+ tVal, parseErr := strconv.ParseInt(tStr, 10, 64)
+ if parseErr != nil {
+ err = errors.New("invalid t")
+ return
+ }
+ ip := ipaddr.ResolveClientIP(c)
+ ticketInfo, ticketOK := CheckH5M3U8TicketForEnkey(videoId, tVal, ip, ticket)
+ if !ticketOK || !ticketInfo.UseNewKey {
+ err = errors.New("ticket check failed")
+ return
+ }
+ if ticketInfo.OfficialPlayletID > 0 && !officialChapterAllowed(c, ticketInfo.OfficialPlayletID, videoId) {
+ err = errors.New("本集暂未在官网开放")
+ return
+ }
+ buffer, err := m3u8.NewEnkey(c)
+ if err != nil {
+ return
+ }
+ contentLength = int64(buffer.Len())
+ contentType = "application/octet-stream"
+ reader = buffer
+ extraHeaders = map[string]string{"Cache-Control": "private, no-store"}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+// officialChapterAllowed 每次获取官网播放清单或密钥时重新读取开放范围
+func officialChapterAllowed(ctx context.Context, playletID, chapterID int64) bool {
+ if playletID <= 0 || chapterID <= 0 {
+ return false
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: playletID,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ })
+ if err != nil || playlet == nil {
+ return false
+ }
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: chapterID,
+ playletchaptermod.FN.PlayletChapter.PlayletId: playletID,
+ })
+ if err != nil || chapter == nil {
+ return false
+ }
+ return playlet.IsOfficialEpisodeOpen(chapter.CurrentEpisode)
+}
+
+func LsjEnkey(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ buffer, err := m3u8.LsjEnkey(c)
+ if err != nil {
+ return
+ }
+ contentLength = int64(buffer.Len())
+ contentType = "application/octet-stream"
+ reader = buffer
+ extraHeaders = map[string]string{}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+func recordM3U8RequestLog(ctx *gin.Context, m3u8Name string, videoId int64, t int64, ticketOK bool, isOverseas bool, cdnDomain string, status string, reqErr error, start time.Time) {
+ doc := &m3u8requestlogmod.M3U8RequestLog{
+ CreatedAt: time.Now(),
+ Host: ctx.Request.Host,
+ RequestURI: ctx.Request.RequestURI,
+ M3U8Path: m3u8Name,
+ RawQuery: ctx.Request.URL.RawQuery,
+ Query: cloneURLValues(ctx.Request.URL.Query()),
+ Headers: cloneHeader(ctx.Request.Header),
+ VideoId: videoId,
+ T: t,
+ Token: ctx.Query("ticket"),
+ ClientIP: ipaddr.ResolveClientIP(ctx),
+ RemoteAddr: ctx.Request.RemoteAddr,
+ XForwardedFor: ctx.GetHeader("X-Forwarded-For"),
+ XRealIP: ctx.GetHeader("X-Real-IP"),
+ CFConnectingIP: ctx.GetHeader("CF-Connecting-IP"),
+ UserAgent: ctx.Request.UserAgent(),
+ Referer: ctx.GetHeader("Referer"),
+ IsOverseas: isOverseas,
+ CDNDomain: cdnDomain,
+ TokenOK: ticketOK,
+ PathOK: true,
+ Status: status,
+ CostMs: time.Since(start).Milliseconds(),
+ }
+ if reqErr != nil {
+ doc.Error = reqErr.Error()
+ }
+
+ go func(data *m3u8requestlogmod.M3U8RequestLog) {
+ c, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+ if err1 := m3u8requestlogmod.InsertOneM3U8RequestLog(c, data); err1 != nil {
+ log.Warn("recordM3U8RequestLog insert failed", log.Any("status", data.Status), log.Any("path", data.M3U8Path))
+ }
+ }(doc)
+}
+
+func cloneHeader(header http.Header) map[string][]string {
+ return cloneStringValues(map[string][]string(header))
+}
+
+func cloneURLValues(values url.Values) map[string][]string {
+ return cloneStringValues(map[string][]string(values))
+}
+
+func cloneStringValues(values map[string][]string) map[string][]string {
+ result := make(map[string][]string, len(values))
+ for key, vals := range values {
+ copied := make([]string, len(vals))
+ copy(copied, vals)
+ result[key] = copied
+ }
+ return result
+}
diff --git a/app/service/videoser/video.go b/app/service/videoser/video.go
new file mode 100644
index 0000000..a7db6b6
--- /dev/null
+++ b/app/service/videoser/video.go
@@ -0,0 +1,1686 @@
+package videoser
+
+import (
+ "context"
+ "encoding/json"
+ "strconv"
+ "sync"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/moreser"
+ "videoweb/app/service/reportser"
+ "videoweb/app/service/slugser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videofavoritemod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videolikemod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ videoGuessMinPageSize int64 = 8
+ videoGuessMaxPageSize int64 = 100
+ homeVideoDataLimit int64 = 12
+ queryVideoDefaultPageSize int64 = 20
+ queryVideoMaxPageSize int64 = 100
+ queryVideoAdInterval int64 = 9
+ videoRecommendAdInterval int64 = 8
+ videoListAdInterval int64 = 5
+)
+
+// VideoCategoryList 查询全部已启用视频分类
+func VideoCategoryList(ctx context.Context) (*proto.VideoCategoryListResp, *srvdef.SrvErr) {
+ list, err := videocategorymod.FindManyVideoCategorys(ctx,
+ bson.M{videocategorymod.FN.VideoCategory.Status: true},
+ options.Find().
+ SetProjection(bson.D{
+ {Key: videocategorymod.FN.VideoCategory.Id, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.Name, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.Slug, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.Title, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.Desc, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.Rank, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.SeoInfo.Title, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.SeoInfo.Description, Value: 1},
+ {Key: videocategorymod.FN.VideoCategory.SeoInfo.Keyworld, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: videocategorymod.FN.VideoCategory.Rank, Value: -1},
+ {Key: videocategorymod.FN.VideoCategory.Id, Value: -1},
+ }),
+ )
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.VideoCategoryListResp{List: make([]proto.VideoCategoryListItem, 0, len(list))}
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ resp.List = append(resp.List, proto.VideoCategoryListItem{
+ Id: item.Id, Name: item.Name, Slug: item.Slug,
+ Title: item.Title, Desc: item.Desc, Rank: item.Rank, SeoInfo: item.SeoInfo,
+ })
+ }
+ return resp, nil
+}
+
+// VideoGuessList 分页查询猜你喜欢视频,按真实与后台配置观看量之和排序
+func VideoGuessList(ctx context.Context, req *proto.VideoGuessListReq) (*proto.VideoGuessListResp, *srvdef.SrvErr) {
+ if req == nil || req.Id < 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "视频ID无效")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, videoGuessMinPageSize, videoGuessMaxPageSize)
+ if pageSize < videoGuessMinPageSize {
+ pageSize = videoGuessMinPageSize
+ }
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ if req.Id > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Id] = bson.M{"$ne": req.Id}
+ }
+ total, err := videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.VideoGuessListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total,
+ PageNum: int(pageNum),
+ PageSize: int(pageSize),
+ List: make([]*proto.VideoRecommendListItem, 0),
+ }}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ list := make(videoinformationmod.VideoInformationList, 0)
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &list, videoGuessPipeline(filter, (pageNum-1)*pageSize, pageSize)); err != nil {
+ return nil, err
+ }
+ cdn := configser.ImageCdnFromCtx(ctx)
+ items := make([]*proto.VideoRecommendListItem, 0, len(list))
+ for _, item := range list {
+ if item != nil {
+ items = append(items, videoToRecommendListItem(item, cdn))
+ }
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// videoGuessPipeline 按视频展示观看量排序,并使用ID稳定同分顺序
+func videoGuessPipeline(filter bson.M, skip, limit int64) []bson.M {
+ return []bson.M{
+ {"$match": filter},
+ {"$addFields": bson.M{"watchScore": bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Real.WatchTimes, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Fake.WatchTimes, 0}},
+ }}}},
+ {"$sort": bson.D{{Key: "watchScore", Value: -1}, {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1}}},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$project": queryVideoProjection()},
+ }
+}
+
+// videoRankAggItem 视频排行榜聚合结果,只保留榜单展示需要的轻量字段
+type videoRankAggItem struct {
+ Id int64 `bson:"_id"`
+ Title string `bson:"title"`
+ Slug string `bson:"slug"`
+ AddedTime time.Time `bson:"addedTime"`
+ UpdatedAt time.Time `bson:"updatedAt"`
+ CoverImg string `bson:"coverImg"`
+ Tags []int64 `bson:"tags"`
+ Real pubdef.Flow `bson:"real"`
+ Fake pubdef.FakeFlow `bson:"fake"`
+ RankScore int64 `bson:"rankScore"`
+}
+
+// VideoList 分页查询已上架视频,每五条视频后穿插一条广告
+func VideoList(ctx context.Context, req *proto.VideoListReq) (*proto.VideoListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ if req.CategoryId < 0 || (req.SortType != 0 && req.SortType != 1 && req.SortType != 2) {
+ return nil, srvdef.New(srvdef.ErrParamError, "分类或排序方式无效", "")
+ }
+ filter := videoListFilter(req.CategoryId)
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
+ videoCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ advitise := configser.GetAdvitise()
+ ads := advitise.PostList
+ adCount := videoListInsertedAdsCount(videoCount, len(ads))
+ mixedCount := videoCount + adCount
+ resp := &proto.VideoListResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: mixedCount, PageNum: int(pageNum), PageSize: int(pageSize),
+ List: make([]*proto.VideoRecommendListItem, 0),
+ },
+ Advitise: advitise,
+ }
+ start := (pageNum - 1) * pageSize
+ if start >= mixedCount {
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedCount {
+ end = mixedCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, videoCount, adCount, videoListAdInterval)
+ list := make(videoinformationmod.VideoInformationList, 0)
+ if contentLimit > 0 {
+ list, err = findVideoRecommendPage(ctx, filter, videoListQuerySort(req.SortType), contentStart, contentLimit)
+ if err != nil {
+ return nil, err
+ }
+ }
+ resp.List = buildVideoListMixedPage(list, ads, configser.GetRequestConfig(ctx).ImgCdn,
+ start, end, contentStart, adCount)
+ return resp, nil
+}
+
+// videoListFilter 按上架状态和分类构建筛选条件
+func videoListFilter(categoryId int64) bson.M {
+ filter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}
+ if categoryId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CategoryId] = categoryId
+ }
+ return filter
+}
+
+// videoListSort 根据热度或最近更新时间生成稳定的视频顺序
+func videoListSort(sortType int64) bson.D {
+ if sortType == 2 {
+ return bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+ }
+ return bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+}
+
+// videoListQuerySort 将后台热度置于原观看数顺序之前
+func videoListQuerySort(sortType int64) bson.D {
+ sort := videoListSort(sortType)
+ if sortType == 2 {
+ return sort
+ }
+ return append(bson.D{{Key: videoinformationmod.FN.VideoInformation.HeatValue, Value: -1}}, sort...)
+}
+
+// videoListInsertedAdsCount 只在仍有后续视频时插入广告,素材不足时循环使用
+func videoListInsertedAdsCount(videoCount int64, adsCount int) int64 {
+ if videoCount <= 1 || adsCount == 0 {
+ return 0
+ }
+ return (videoCount - 1) / videoListAdInterval
+}
+
+// buildVideoListMixedPage 根据全局位置组装视频与广告,保持跨页间隔一致
+func buildVideoListMixedPage(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string,
+ start, end, contentStart, adCount int64) []*proto.VideoRecommendListItem {
+ items := make([]*proto.VideoRecommendListItem, 0, end-start)
+ contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
+ for i, item := range list {
+ contentByIndex[contentStart+int64(i)] = item
+ }
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adCount, videoListAdInterval)
+ if adIndex >= 0 && len(ads) > 0 {
+ items = append(items, videoAdToRecommendListItem(ads[adIndex%int64(len(ads))], cdn))
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adCount, videoListAdInterval)
+ if item := contentByIndex[contentIndex]; item != nil {
+ items = append(items, videoToRecommendListItem(item, cdn))
+ }
+ }
+ return items
+}
+
+type videoRankCount struct {
+ Total int64 `bson:"total"`
+}
+
+// incrementVideoWatchCount 更新视频总观看、展示和日/周/月/年窗口观看数。
+// 周/月/年窗口按北京时间切:周一 00:00、每月 1 日 00:00、每年 1 月 1 日 00:00。
+func incrementVideoWatchCount(videoInfo *videoinformationmod.VideoInformation) {
+ if videoInfo == nil {
+ return
+ }
+ now := utils.Now()
+ incDoc := bson.M{
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.ShowTimes: 1,
+ }
+ setDoc := bson.M{
+ videoinformationmod.FN.VideoInformation.RealUpdateAt: now,
+ }
+ if videoInfo.RealUpdateAt.After(utils.BeginOfToday()) {
+ incDoc[videoinformationmod.FN.VideoInformation.Real.TodayWatchs] = 1
+ } else {
+ setDoc[videoinformationmod.FN.VideoInformation.Real.TodayWatchs] = 1
+ }
+ if videoInfo.RealUpdateAt.After(utils.BeginOfWeekByTime(now)) {
+ incDoc[videoinformationmod.FN.VideoInformation.Real.WeekWatchs] = 1
+ } else {
+ setDoc[videoinformationmod.FN.VideoInformation.Real.WeekWatchs] = 1
+ }
+ if videoInfo.RealUpdateAt.After(utils.BeginOfMonth(now)) {
+ incDoc[videoinformationmod.FN.VideoInformation.Real.MonthWatchs] = 1
+ } else {
+ setDoc[videoinformationmod.FN.VideoInformation.Real.MonthWatchs] = 1
+ }
+ if videoInfo.RealUpdateAt.After(utils.BeginOfYear(now)) {
+ incDoc[videoinformationmod.FN.VideoInformation.Real.YearWatchs] = 1
+ } else {
+ setDoc[videoinformationmod.FN.VideoInformation.Real.YearWatchs] = 1
+ }
+ if err := videoinformationmod.UpdateOneVideoInformation(context.Background(), bson.M{
+ videoinformationmod.FN.VideoInformation.Id: videoInfo.Id,
+ }, bson.M{"$set": setDoc, "$inc": incDoc}); err != nil {
+ log.Warn("incrementVideoWatchCount", log.Any("videoId", videoInfo.Id), log.E(err))
+ }
+}
+
+// HomeVideoDataForHome 查询总首页推荐、最新、最热三组视频数据
+func HomeVideoDataForHome(ctx context.Context, imgCdn string) (proto.HomeVideoDataResp, *srvdef.SrvErr) {
+ resp := proto.HomeVideoDataResp{
+ RecommendList: make([]proto.VideoInfo, 0),
+ NewestList: make([]proto.VideoInfo, 0),
+ HotList: make([]proto.VideoInfo, 0),
+ }
+ var (
+ recommendList videoinformationmod.VideoInformationList
+ newestList videoinformationmod.VideoInformationList
+ hotList videoinformationmod.VideoInformationList
+ wg sync.WaitGroup
+ mu sync.Mutex
+ err1 *srvdef.SrvErr
+ )
+ setErr := func(err *srvdef.SrvErr) {
+ if err == nil {
+ return
+ }
+ mu.Lock()
+ defer mu.Unlock()
+ if err1 == nil {
+ err1 = err
+ }
+ }
+ queryList := func(sortType videoinformationmod.VideoSortType, target *videoinformationmod.VideoInformationList) {
+ defer wg.Done()
+ list, err := homeVideoDataList(ctx, sortType)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ *target = list
+ }
+ wg.Add(3)
+ go queryList(videoinformationmod.VideoSortType_Recommend, &recommendList)
+ go queryList(videoinformationmod.VideoSortType_New, &newestList)
+ go queryList(videoinformationmod.VideoSortType_Hot, &hotList)
+ wg.Wait()
+ if err1 != nil {
+ return resp, err1
+ }
+ resp.RecommendList = videoListToHomeData(recommendList, imgCdn)
+ resp.NewestList = videoListToHomeData(newestList, imgCdn)
+ resp.HotList = videoListToHomeData(hotList, imgCdn)
+ return resp, nil
+}
+
+// QueryVideoMore 根据分类id分页查询视频列表,并按固定间隔穿插列表广告
+func QueryVideoMore(ctx context.Context, req *proto.QueryVideoReq) (*proto.QueryVideoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ categoryId, err := slugser.VideoCategoryID(ctx, req.CategoryId, req.CategorySlug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.CategoryId = categoryId
+ categoryInfo := videoCategoryInfo(req.CategoryId)
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
+ filter, sort, err := queryVideoFilterAndSort(ctx, req.CategoryId)
+ if err != nil {
+ return nil, err
+ }
+ config := configser.GetRequestConfig(ctx)
+ ads := configser.GetAdvitise().PostList
+ totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ if len(ads) == 0 {
+ resp, err := queryVideoMoreWithoutAds(ctx, filter, sort, req.CategoryId, pageNum, pageSize, totalCount, config.ImgCdn)
+ if resp != nil {
+ resp.CategoryInfo = categoryInfo
+ }
+ return resp, err
+ }
+ adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), queryVideoAdInterval)
+ mixedTotalCount := totalCount + adsTotalCount
+ start := (pageNum - 1) * pageSize
+ if start >= mixedTotalCount || totalCount == 0 {
+ resp := emptyQueryVideoMorePage(pageNum, pageSize, categoryInfo)
+ resp.TotalCount = mixedTotalCount
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedTotalCount {
+ end = mixedTotalCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, queryVideoAdInterval)
+ list := videoinformationmod.VideoInformationList{}
+ if contentLimit > 0 {
+ list, err = findVideoCategoryPage(ctx, filter, sort, req.CategoryId, contentStart, contentLimit)
+ if err != nil {
+ return nil, err
+ }
+ }
+ resp := buildQueryVideoMixedList(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
+ return &proto.QueryVideoResp{BasePageResp: response.BasePageResp{
+ TotalCount: mixedTotalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: resp,
+ }, CategoryInfo: categoryInfo, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
+}
+
+// VideoRecommendList 分页查询 app 端推荐视频列表,并按固定间隔穿插列表广告
+func VideoRecommendList(ctx context.Context, req *proto.VideoRecommendListReq) (*proto.VideoRecommendListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ categoryId, err := slugser.VideoCategoryID(ctx, req.CategoryId, req.CategorySlug, false)
+ if err != nil {
+ return nil, err
+ }
+ tagId, err := slugser.ContentTagID(ctx, req.TagId, req.TagSlug, false)
+ if err != nil {
+ return nil, err
+ }
+ req.CategoryId = categoryId
+ req.TagId = tagId
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
+ filter, sort, err := videoRecommendFilterAndSort(ctx, req.CategoryId, req.TagId, req.SortList)
+ if err != nil {
+ return nil, err
+ }
+ config := configser.GetRequestConfig(ctx)
+ ads := configser.GetAdvitise().PostList
+ totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ if len(ads) == 0 {
+ return videoRecommendListWithoutAds(ctx, filter, sort, req.CategoryId, pageNum, pageSize, totalCount, config.ImgCdn)
+ }
+ adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), videoRecommendAdInterval)
+ mixedTotalCount := totalCount + adsTotalCount
+ start := (pageNum - 1) * pageSize
+ if start >= mixedTotalCount || totalCount == 0 {
+ resp := emptyVideoRecommendListPage(pageNum, pageSize)
+ resp.TotalCount = mixedTotalCount
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedTotalCount {
+ end = mixedTotalCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, videoRecommendAdInterval)
+ list := videoinformationmod.VideoInformationList{}
+ if contentLimit > 0 {
+ list, err = findVideoCategoryPage(ctx, filter, sort, req.CategoryId, contentStart, contentLimit)
+ if err != nil {
+ return nil, err
+ }
+ }
+ items := buildVideoRecommendMixedList(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
+ return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
+ TotalCount: mixedTotalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }}, nil
+}
+
+// VideoTagList 根据统一标签ID分页查询视频列表,并按固定间隔穿插列表广告
+func VideoTagList(ctx context.Context, req *proto.ContentTagPageReq) (*proto.VideoRecommendListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
+ }
+ tagId, err := slugser.ContentTagID(ctx, req.Id, req.Slug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.Id = tagId
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, queryVideoDefaultPageSize, queryVideoMaxPageSize)
+ ok, err := configser.HasEnabledContentTagBind(ctx, req.Id, contenttagmod.ContentTagTypeVideo)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return emptyVideoRecommendListPage(pageNum, pageSize), nil
+ }
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.Tags: req.Id,
+ }
+ sort := bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+ config := configser.GetRequestConfig(ctx)
+ ads := configser.GetAdvitise().PostList
+ totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ if len(ads) == 0 {
+ return videoRecommendListWithoutAds(ctx, filter, sort, 0, pageNum, pageSize, totalCount, config.ImgCdn)
+ }
+ adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), videoRecommendAdInterval)
+ mixedTotalCount := totalCount + adsTotalCount
+ start := (pageNum - 1) * pageSize
+ if start >= mixedTotalCount || totalCount == 0 {
+ resp := emptyVideoRecommendListPage(pageNum, pageSize)
+ resp.TotalCount = mixedTotalCount
+ return resp, nil
+ }
+ end := start + pageSize
+ if end > mixedTotalCount {
+ end = mixedTotalCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, videoRecommendAdInterval)
+ list := videoinformationmod.VideoInformationList{}
+ if contentLimit > 0 {
+ list, err = findVideoRecommendPage(ctx, filter, sort, contentStart, contentLimit)
+ if err != nil {
+ return nil, err
+ }
+ }
+ items := buildVideoRecommendMixedList(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
+ return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
+ TotalCount: mixedTotalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }}, nil
+}
+
+// FavoriteVideo 当前登录用户收藏或取消收藏指定视频
+func FavoriteVideo(ctx context.Context, userId int64, ip string, ua useragent.UA, req *proto.FavoriteVideoReq) (*proto.FavoriteVideoResp, *srvdef.SrvErr) {
+ if req == nil || userId <= 0 || req.Id <= 0 || req.IsFavorite == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ videoInfo, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.Id,
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }, options.FindOne().SetProjection(bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.CategoryId: 1,
+ videoinformationmod.FN.VideoInformation.MediaType: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ if *req.IsFavorite {
+ created, err := videofavoritemod.UpsertOneVideoFavorite(ctx, userId, req.Id, time.Now())
+ if err != nil {
+ return nil, err
+ }
+ if created {
+ clearFavoriteVideoListCache(userId)
+ if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: 1}}); err != nil {
+ return nil, err
+ }
+ if err := reportser.ReportVideoCollect(userId, ua, ip, videoInfo, 1); err != nil {
+ log.Warn("ReportVideoCollect failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
+ }
+ }
+ } else {
+ deleted, err := videofavoritemod.DeleteOneVideoFavorite(ctx, bson.M{
+ videofavoritemod.FN.VideoFavorite.UserID: userId,
+ videofavoritemod.FN.VideoFavorite.VideoID: req.Id,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if deleted {
+ clearFavoriteVideoListCache(userId)
+ if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: -1}}); err != nil {
+ return nil, err
+ }
+ if err := reportser.ReportVideoCollect(userId, ua, ip, videoInfo, 2); err != nil {
+ log.Warn("ReportVideoCollect failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
+ }
+ }
+ }
+ return &proto.FavoriteVideoResp{Id: req.Id, IsFavorite: *req.IsFavorite}, nil
+}
+
+// LikeVideo 当前登录用户点赞或取消点赞指定视频
+func LikeVideo(ctx context.Context, userId int64, ip string, ua useragent.UA, req *proto.LikeVideoReq) (*proto.LikeVideoResp, *srvdef.SrvErr) {
+ if req == nil || userId <= 0 || req.Id <= 0 || req.IsLike == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ videoInfo, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.Id,
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }, options.FindOne().SetProjection(bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.CategoryId: 1,
+ videoinformationmod.FN.VideoInformation.MediaType: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Video.TaskId: 1,
+ videoinformationmod.FN.VideoInformation.Video.FileId: 1,
+ videoinformationmod.FN.VideoInformation.Video.Url: 1,
+ videoinformationmod.FN.VideoInformation.Video.H265Url: 1,
+ videoinformationmod.FN.VideoInformation.Video.VideoTime: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ if *req.IsLike {
+ created, err := videolikemod.UpsertOneVideoLike(ctx, userId, req.Id, time.Now())
+ if err != nil {
+ return nil, err
+ }
+ if created {
+ videoInfo.Real.Likes++
+ if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: 1}}); err != nil {
+ return nil, err
+ }
+ if err := reportser.ReportVideoLike(userId, ua, ip, videoInfo, 1); err != nil {
+ log.Warn("ReportVideoLike failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
+ }
+ }
+ } else {
+ deleted, err := videolikemod.DeleteOneVideoLike(ctx, bson.M{
+ videolikemod.FN.VideoLike.UserID: userId,
+ videolikemod.FN.VideoLike.VideoID: req.Id,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if deleted {
+ if videoInfo.Real.Likes > 0 {
+ videoInfo.Real.Likes--
+ if err := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: req.Id}, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Likes: -1}}); err != nil {
+ return nil, err
+ }
+ }
+ if err := reportser.ReportVideoLike(userId, ua, ip, videoInfo, 2); err != nil {
+ log.Warn("ReportVideoLike failed", log.Any("videoId", req.Id), log.Any("userId", userId), log.E(err))
+ }
+ }
+ }
+ return &proto.LikeVideoResp{
+ Id: req.Id,
+ IsLike: *req.IsLike,
+ LikeNum: videoInfo.Real.Likes + videoInfo.Fake.Likes,
+ }, nil
+}
+
+// QueryFavoriteVideoList 分页查询当前登录用户收藏的视频,按收藏时间倒序返回
+func QueryFavoriteVideoList(ctx context.Context, userId int64, req *proto.QueryFavoriteVideoListReq) (*proto.QueryFavoriteVideoListResp, *srvdef.SrvErr) {
+ if userId <= 0 || req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizeQueryVideoPage(req.PageNum, req.PageSize)
+ version := getFavoriteVideoListCacheVersion(userId)
+ cacheKey := redisconst.FavoriteVideoListCacheKey(userId, version, pageNum, pageSize)
+ if resp := getFavoriteVideoListCache(cacheKey); resp != nil {
+ return resp, nil
+ }
+ filter := bson.M{videofavoritemod.FN.VideoFavorite.UserID: userId}
+ totalCount, err := videofavoritemod.CountVideoFavorite(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ favorites, err := videofavoritemod.FindManyVideoFavorite(ctx, filter,
+ options.Find().
+ SetSort(bson.D{{Key: videofavoritemod.FN.VideoFavorite.CreatedAt, Value: -1}, {Key: videofavoritemod.FN.VideoFavorite.Id, Value: -1}}).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize).
+ SetProjection(bson.M{videofavoritemod.FN.VideoFavorite.VideoID: 1}))
+ if err != nil {
+ return nil, err
+ }
+ if len(favorites) == 0 {
+ resp := emptyFavoriteVideoPage(pageNum, pageSize)
+ resp.TotalCount = totalCount
+ setFavoriteVideoListCache(cacheKey, resp)
+ return resp, nil
+ }
+ ids := make([]int64, 0, len(favorites))
+ for _, favorite := range favorites {
+ ids = append(ids, favorite.VideoID)
+ }
+ videoList, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }, options.Find().SetProjection(favoriteVideoProjection()))
+ if err != nil {
+ return nil, err
+ }
+ videoMap := make(map[int64]*videoinformationmod.VideoInformation, len(videoList))
+ for _, item := range videoList {
+ videoMap[item.Id] = item
+ }
+ config := configser.GetRequestConfig(ctx)
+ list := make([]*proto.FavoriteVideoListItem, 0, len(favorites))
+ for _, favorite := range favorites {
+ item, ok := videoMap[favorite.VideoID]
+ if !ok {
+ continue
+ }
+ list = append(list, favoriteVideoListItem(item, config.ImgCdn))
+ }
+ resp := &proto.QueryFavoriteVideoListResp{BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: list,
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+ setFavoriteVideoListCache(cacheKey, resp)
+ return resp, nil
+}
+
+// QueryVideoRank 分页查询视频排行榜,读取独立的 10 分钟内存缓存。
+// 点赞榜按累计点赞量、周/月/年榜按对应周期观看量取前 50;不足时随机补位。分页 pageSize 不是 50 时强制为 50。
+func QueryVideoRank(ctx context.Context, req *proto.VideoRankReq) (*proto.VideoRankResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ period := proto.VideoRankPeriod(req.RankPeriod)
+ if period < proto.VideoRankPeriodLike || period > proto.VideoRankPeriodYear {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := configser.NormalizeRankPage(req.PageNum, req.PageSize)
+ all := configser.GetVideoRankList(configser.RankPeriod(period), 0)
+ return &proto.VideoRankResp{BasePageResp: response.BasePageResp{
+ TotalCount: int64(len(all)),
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: configser.SliceRankPage(all, pageNum, pageSize),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
+}
+
+// countVideoTotalLikeRank 统计累计点赞榜可展示视频数量
+func countVideoTotalLikeRank(ctx context.Context) (int64, *srvdef.SrvErr) {
+ rows := make([]videoRankCount, 0)
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &rows, videoTotalLikeRankCountPipeline()); err != nil {
+ return 0, err
+ }
+ if len(rows) == 0 {
+ return 0, nil
+ }
+ return rows[0].Total, nil
+}
+
+// aggregateVideoTotalLikeRank 分页查询累计点赞榜视频
+func aggregateVideoTotalLikeRank(ctx context.Context, skip, limit int64) ([]videoRankAggItem, *srvdef.SrvErr) {
+ list := make([]videoRankAggItem, 0)
+ if err := videoinformationmod.AggregateVideoInformations(ctx, &list, videoTotalLikeRankPagePipeline(skip, limit)); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// countVideoPeriodLikeRank 统计周期新增点赞榜可展示视频数量
+func countVideoPeriodLikeRank(ctx context.Context, since time.Time) (int64, *srvdef.SrvErr) {
+ rows := make([]videoRankCount, 0)
+ if err := videolikemod.AggregateVideoLike(ctx, &rows, videoPeriodLikeRankCountPipeline(since)); err != nil {
+ return 0, err
+ }
+ if len(rows) == 0 {
+ return 0, nil
+ }
+ return rows[0].Total, nil
+}
+
+// aggregateVideoPeriodLikeRank 分页查询周期新增点赞榜视频
+func aggregateVideoPeriodLikeRank(ctx context.Context, since time.Time, skip, limit int64) ([]videoRankAggItem, *srvdef.SrvErr) {
+ list := make([]videoRankAggItem, 0)
+ if err := videolikemod.AggregateVideoLike(ctx, &list, videoPeriodLikeRankPagePipeline(since, skip, limit)); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// getFavoriteVideoListCache 读取当前用户收藏视频分页缓存,缓存异常时降级查库
+func getFavoriteVideoListCache(cacheKey string) *proto.QueryFavoriteVideoListResp {
+ cacheData, err := redis.Get(cacheKey)
+ if err != nil {
+ log.Warn("读取视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
+ return nil
+ }
+ if cacheData == nil || *cacheData == "" {
+ return nil
+ }
+ resp := &proto.QueryFavoriteVideoListResp{}
+ if err := json.Unmarshal([]byte(*cacheData), resp); err != nil {
+ log.Warn("解析视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
+ return nil
+ }
+ return resp
+}
+
+// setFavoriteVideoListCache 写入当前用户收藏视频分页缓存,缓存异常不影响主流程
+func setFavoriteVideoListCache(cacheKey string, resp *proto.QueryFavoriteVideoListResp) {
+ if resp == nil {
+ return
+ }
+ bytes, err := json.Marshal(resp)
+ if err != nil {
+ log.Warn("序列化视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
+ return
+ }
+ if err := redis.Set(cacheKey, string(bytes), redisconst.FavoriteVideoListKeyExpire); err != nil {
+ log.Warn("写入视频收藏列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
+ }
+}
+
+// getFavoriteVideoListCacheVersion 读取当前用户视频收藏列表缓存版本,读取失败时使用默认版本降级查库
+func getFavoriteVideoListCacheVersion(userId int64) int64 {
+ cacheKey := redisconst.FavoriteVideoListVersionCacheKey(userId)
+ cacheData, err := redis.Get(cacheKey)
+ if err != nil {
+ log.Warn("读取视频收藏列表缓存版本失败", log.Any("cacheKey", cacheKey), log.E(err))
+ return 0
+ }
+ if cacheData == nil || *cacheData == "" {
+ return 0
+ }
+ version, err := strconv.ParseInt(*cacheData, 10, 64)
+ if err != nil {
+ log.Warn("解析视频收藏列表缓存版本失败", log.Any("cacheKey", cacheKey), log.Any("cacheData", *cacheData), log.E(err))
+ return 0
+ }
+ return version
+}
+
+// clearFavoriteVideoListCache 刷新当前用户视频收藏列表缓存版本,旧分页缓存等待TTL自然过期
+func clearFavoriteVideoListCache(userId int64) {
+ if userId <= 0 {
+ return
+ }
+ cacheKey := redisconst.FavoriteVideoListVersionCacheKey(userId)
+ if _, err := redis.Incr(cacheKey, redisconst.FavoriteListVersionKeyExpire); err != nil {
+ log.Warn("刷新视频收藏列表缓存版本失败", log.Any("cacheKey", cacheKey), log.E(err))
+ }
+}
+
+// QueryVideoInfo 查询视频详情、收藏状态和点赞状态
+func QueryVideoInfo(ctx context.Context, userId int64, ip, ua string, req *proto.QueryVideoInfoReq) (*proto.QueryVideoInfoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ videoId, err := slugser.VideoID(ctx, req.Id, req.Slug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.Id = videoId
+ config := configser.GetRequestConfig(ctx)
+ videoInfo, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.Id,
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }, options.FindOne().SetProjection(videoDetailProjection()))
+ if err != nil {
+ return nil, err
+ }
+ // 播放详情访问量异步累加,避免写操作阻塞详情读取链路
+ go incrementVideoWatchCount(videoInfo)
+ // 并发查询收藏状态、点赞状态和收藏总数
+ isFavorite, isLike, countCollect, err := queryVideoDetailExtraData(ctx, userId, videoInfo.Id)
+ if err != nil {
+ return nil, err
+ }
+ return &proto.QueryVideoInfoResp{
+ Path: videoRequestPath(ctx),
+ Translations: translationser.GetTranslationData(),
+ Config: config,
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Advitise: configser.GetAdvitise(),
+ Announcement: configser.GetAnnouncement(),
+ VideoInfo: videoDetailInfoData(videoInfo, config.ImgCdn, ip, ua, isFavorite, isLike, countCollect),
+ IsFavorite: isFavorite,
+ IsLike: isLike,
+ }, nil
+}
+
+// queryVideoDetailExtraData 并发查询详情页附加数据,缩短详情接口整体等待时间
+func queryVideoDetailExtraData(ctx context.Context, userId, videoId int64) (bool, bool, int64, *srvdef.SrvErr) {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var firstErr *srvdef.SrvErr
+ isFavorite := false
+ isLike := false
+ countCollect := int64(0)
+ setErr := func(err *srvdef.SrvErr) {
+ if err == nil {
+ return
+ }
+ mu.Lock()
+ if firstErr == nil {
+ firstErr = err
+ }
+ mu.Unlock()
+ }
+
+ wg.Add(3)
+ go func() {
+ defer wg.Done()
+ data, err := queryVideoIsFavorite(ctx, userId, videoId)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ isFavorite = data
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ data, err := queryVideoIsLike(ctx, userId, videoId)
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ isLike = data
+ mu.Unlock()
+ }()
+ go func() {
+ defer wg.Done()
+ count, err := videofavoritemod.CountVideoFavorite(ctx, bson.M{
+ videofavoritemod.FN.VideoFavorite.VideoID: videoId,
+ })
+ if err != nil {
+ setErr(err)
+ return
+ }
+ mu.Lock()
+ countCollect = count
+ mu.Unlock()
+ }()
+ wg.Wait()
+ if firstErr != nil {
+ return false, false, 0, firstErr
+ }
+ return isFavorite, isLike, countCollect, nil
+}
+
+// queryVideoIsFavorite 判断当前登录用户是否已收藏该视频,未登录直接返回false
+func queryVideoIsFavorite(ctx context.Context, userId, videoId int64) (bool, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return false, nil
+ }
+ count, err := videofavoritemod.CountVideoFavorite(ctx, bson.M{
+ videofavoritemod.FN.VideoFavorite.UserID: userId,
+ videofavoritemod.FN.VideoFavorite.VideoID: videoId,
+ })
+ if err != nil {
+ return false, err
+ }
+ return count > 0, nil
+}
+
+// queryVideoIsLike 判断当前登录用户是否已点赞该视频,未登录直接返回false
+func queryVideoIsLike(ctx context.Context, userId, videoId int64) (bool, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return false, nil
+ }
+ count, err := videolikemod.CountVideoLike(ctx, bson.M{
+ videolikemod.FN.VideoLike.UserID: userId,
+ videolikemod.FN.VideoLike.VideoID: videoId,
+ })
+ if err != nil {
+ return false, err
+ }
+ return count > 0, nil
+}
+
+// videoRankPeriodSince 将排行榜周期转换为点赞起始时间
+func videoRankPeriodSince(period proto.VideoRankPeriod) time.Time {
+ now := time.Now()
+ switch period {
+ case proto.VideoRankPeriodWeek:
+ return now.AddDate(0, 0, -7)
+ case proto.VideoRankPeriodMonth:
+ return now.AddDate(0, -1, 0)
+ case proto.VideoRankPeriodYear:
+ return now.AddDate(-1, 0, 0)
+ default:
+ return time.Time{}
+ }
+}
+
+// videoTotalLikeRankCountPipeline 构建累计点赞榜数量统计管道
+func videoTotalLikeRankCountPipeline() []bson.M {
+ return append(videoTotalLikeRankBasePipeline(), bson.M{"$count": "total"})
+}
+
+// videoTotalLikeRankPagePipeline 构建累计点赞榜分页查询管道
+func videoTotalLikeRankPagePipeline(skip, limit int64) []bson.M {
+ pipeline := videoTotalLikeRankBasePipeline()
+ pipeline = append(pipeline,
+ bson.M{"$sort": bson.D{
+ {Key: "rankScore", Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }},
+ bson.M{"$skip": skip},
+ bson.M{"$limit": limit},
+ bson.M{"$project": videoRankProjection()},
+ )
+ return pipeline
+}
+
+// videoTotalLikeRankBasePipeline 构建累计点赞榜基础过滤管道
+func videoTotalLikeRankBasePipeline() []bson.M {
+ return []bson.M{
+ {"$match": bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
+ {"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
+ bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Real.Likes, 0}},
+ bson.M{"$ifNull": []interface{}{"$" + videoinformationmod.FN.VideoInformation.Fake.Likes, 0}},
+ }}}},
+ {"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
+ }
+}
+
+// videoPeriodLikeRankCountPipeline 构建周期新增点赞榜数量统计管道
+func videoPeriodLikeRankCountPipeline(since time.Time) []bson.M {
+ return append(videoPeriodLikeRankBasePipeline(since), bson.M{"$count": "total"})
+}
+
+// videoPeriodLikeRankPagePipeline 构建周期新增点赞榜分页查询管道
+func videoPeriodLikeRankPagePipeline(since time.Time, skip, limit int64) []bson.M {
+ pipeline := videoPeriodLikeRankBasePipeline(since)
+ pipeline = append(pipeline,
+ bson.M{"$sort": bson.D{
+ {Key: "rankScore", Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }},
+ bson.M{"$skip": skip},
+ bson.M{"$limit": limit},
+ bson.M{"$project": videoRankProjection()},
+ )
+ return pipeline
+}
+
+// videoPeriodLikeRankBasePipeline 构建周期新增点赞榜基础过滤管道
+func videoPeriodLikeRankBasePipeline(since time.Time) []bson.M {
+ return []bson.M{
+ {"$match": bson.M{videolikemod.FN.VideoLike.CreatedAt: bson.M{"$gte": since}}},
+ {"$group": bson.M{
+ "_id": "$" + videolikemod.FN.VideoLike.VideoID,
+ "rankScore": bson.M{"$sum": 1},
+ }},
+ {"$lookup": bson.M{
+ "from": videoinformationmod.TN.VideoInformation,
+ "localField": "_id",
+ "foreignField": videoinformationmod.FN.VideoInformation.Id,
+ "as": "video",
+ }},
+ {"$unwind": "$video"},
+ {"$match": bson.M{"video." + videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
+ {"$replaceRoot": bson.M{"newRoot": bson.M{"$mergeObjects": []interface{}{"$video", bson.M{"rankScore": "$rankScore"}}}}},
+ }
+}
+
+// videoRankProjection 限定排行榜返回字段,减少分页查询的数据传输体积
+func videoRankProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ "rankScore": 1,
+ }
+}
+
+// videoRankToContentData 将排行榜聚合结果转换为榜单展示数据
+func videoRankToContentData(item videoRankAggItem, imgCdn string) proto.ContentData {
+ tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
+ return proto.ContentData{
+ Id: item.Id,
+ Title: item.Title,
+ Slug: item.Slug,
+ AddedTime: item.AddedTime,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Tags: item.Tags,
+ TagsName: tagNames,
+ TagSlugs: tagSlugs,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.RankScore,
+ }
+}
+
+// videoDetailInfoData 将视频模型转换为详情页专用响应结构,视频播放地址按m3u8授权格式返回
+func videoDetailInfoData(item *videoinformationmod.VideoInformation, imgCdn, ip, ua string, isFavorite, isLike bool, countCollect int64) proto.VideoDetailInfo {
+ tags := make([]proto.TagsInfoData, 0, len(item.Tags))
+ for _, tagId := range item.Tags {
+ tag := configser.GetVideoTag(tagId)
+ if tag != nil {
+ tags = append(tags, *tag)
+ }
+ }
+ video := item.Video
+ if video.Url != "" {
+ video.Url = GetM3U8AuthParam(item.Id, video.Url, ip, ua)
+ }
+ if video.H265Url != "" {
+ video.H265Url = GetM3U8AuthParam(item.Id, video.H265Url, ip, ua)
+ }
+ return proto.VideoDetailInfo{
+ Id: item.Id,
+ Title: item.Title,
+ Slug: item.Slug,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Video: video,
+ Tags: tags,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ CountCollect: countCollect,
+ AddedTime: item.AddedTime,
+ Desc: item.Desc,
+ IsFavorite: isFavorite,
+ IsLike: isLike,
+ }
+}
+
+// videoDetailProjection 限定视频详情查询字段,避免返回整条视频文档
+func videoDetailProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Video.TaskId: 1,
+ videoinformationmod.FN.VideoInformation.Video.FileId: 1,
+ videoinformationmod.FN.VideoInformation.Video.Url: 1,
+ videoinformationmod.FN.VideoInformation.Video.H265Url: 1,
+ videoinformationmod.FN.VideoInformation.Video.Cover: 1,
+ videoinformationmod.FN.VideoInformation.Video.Height: 1,
+ videoinformationmod.FN.VideoInformation.Video.Width: 1,
+ videoinformationmod.FN.VideoInformation.Video.VideoTime: 1,
+ videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs: 1,
+ videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.Desc: 1,
+ videoinformationmod.FN.VideoInformation.CategoryId: 1,
+ videoinformationmod.FN.VideoInformation.RealUpdateAt: 1,
+ }
+}
+
+// homeVideoDataList 按首页视频排序类型查询固定数量的视频
+func homeVideoDataList(ctx context.Context, sortType videoinformationmod.VideoSortType) (videoinformationmod.VideoInformationList, *srvdef.SrvErr) {
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ if sortType == videoinformationmod.VideoSortType_Recommend {
+ filter[videoinformationmod.FN.VideoInformation.IsRecommend] = true
+ }
+ return videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
+ SetProjection(queryVideoProjection()).
+ SetSort(homeVideoDataSort(sortType)).
+ SetLimit(homeVideoDataLimit))
+}
+
+// homeVideoDataSort 将首页视频推荐、最新、最热转换为 MongoDB 排序条件
+func homeVideoDataSort(sortType videoinformationmod.VideoSortType) bson.D {
+ switch sortType {
+ case videoinformationmod.VideoSortType_Recommend:
+ return bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.WeekWatchs, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+ case videoinformationmod.VideoSortType_Hot:
+ return bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.HeatValue, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+ default:
+ return bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+ }
+}
+
+// videoListToHomeData 批量转换视频模型为 app 首页视频数据
+func videoListToHomeData(list videoinformationmod.VideoInformationList, imgCdn string) []proto.VideoInfo {
+ result := make([]proto.VideoInfo, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, homeVideoInfo(item, imgCdn))
+ }
+ return result
+}
+
+// homeVideoSort 将分类配置的排序方式转换为MG排序条件
+func homeVideoSort(sortType pubdef.CategorySortType) bson.D {
+ switch sortType {
+ case pubdef.CategorySortTypeAddedTimeDesc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.AddedTime, -1}}
+ case pubdef.CategorySortTypeWatchDesc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1}, {videoinformationmod.FN.VideoInformation.Fake.WatchTimes, -1}}
+ case pubdef.CategorySortTypeLikeDesc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.Real.Likes, -1}, {videoinformationmod.FN.VideoInformation.Fake.Likes, -1}}
+ case pubdef.CategorySortTypeAddedTimeAsc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.AddedTime, 1}}
+ case pubdef.CategorySortTypeWatchAsc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.Real.WatchTimes, 1}, {videoinformationmod.FN.VideoInformation.Fake.WatchTimes, 1}}
+ case pubdef.CategorySortTypeLikeAsc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.Real.Likes, 1}, {videoinformationmod.FN.VideoInformation.Fake.Likes, 1}}
+ case pubdef.CategorySortTypeUpdateTimeDesc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.UpdatedAt, -1}}
+ case pubdef.CategorySortTypeUpdateTimeAsc:
+ return bson.D{{videoinformationmod.FN.VideoInformation.UpdatedAt, 1}}
+ case pubdef.CategorySortTypeSortType:
+ return videoinformationmod.CategoryRankSort()
+ default:
+ return bson.D{{videoinformationmod.FN.VideoInformation.AddedTime, -1}}
+ }
+}
+
+// homeVideoInfo 将视频模型转换为首页轻量视频结构
+func homeVideoInfo(item *videoinformationmod.VideoInformation, imgCdn string) proto.VideoInfo {
+ // 标签名称来自本地配置缓存,避免为每条视频额外访问数据库
+ tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
+ // 返回
+ return proto.VideoInfo{ContentData: proto.ContentData{
+ Id: item.Id,
+ Title: item.Title,
+ Slug: item.Slug,
+ AddedTime: item.AddedTime,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Tags: item.Tags,
+ TagsName: tagNames,
+ TagSlugs: tagSlugs,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ SeoInfo: pubdef.SeoValue{
+ Title: item.Title,
+ Keyworld: append([]string{}, item.Keyworld...),
+ Description: item.Desc,
+ },
+ }}
+}
+
+// queryVideoFilterAndSort 根据视频分类构建更多列表查询条件和排序规则
+func queryVideoFilterAndSort(ctx context.Context, categoryID int64) (bson.M, bson.D, *srvdef.SrvErr) {
+ translationser.EnsureVideoCategoryCache(ctx)
+ category := translationser.GetVideoCategory(categoryID)
+ if category == nil || !category.Status {
+ return nil, nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": category.Tags}
+ return filter, homeVideoSort(category.SortType), nil
+}
+
+// queryVideoProjection 限定视频更多列表字段,避免返回详情页才需要的大字段
+func queryVideoProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.Keyworld: 1,
+ videoinformationmod.FN.VideoInformation.Desc: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ }
+}
+
+// videoRecommendFilterAndSort 根据分类标签、内容标签和推荐视频排序枚举构建查询条件
+func videoRecommendFilterAndSort(ctx context.Context, categoryId, tagId, sortList int64) (bson.M, bson.D, *srvdef.SrvErr) {
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ tagConditions := make([]bson.M, 0, 2)
+ if categoryId > 0 {
+ translationser.EnsureVideoCategoryCache(ctx)
+ category := translationser.GetVideoCategory(categoryId)
+ if category == nil || !category.Status {
+ return nil, nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ tagConditions = append(tagConditions, bson.M{videoinformationmod.FN.VideoInformation.Tags: bson.M{"$in": category.Tags}})
+ }
+ if tagId > 0 {
+ tagConditions = append(tagConditions, bson.M{videoinformationmod.FN.VideoInformation.Tags: tagId})
+ }
+ switch len(tagConditions) {
+ case 1:
+ for key, value := range tagConditions[0] {
+ filter[key] = value
+ }
+ case 2:
+ filter["$and"] = tagConditions
+ }
+ switch proto.VideoRecommendSort(sortList) {
+ case proto.VideoRecommendSortNewest:
+ return filter, bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }, nil
+ case proto.VideoRecommendSortHot:
+ return filter, bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.HeatValue, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }, nil
+ default:
+ if categoryId <= 0 {
+ filter[videoinformationmod.FN.VideoInformation.IsRecommend] = true
+ } else {
+ return filter, videoinformationmod.CategoryRankSort(), nil
+ }
+ return filter, bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.WeekWatchs, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }, nil
+ }
+}
+
+// findVideoRecommendPage 查询当前混排分页需要的视频数据
+func findVideoRecommendPage(ctx context.Context, filter bson.M, sort bson.D, skip, limit int64) (videoinformationmod.VideoInformationList, *srvdef.SrvErr) {
+ return videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
+ SetProjection(queryVideoProjection()).
+ SetSort(sort).
+ SetSkip(skip).
+ SetLimit(limit))
+}
+
+// findVideoCategoryPage 仅在分类自定义排序时计算当前分类排序值
+func findVideoCategoryPage(ctx context.Context, filter bson.M, sort bson.D, categoryId, skip, limit int64) (videoinformationmod.VideoInformationList, *srvdef.SrvErr) {
+ if categoryId > 0 && len(sort) > 0 && sort[0].Key == videoinformationmod.CategoryRankField {
+ return videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, categoryId, skip, limit, queryVideoProjection())
+ }
+ return findVideoRecommendPage(ctx, filter, sort, skip, limit)
+}
+
+// videoRecommendListWithoutAds 无广告时直接按推荐视频分页查询
+func videoRecommendListWithoutAds(ctx context.Context, filter bson.M, sort bson.D, categoryId, pageNum, pageSize, totalCount int64, cdn string) (*proto.VideoRecommendListResp, *srvdef.SrvErr) {
+ list, err := findVideoCategoryPage(ctx, filter, sort, categoryId, (pageNum-1)*pageSize, pageSize)
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.VideoRecommendListItem, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, videoToRecommendListItem(item, cdn))
+ }
+ return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }}, nil
+}
+
+// queryVideoMoreWithoutAds 无广告时直接按视频分页查询
+func queryVideoMoreWithoutAds(ctx context.Context, filter bson.M, sort bson.D, categoryId, pageNum, pageSize, totalCount int64, cdn string) (*proto.QueryVideoResp, *srvdef.SrvErr) {
+ skip := (pageNum - 1) * pageSize
+ list, err := findVideoCategoryPage(ctx, filter, sort, categoryId, skip, pageSize)
+ if err != nil {
+ return nil, err
+ }
+ resp := make([]*proto.QueryVideoListItem, 0, len(list))
+ for _, item := range list {
+ resp = append(resp, queryVideoToListItem(item, cdn))
+ }
+ return &proto.QueryVideoResp{BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: resp,
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}, nil
+}
+
+// buildQueryVideoMixedList 按混排后的全局位置切出当前页视频和广告数据
+func buildQueryVideoMixedList(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string, start, end, contentStart, adsTotal int64) []*proto.QueryVideoListItem {
+ resp := make([]*proto.QueryVideoListItem, 0, end-start)
+ contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
+ for i, item := range list {
+ contentByIndex[contentStart+int64(i)] = item
+ }
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, queryVideoAdInterval)
+ if adIndex >= 0 && adIndex < adsTotal {
+ resp = append(resp, queryVideoAdToListItem(ads[adIndex], cdn))
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, queryVideoAdInterval)
+ if item := contentByIndex[contentIndex]; item != nil {
+ resp = append(resp, queryVideoToListItem(item, cdn))
+ }
+ }
+ return resp
+}
+
+// buildVideoRecommendMixedList 按混排后的全局位置切出当前页视频和广告数据
+func buildVideoRecommendMixedList(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string, start, end, contentStart, adsTotal int64) []*proto.VideoRecommendListItem {
+ items := make([]*proto.VideoRecommendListItem, 0, end-start)
+ contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
+ for i, item := range list {
+ contentByIndex[contentStart+int64(i)] = item
+ }
+ for pos := start; pos < end; pos++ {
+ adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, videoRecommendAdInterval)
+ if adIndex >= 0 && adIndex < adsTotal {
+ items = append(items, videoAdToRecommendListItem(ads[adIndex], cdn))
+ continue
+ }
+ contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, videoRecommendAdInterval)
+ if item := contentByIndex[contentIndex]; item != nil {
+ items = append(items, videoToRecommendListItem(item, cdn))
+ }
+ }
+ return items
+}
+
+// queryVideoToListItem 将视频模型转换为视频更多列表项
+func queryVideoToListItem(item *videoinformationmod.VideoInformation, cdn string) *proto.QueryVideoListItem {
+ return &proto.QueryVideoListItem{
+ Type: proto.QueryVideoItemTypeVideo,
+ VideoInfo: homeVideoInfo(item, cdn),
+ }
+}
+
+// videoToRecommendListItem 将视频模型转换为 app 推荐视频列表项
+func videoToRecommendListItem(item *videoinformationmod.VideoInformation, cdn string) *proto.VideoRecommendListItem {
+ return &proto.VideoRecommendListItem{
+ Type: proto.VideoRecommendItemTypeVideo,
+ VideoInfo: homeVideoInfo(item, cdn),
+ }
+}
+
+// favoriteVideoProjection 返回收藏视频列表需要的最小字段,减少 MongoDB 网络传输和反序列化开销
+func favoriteVideoProjection() bson.M {
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Slug: 1,
+ videoinformationmod.FN.VideoInformation.AddedTime: 1,
+ videoinformationmod.FN.VideoInformation.CoverImg: 1,
+ videoinformationmod.FN.VideoInformation.Tags: 1,
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
+ videoinformationmod.FN.VideoInformation.Real.Likes: 1,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
+ }
+}
+
+// favoriteVideoListItem 将视频模型转换为当前用户收藏列表项
+func favoriteVideoListItem(item *videoinformationmod.VideoInformation, imgCdn string) *proto.FavoriteVideoListItem {
+ tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
+ return &proto.FavoriteVideoListItem{
+ Id: item.Id,
+ Title: item.Title,
+ Slug: item.Slug,
+ AddedTime: item.AddedTime,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Tags: item.Tags,
+ TagsName: tagNames,
+ TagSlugs: tagSlugs,
+ WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
+ LikeNum: item.Real.Likes + item.Fake.Likes,
+ }
+}
+
+type videoHistoryRow struct {
+ UpdatedAt time.Time `bson:"updatedAt"`
+ Video videoinformationmod.VideoInformation `bson:"video"`
+}
+
+// ReportVideoHistory 校验上架视频后写入当前用户观看历史
+func ReportVideoHistory(ctx context.Context, userID int64, req *proto.VideoHistoryReportReq) (*proto.VideoHistoryReportResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil || req.Id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
+ }
+ video, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.Id,
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if video.Video.Url == "" && video.Video.H265Url == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "视频不可播放", "")
+ }
+ now := time.Now().UTC()
+ if err := models.TouchWatchHistory(ctx, models.WatchHistory{
+ UserID: userID, DataType: models.WatchHistoryTypeVideo,
+ ContentID: req.Id, CreatedAt: now, UpdatedAt: now,
+ }); err != nil {
+ return nil, err
+ }
+ return &proto.VideoHistoryReportResp{WatchedAt: now}, nil
+}
+
+// QueryVideoHistoryList 按最近观看时间倒序分页返回当前用户的上架视频
+func QueryVideoHistoryList(ctx context.Context, userID int64, req *proto.VideoHistoryListReq) (*proto.VideoHistoryListResp, *srvdef.SrvErr) {
+ if userID <= 0 || req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "观看历史参数无效", "")
+ }
+ pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
+ base := []bson.M{
+ {"$match": bson.M{models.WatchHistoryUserID: userID, models.WatchHistoryDataType: models.WatchHistoryTypeVideo, models.WatchHistoryUpdatedAt: bson.M{"$gte": time.Now().UTC().AddDate(0, 0, -models.WatchHistoryRetentionDays)}}},
+ {"$lookup": bson.M{"from": videoinformationmod.TN.VideoInformation, "localField": models.WatchHistoryContentID, "foreignField": videoinformationmod.FN.VideoInformation.Id, "as": "video"}},
+ {"$unwind": "$video"},
+ {"$match": bson.M{"video." + videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}},
+ }
+ countRows := make([]struct {
+ Total int64 `bson:"total"`
+ }, 0)
+ if err := models.AggregateWatchHistory(ctx, &countRows, append(append([]bson.M(nil), base...), bson.M{"$count": "total"})); err != nil {
+ return nil, err
+ }
+ total := int64(0)
+ if len(countRows) > 0 {
+ total = countRows[0].Total
+ }
+ resp := &proto.VideoHistoryListResp{BasePageResp: response.BasePageResp{
+ TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.VideoHistoryItem, 0),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+ if total == 0 || pageNum > (total-1)/pageSize+1 {
+ return resp, nil
+ }
+ sortStage := bson.M{"$sort": bson.D{{Key: models.WatchHistoryUpdatedAt, Value: -1}, {Key: "_id", Value: -1}}}
+ pipeline := append([]bson.M{base[0], sortStage}, base[1:]...)
+ pipeline = append(pipeline, bson.M{"$skip": (pageNum - 1) * pageSize}, bson.M{"$limit": pageSize})
+ rows := make([]videoHistoryRow, 0)
+ if err := models.AggregateWatchHistory(ctx, &rows, pipeline); err != nil {
+ return nil, err
+ }
+ cdn := configser.GetRequestConfig(ctx).ImgCdn
+ items := make([]*proto.VideoHistoryItem, 0, len(rows))
+ for _, row := range rows {
+ items = append(items, &proto.VideoHistoryItem{
+ FavoriteVideoListItem: *favoriteVideoListItem(&row.Video, cdn),
+ Desc: row.Video.Desc, WatchedAt: row.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// ClearVideoHistory 清空当前用户的视频观看历史
+func ClearVideoHistory(ctx context.Context, userID int64) (*proto.VideoHistoryClearResp, *srvdef.SrvErr) {
+ if userID <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "用户无效", "")
+ }
+ count, err := models.DeleteWatchHistory(ctx, userID, models.WatchHistoryTypeVideo)
+ if err != nil {
+ return nil, err
+ }
+ return &proto.VideoHistoryClearResp{DeletedCount: count}, nil
+}
+
+// normalizeQueryVideoPage 规范化视频分页参数,限制单次请求数量避免大分页拖垮 MongoDB
+func normalizeQueryVideoPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = queryVideoDefaultPageSize
+ }
+ if pageSize > queryVideoMaxPageSize {
+ pageSize = queryVideoMaxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// emptyFavoriteVideoPage 构造当前用户收藏视频空分页响应
+func emptyFavoriteVideoPage(pageNum, pageSize int64) *proto.QueryFavoriteVideoListResp {
+ return &proto.QueryFavoriteVideoListResp{BasePageResp: response.BasePageResp{
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.FavoriteVideoListItem, 0),
+ }, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+}
+
+// queryVideoAdToListItem 将广告转换为视频更多列表项
+func queryVideoAdToListItem(ad proxy.AdvitiseInfo, cdn string) *proto.QueryVideoListItem {
+ return &proto.QueryVideoListItem{
+ Type: proto.QueryVideoItemTypeAdvertise,
+ AdvertiseId: ad.Id,
+ Link: ad.Link,
+ Location: ad.Location,
+ VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
+ Title: ad.Title,
+ CoverImg: netutil.MergeURL(cdn, ad.Images),
+ }},
+ }
+}
+
+// videoAdToRecommendListItem 将广告转换为 app 推荐视频列表项
+func videoAdToRecommendListItem(ad proxy.AdvitiseInfo, cdn string) *proto.VideoRecommendListItem {
+ return &proto.VideoRecommendListItem{
+ Type: proto.VideoRecommendItemTypeAdvertise,
+ AdvertiseId: ad.Id,
+ Link: ad.Link,
+ Location: ad.Location,
+ VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
+ Title: ad.Title,
+ CoverImg: netutil.MergeURL(cdn, ad.Images),
+ }},
+ }
+}
+
+// emptyQueryVideoMorePage 构造视频更多空分页响应
+func emptyQueryVideoMorePage(pageNum, pageSize int64, categoryInfo proto.VideoCategoryInfo) *proto.QueryVideoResp {
+ return &proto.QueryVideoResp{BasePageResp: response.BasePageResp{
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.QueryVideoListItem, 0),
+ }, CategoryInfo: categoryInfo, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
+}
+
+func videoCategoryInfo(categoryID int64) proto.VideoCategoryInfo {
+ return toVideoCategoryInfo(translationser.GetVideoCategory(categoryID))
+}
+
+func toVideoCategoryInfo(category *videocategorymod.VideoCategory) proto.VideoCategoryInfo {
+ if category == nil {
+ return proto.VideoCategoryInfo{}
+ }
+ return proto.VideoCategoryInfo{
+ Id: category.Id,
+ Name: category.Name,
+ Slug: category.Slug,
+ Title: category.Title,
+ Desc: category.Desc,
+ SeoInfo: category.SeoInfo,
+ }
+}
+
+// emptyVideoRecommendListPage 构造推荐视频空分页响应
+func emptyVideoRecommendListPage(pageNum, pageSize int64) *proto.VideoRecommendListResp {
+ return &proto.VideoRecommendListResp{BasePageResp: response.BasePageResp{
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.VideoRecommendListItem, 0),
+ }}
+}
+
+// videoRequestPath 从上下文中提取当前请求路径,避免API层重复组装视频首页响应字段
+func videoRequestPath(ctx context.Context) string {
+ ginCtx, ok := ctx.(*gin.Context)
+ if !ok || ginCtx.Request == nil || ginCtx.Request.URL == nil {
+ return ""
+ }
+ return ginCtx.Request.URL.Path
+}
diff --git a/app/service/videoser/video_list_test.go b/app/service/videoser/video_list_test.go
new file mode 100644
index 0000000..e394c25
--- /dev/null
+++ b/app/service/videoser/video_list_test.go
@@ -0,0 +1,127 @@
+package videoser
+
+import (
+ "context"
+ "net/url"
+ "strconv"
+ "testing"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/moreser"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+)
+
+func TestOfficialPlaybackTicket(t *testing.T) {
+ const ip = "127.0.0.1"
+ const path = "v3/official-test.m3u8"
+ signed := BuildH5M3U8OfficialURL(321, 123, path, ip, false)
+ u, err := url.Parse(signed)
+ if err != nil || u.Path != path {
+ t.Fatalf("官网票据地址无效 %q %v", signed, err)
+ }
+ expireAt, err := strconv.ParseInt(u.Query().Get("t"), 10, 64)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ticket, ok := CheckH5M3U8Ticket(321, expireAt, path, ip, u.Query().Get("ticket"))
+ if !ok || ticket.OfficialPlayletID != 123 {
+ t.Fatalf("官网短剧标识未保存在签名票据中 %+v", ticket)
+ }
+ if _, ok := CheckH5M3U8Ticket(321, expireAt, path, "127.0.0.2", u.Query().Get("ticket")); ok {
+ t.Fatal("官网票据未校验请求IP")
+ }
+ if ticket.UseNewKey {
+ t.Fatal("水印片不应使用原片密钥")
+ }
+ original := BuildH5M3U8OfficialURL(321, 123, "bktappupdata/sp/a.m3u8", ip, true)
+ ou, err := url.Parse(original)
+ if err != nil {
+ t.Fatal(err)
+ }
+ originalExpire, err := strconv.ParseInt(ou.Query().Get("t"), 10, 64)
+ if err != nil {
+ t.Fatal(err)
+ }
+ originalTicket, ok := CheckH5M3U8Ticket(321, originalExpire, "bktappupdata/sp/a.m3u8", ip, ou.Query().Get("ticket"))
+ if !ok || !originalTicket.UseNewKey {
+ t.Fatalf("原片票据未标记新密钥 %+v", originalTicket)
+ }
+}
+
+func TestVideoListFilterAndSort(t *testing.T) {
+ filter := videoListFilter(12)
+ if filter[videoinformationmod.FN.VideoInformation.Status] != pubdef.InformationStatusVerifyOK ||
+ filter[videoinformationmod.FN.VideoInformation.CategoryId] != int64(12) {
+ t.Fatalf("分类或上架状态筛选错误: %v", filter)
+ }
+ if _, ok := filter[videoinformationmod.FN.VideoInformation.UpdatedAt]; ok {
+ t.Fatalf("最近更新不应限制更新时间: %v", filter)
+ }
+ if got := videoListSort(1); got[0].Key != videoinformationmod.FN.VideoInformation.Real.WatchTimes {
+ t.Fatalf("热度排序错误: %v", got)
+ }
+ if got := videoListSort(2); got[0].Key != videoinformationmod.FN.VideoInformation.UpdatedAt || got[0].Value != -1 {
+ t.Fatalf("最近更新排序错误: %v", got)
+ }
+ if _, err := VideoList(context.Background(), &proto.VideoListReq{SortType: 3}); err == nil {
+ t.Fatal("未拒绝无效排序方式")
+ }
+}
+
+func TestVideoListMixedPagination(t *testing.T) {
+ const videoCount int64 = 17
+ ads := []proxy.AdvitiseInfo{{Id: "ad-1"}, {Id: "ad-2"}}
+ adCount := videoListInsertedAdsCount(videoCount, len(ads))
+ if adCount != 3 {
+ t.Fatalf("广告位数量 = %d, want 3", adCount)
+ }
+ all := make(videoinformationmod.VideoInformationList, 0, videoCount)
+ for id := int64(1); id <= videoCount; id++ {
+ all = append(all, &videoinformationmod.VideoInformation{Id: id})
+ }
+ var got []*proto.VideoRecommendListItem
+ for start := int64(0); start < videoCount+adCount; start += 7 {
+ end := start + 7
+ if end > videoCount+adCount {
+ end = videoCount + adCount
+ }
+ contentStart, contentLimit := moreser.ContentRange(start, end, videoCount, adCount, videoListAdInterval)
+ page := buildVideoListMixedPage(all[contentStart:contentStart+contentLimit], ads, "",
+ start, end, contentStart, adCount)
+ if int64(len(page)) != end-start {
+ t.Fatalf("分页 [%d,%d) 返回 %d 条", start, end, len(page))
+ }
+ got = append(got, page...)
+ }
+ var videoID int64 = 1
+ for pos, item := range got {
+ if pos%6 == 5 && pos < 18 {
+ if item.Type != proto.VideoRecommendItemTypeAdvertise || item.AdvertiseId != ads[(pos/6)%len(ads)].Id {
+ t.Fatalf("位置 %d 广告错误: %+v", pos, item)
+ }
+ continue
+ }
+ if item.Type != proto.VideoRecommendItemTypeVideo || item.Id != videoID {
+ t.Fatalf("位置 %d 视频错误: %+v, want ID %d", pos, item, videoID)
+ }
+ videoID++
+ }
+ if videoID != videoCount+1 {
+ t.Fatalf("视频末尾 ID = %d, want %d", videoID, videoCount+1)
+ }
+}
+
+func TestVideoListAdSlots(t *testing.T) {
+ for _, tc := range []struct {
+ videos int64
+ ads int
+ want int64
+ }{
+ {0, 2, 0}, {5, 2, 0}, {6, 2, 1}, {10, 2, 1}, {11, 2, 2}, {16, 1, 3}, {16, 0, 0},
+ } {
+ if got := videoListInsertedAdsCount(tc.videos, tc.ads); got != tc.want {
+ t.Errorf("视频 %d 条、广告 %d 条得到 %d 个广告位,want %d", tc.videos, tc.ads, got, tc.want)
+ }
+ }
+}
diff --git a/app/service/videoser/vtt.go b/app/service/videoser/vtt.go
new file mode 100644
index 0000000..609b7b2
--- /dev/null
+++ b/app/service/videoser/vtt.go
@@ -0,0 +1,40 @@
+package videoser
+
+import (
+ "bytes"
+ "io"
+ "videoweb/app/appg"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/m3u8"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetVTT 获取雪碧图 vtt,缩略图路径替换为图片 cdn,兼容 /laosiji 路径资源。
+func GetVTT(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ vttName := c.Param("key")
+
+ var req m3u8.Req
+ if err = c.BindQuery(&req); err != nil {
+ return
+ }
+ req.Name = vttName
+
+ imgCdn := configser.GetAppImageCdn(ipaddr.ResolveClientIP(c))
+ originDomain := appg.Conf.OrigDomain
+ lsjImageYuan := appg.Conf.LsjMediaConf.ImageYuan
+
+ buff, err := m3u8.TransformVtt(req.Name, imgCdn, originDomain, lsjImageYuan)
+ if err != nil {
+ return
+ }
+ contentType = "text/vtt; charset=utf-8"
+ reader = bytes.NewReader(buff)
+ contentLength = int64(len(buff))
+ extraHeaders = map[string]string{}
+ return
+ }
+ handlerFile(ctx, fn)
+}
diff --git a/app/service/wordsfilterser/advertise.go b/app/service/wordsfilterser/advertise.go
new file mode 100644
index 0000000..b4fa2f8
--- /dev/null
+++ b/app/service/wordsfilterser/advertise.go
@@ -0,0 +1,104 @@
+package wordsfilterser
+
+import (
+ "regexp"
+ "strings"
+
+ "golang.org/x/text/unicode/norm"
+)
+
+var (
+ // 只识别带点号和明确后缀的域名,避免把普通英文、拼音和中文昵称当成网址。
+ advertiseDomainPattern = regexp.MustCompile(`(?i)(?:https?://|www[.,])?[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9][a-z0-9-]{0,62})*[.,](?:ai|app|biz|cc|club|cn|co|com|fun|info|io|link|live|me|net|online|org|pro|shop|site|top|tv|vip|win|work|xyz)(?:[^a-z0-9]|$)`)
+ // “加”与联系方式必须成组出现,不再通过中文拼音推断,避免误伤正常姓名。
+ advertiseAddContactPattern = regexp.MustCompile(`(?i)(?:加|\+)[\s._,:,。-]{0,3}(?:v(?:x)?|w(?:x)?|q{1,2}|微(?:信)?|扣{1,2}|群|好友|电报|飞机)`)
+ // 联系方式名称与账号同时出现时才拦截,单独的英文或数字允许正常发布。
+ advertiseContactThenAccountPattern = regexp.MustCompile(`(?i)(?:微信|微号|vx|wx|qq|扣扣|电报|telegram|tg|飞机|联系方式|联系我|联系)[\s::=._-]{0,6}[a-z0-9][a-z0-9._-]{3,}`)
+ advertiseAccountThenContactPattern = regexp.MustCompile(`(?i)[a-z0-9][a-z0-9._-]{3,}[\s()()::=._-]{0,8}(?:微信|微号|vx|wx|qq|扣扣|电报|telegram|tg|飞机)`)
+ // 明确动作词与长账号同时出现时拦截,单独的数字仍交给AI结合内容上下文判断。
+ advertiseAddAccountPattern = regexp.MustCompile(`(?i)(?:加我|加(?:v|q{1,2}|微(?:信)?)|\+)[\s::=._-]{0,6}(?:[a-z][a-z0-9._-]{3,}|[1-9][0-9]{6,10})`)
+ advertiseAccountThenAddPattern = regexp.MustCompile(`(?i)[1-9][0-9]{6,10}[^0-9]{0,16}(?:加我(?:v|q{1,2}|微信)|加(?:v|q{1,2}|微信))`)
+ advertiseActionAccountPattern = regexp.MustCompile(`(?i)(?:换|留(?:下)?(?:联系方式)?|私(?:我|聊)?|找我)[\s::=+._-]{0,6}[1-9][0-9]{8,10}`)
+ // 识别用空白或标点拆开的微信号,例如“wxc z n q 3018”。
+ advertiseBrokenAccountPattern = regexp.MustCompile(`(?i)(?:wx|vx|wxc)(?:[\s._-]*[a-z0-9]){4,}`)
+ // 这些短语本身已经表达站外引流意图,不依赖账号长度判断。
+ advertisePromotionPattern = regexp.MustCompile(`(?i)(?:搜(?:索)?(?:网页|网址|网站)|浏览器搜索|免费(?:空降|约炮|一夜情)|同城(?:约炮|空降|上门)|(?:约炮|约妹|骚妹).{0,8}(?:上门|空降)|电鸽|纸飞机|(?:来|去|上)?(?:薇博|微博)[\s::=._-]{0,3}(?:搜|找|关注))`)
+
+ advertiseTextReplacer = strings.NewReplacer(
+ "丶", ".", "、", ".", "。", ".", ".", ".", "。", ".", "﹒", ".", "·", ".", "⢀", ".",
+ ",", ",", "➕", "+", "+", "+",
+ "匚", "c", "匸", "c", "丅", "t", "卩", "p", "卪", "p", "讠", "i",
+ )
+)
+
+// validateAdvertiseText 只拦截高置信联系方式、网址和站外引流文本。
+func validateAdvertiseText(text string) bool {
+ normalized := normalizeAdvertiseText(text)
+ // 纯数字6是用户常见的互动表达,无论长度都不作为广告处理。
+ if normalized != "" && strings.Trim(normalized, "6") == "" {
+ return true
+ }
+ return !advertiseDomainPattern.MatchString(normalized) &&
+ !advertiseAddContactPattern.MatchString(normalized) &&
+ !advertiseContactThenAccountPattern.MatchString(normalized) &&
+ !advertiseAccountThenContactPattern.MatchString(normalized) &&
+ !advertiseAddAccountPattern.MatchString(normalized) &&
+ !advertiseAccountThenAddPattern.MatchString(normalized) &&
+ !advertiseActionAccountPattern.MatchString(normalized) &&
+ !advertiseBrokenAccountPattern.MatchString(normalized) &&
+ !advertisePromotionPattern.MatchString(normalized)
+}
+
+// normalizeAdvertiseText 将全角和数学字体还原为普通字符,再处理常见分隔符变形。
+func normalizeAdvertiseText(text string) string {
+ normalized := strings.ToLower(norm.NFKC.String(strings.TrimSpace(text)))
+ normalized = strings.Map(normalizeAdvertiseRune, normalized)
+ return advertiseTextReplacer.Replace(normalized)
+}
+
+// normalizeAdvertiseRune 还原广告中常见的圈字母、圈数字和跨字符集同形字。
+func normalizeAdvertiseRune(r rune) rune {
+ switch {
+ case r >= '\U0001F150' && r <= '\U0001F169':
+ return 'a' + r - '\U0001F150'
+ case r >= '\u2776' && r <= '\u277e':
+ return '1' + r - '\u2776'
+ case r >= '\u2780' && r <= '\u2788':
+ return '1' + r - '\u2780'
+ case r >= '\u278a' && r <= '\u2792':
+ return '1' + r - '\u278a'
+ }
+
+ switch r {
+ case 'а', 'α':
+ return 'a'
+ case 'в', 'β':
+ return 'b'
+ case 'с':
+ return 'c'
+ case 'е', 'ε':
+ return 'e'
+ case 'і', 'ι':
+ return 'i'
+ case 'ј':
+ return 'j'
+ case 'к', 'κ':
+ return 'k'
+ case 'м', 'μ':
+ return 'm'
+ case 'о', 'ο':
+ return 'o'
+ case 'р', 'ρ':
+ return 'p'
+ case 'ѕ':
+ return 's'
+ case 'т', 'τ':
+ return 't'
+ case 'у', 'υ':
+ return 'y'
+ case 'х', 'χ':
+ return 'x'
+ default:
+ return r
+ }
+}
diff --git a/app/service/wordsfilterser/wordsfilter.go b/app/service/wordsfilterser/wordsfilter.go
new file mode 100644
index 0000000..3c9f885
--- /dev/null
+++ b/app/service/wordsfilterser/wordsfilter.go
@@ -0,0 +1,11 @@
+package wordsfilterser
+
+// IsAdvertise 判断文本是否包含广告链接、联系方式或站外引流。
+func IsAdvertise(text string) bool {
+ return !validateAdvertiseText(text)
+}
+
+// Validate 校验文本是否可发布。当前仅拦截广告内容。
+func Validate(text string) bool {
+ return !IsAdvertise(text)
+}
diff --git a/app/skd/ipstat/sync.go b/app/skd/ipstat/sync.go
new file mode 100644
index 0000000..431d237
--- /dev/null
+++ b/app/skd/ipstat/sync.go
@@ -0,0 +1,58 @@
+package ipstat
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/ipdailystatmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+)
+
+const syncInterval = 5 * time.Minute
+
+// StartSync 启动后台协程,每隔 syncInterval 将当日和昨日的 Redis IP SET
+// 数量同步写入 MongoDB(ip_daily_stat),供 web 端查询。
+func StartSync() {
+ go func() {
+ // 启动时立即同步一次
+ syncOnce()
+ ticker := time.NewTicker(syncInterval)
+ defer ticker.Stop()
+ for range ticker.C {
+ syncOnce()
+ }
+ }()
+}
+
+func syncOnce() {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+
+ now := time.Now().In(utils.LocBJ)
+ appId := int64(pubdef.AppId)
+
+ for _, offset := range []int{0, -1} {
+ date := now.AddDate(0, 0, offset).Format("20060102")
+ key := redisconst.IpStatKey(appId, date)
+
+ count, err := redis.SCard(key)
+ if err != nil {
+ log.Warn("ipstat sync: SCard failed",
+ log.Any("date", date), log.E(err))
+ continue
+ }
+
+ // 今日 Redis 为 0 时不覆盖 MongoDB,避免 key 丢失把已有数据写成 0。
+ if offset == 0 && count == 0 {
+ continue
+ }
+
+ if err := ipdailystatmod.UpsertCount(ctx, appId, date, count); err != nil {
+ log.Warn("ipstat sync: UpsertCount failed",
+ log.Any("appId", appId), log.Any("date", date), log.E(err))
+ }
+ }
+}
diff --git a/app/skd/job/dhsitemap/dhsitemap.go b/app/skd/job/dhsitemap/dhsitemap.go
new file mode 100644
index 0000000..4e35659
--- /dev/null
+++ b/app/skd/job/dhsitemap/dhsitemap.go
@@ -0,0 +1,134 @@
+package dhsitemap
+
+import (
+ "context"
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+)
+
+const (
+ outputDir = "../h5/templates/page/sitemaps"
+ filename = "dhsitemap.xml"
+
+ // 与主站 sitemap 一致:浏览器用 XSLT 把 loc 渲成可点击链接
+ stylesheetURLSet = `` + "\n"
+)
+
+var generateMu sync.Mutex
+
+type urlSet struct {
+ XMLName xml.Name `xml:"urlset"`
+ Xmlns string `xml:"xmlns,attr"`
+ URLs []urlEntry `xml:"url"`
+}
+
+type urlEntry struct {
+ Loc string `xml:"loc"`
+ LastMod string `xml:"lastmod"`
+}
+
+// GenerateDHSitemap 生成导航站 sitemap,域名取自系统配置 newUrlAddr。
+func GenerateDHSitemap() {
+ generateMu.Lock()
+ defer generateMu.Unlock()
+
+ ctx := context.Background()
+ config, serr := configser.GetSystemConfig(ctx)
+ if serr != nil {
+ log.Error("GenerateDHSitemap GetSystemConfig", log.Any("err", serr))
+ return
+ }
+ if config == nil {
+ log.Error("GenerateDHSitemap GetSystemConfig config is nil")
+ return
+ }
+
+ urls := collectDHURLs(config.NewUrlAddr, config.NewUrlAddr1, config.NewUrlAddr2)
+ if len(urls) == 0 {
+ log.Warn("GenerateDHSitemap: no newUrlAddr configured, skip")
+ return
+ }
+
+ today := time.Now().Format("2006-01-02")
+ entries := make([]urlEntry, 0, len(urls))
+ for _, u := range urls {
+ entries = append(entries, urlEntry{
+ Loc: u,
+ LastMod: today,
+ })
+ }
+
+ doc := urlSet{
+ Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
+ URLs: entries,
+ }
+
+ if err := writeFile(doc); err != nil {
+ log.Error("GenerateDHSitemap write", log.Any("err", err))
+ return
+ }
+
+ src := filepath.Join(outputDir, filename+".tmp")
+ dst := filepath.Join(outputDir, filename)
+ if err := os.Rename(src, dst); err != nil {
+ log.Error("GenerateDHSitemap rename", log.Any("err", err))
+ os.Remove(src)
+ return
+ }
+
+ log.Info("GenerateDHSitemap done", log.Any("urls", len(entries)))
+}
+
+func collectDHURLs(addrs ...string) []string {
+ seen := make(map[string]struct{}, len(addrs))
+ urls := make([]string, 0, len(addrs))
+ for _, addr := range addrs {
+ addr = strings.TrimSpace(addr)
+ if addr == "" {
+ continue
+ }
+ loc := utils.JoinUrlPath(addr, "/")
+ if _, ok := seen[loc]; ok {
+ continue
+ }
+ seen[loc] = struct{}{}
+ urls = append(urls, loc)
+ }
+ return urls
+}
+
+func writeFile(doc urlSet) error {
+ if err := os.MkdirAll(outputDir, 0755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", outputDir, err)
+ }
+ path := filepath.Join(outputDir, filename+".tmp")
+ file, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("create %s: %w", filename, err)
+ }
+ defer file.Close()
+
+ if _, err := file.WriteString(xml.Header); err != nil {
+ return fmt.Errorf("write header: %w", err)
+ }
+ if _, err := file.WriteString(stylesheetURLSet); err != nil {
+ return fmt.Errorf("write stylesheet: %w", err)
+ }
+ encoder := xml.NewEncoder(file)
+ encoder.Indent("", " ")
+ if err := encoder.Encode(doc); err != nil {
+ return fmt.Errorf("encode: %w", err)
+ }
+ if err := encoder.Flush(); err != nil {
+ return fmt.Errorf("flush: %w", err)
+ }
+ return file.Sync()
+}
diff --git a/app/skd/job/llmstxt/llmstxt.go b/app/skd/job/llmstxt/llmstxt.go
new file mode 100644
index 0000000..c46b6c0
--- /dev/null
+++ b/app/skd/job/llmstxt/llmstxt.go
@@ -0,0 +1,362 @@
+package llmstxt
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ maxTagsInBrief = 80 // llms.txt 中标签条数上限
+ maxRecentInFull = 500 // llms-full.txt 中最新视频条数上限
+)
+
+var generateMu sync.Mutex
+
+// GenerateLLMSTxt 生成 llms.txt(摘要)和 llms-full.txt(详细),输出到 outDir。
+func GenerateLLMSTxt(outDir string) {
+ generateMu.Lock()
+ defer generateMu.Unlock()
+
+ ctx := context.Background()
+ config, serr := configser.GetSystemConfig(ctx)
+ if serr != nil || config == nil {
+ log.Error("GenerateLLMSTxt GetSystemConfig", log.Any("err", serr))
+ return
+ }
+
+ baseURL := strings.TrimSuffix(config.WebAddr, "/")
+
+ // ── 查询数据 ──────────────────────────────────────────────
+ categoryList, _ := videocategorymod.FindManyVideoCategorys(ctx,
+ bson.M{videocategorymod.FN.VideoCategory.Status: true},
+ func() *options.FindOptions {
+ o := options.Find()
+ o.SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1})
+ return o
+ }())
+
+ tagOpt := options.Find()
+ tagOpt.SetSort(bson.M{videotagmod.FN.VideoTagInfo.WatchTimes: -1})
+ tagOpt.SetLimit(int64(maxTagsInBrief))
+ tagList, _ := videotagmod.FindManyVideoTagInfos(ctx,
+ bson.M{videotagmod.FN.VideoTagInfo.Status: true}, tagOpt)
+
+ actressOpt := options.Find()
+ actressOpt.SetSort(bson.M{actress.FN.Actress.Works: -1})
+ actressList, _ := actress.FindManyActresss(ctx,
+ bson.M{actress.FN.Actress.Status: true}, actressOpt)
+
+ recentOpt := options.Find()
+ recentOpt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1})
+ recentOpt.SetLimit(50)
+ recentVideos, _ := videoinformationmod.FindManyVideoInformations(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK},
+ recentOpt)
+
+ // ── 生成 llms.txt ─────────────────────────────────────────
+ var brief strings.Builder
+ writeBrief(&brief, config.WebTitle, config.WebDescription, baseURL, config.WebKeyworld,
+ categoryList, tagList, actressList, recentVideos)
+
+ if err := writeTextFile(outDir, "llms.txt", brief.String()); err != nil {
+ log.Error("GenerateLLMSTxt write llms.txt", log.Any("err", err))
+ return
+ }
+ if err := commitFile(outDir, "llms.txt"); err != nil {
+ log.Error("GenerateLLMSTxt commit llms.txt", log.Any("err", err))
+ return
+ }
+
+ // ── 生成 llms-full.txt ────────────────────────────────────
+ allVideoOpt := options.Find()
+ allVideoOpt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1})
+ allVideoOpt.SetLimit(int64(maxRecentInFull))
+ allVideos, _ := videoinformationmod.FindManyVideoInformations(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK},
+ allVideoOpt)
+
+ var full strings.Builder
+ writeFull(&full, config.WebTitle, config.WebDescription, baseURL, config.WebKeyworld,
+ categoryList, tagList, actressList, allVideos)
+
+ if err := writeTextFile(outDir, "llms-full.txt", full.String()); err != nil {
+ log.Error("GenerateLLMSTxt write llms-full.txt", log.Any("err", err))
+ return
+ }
+ if err := commitFile(outDir, "llms-full.txt"); err != nil {
+ log.Error("GenerateLLMSTxt commit llms-full.txt", log.Any("err", err))
+ return
+ }
+
+ log.Info("GenerateLLMSTxt done",
+ log.Any("categories", len(categoryList)),
+ log.Any("tags", len(tagList)),
+ log.Any("actresses", len(actressList)),
+ log.Any("videos", len(allVideos)),
+ )
+}
+
+// ── 内容构建 ──────────────────────────────────────────────────
+
+func writeBrief(
+ b *strings.Builder,
+ title, desc, baseURL string,
+ keywords []string,
+ categories videocategorymod.VideoCategoryList,
+ tags videotagmod.VideoTagInfoList,
+ actresses actress.ActressList,
+ recentVideos videoinformationmod.VideoInformationList,
+) {
+ now := time.Now().Format("2006-01-02")
+
+ // 文件头
+ fmt.Fprintf(b, "# %s\n\n", title)
+ if desc != "" {
+ fmt.Fprintf(b, "> %s\n\n", desc)
+ }
+ if len(keywords) > 0 {
+ fmt.Fprintf(b, "> 关键词:%s\n\n", strings.Join(keywords, "、"))
+ }
+ fmt.Fprintf(b, "> 更新时间:%s\n\n", now)
+
+ // 重要页面
+ writeImportantPages(b, baseURL)
+
+ // 分类
+ if len(categories) > 0 {
+ fmt.Fprintf(b, "## 内容分类\n\n")
+ for _, c := range categories {
+ if c == nil || c.Id == 0 {
+ continue
+ }
+ name := c.Name
+ if c.Title != "" {
+ name = c.Title
+ }
+ catDesc := c.Desc
+ if catDesc == "" {
+ catDesc = name + "相关视频内容"
+ }
+ fmt.Fprintf(b, "- [%s](%s/video/more/%d): %s\n", name,
+ baseURL, c.Id, catDesc)
+ }
+ b.WriteString("\n")
+ }
+
+ // 热门标签(取播放量最高的)
+ if len(tags) > 0 {
+ fmt.Fprintf(b, "## 热门标签\n\n")
+ for _, t := range tags {
+ if t == nil || t.Id == 0 {
+ continue
+ }
+ fmt.Fprintf(b, "- [%s](%s/video/all?pageNum=1&pageSize=40&tagsIds=%d)\n", t.Title, baseURL, t.Id)
+ }
+ b.WriteString("\n")
+ }
+
+ // 作者
+ if len(actresses) > 0 {
+ fmt.Fprintf(b, "## 作者\n\n")
+ for _, item := range actresses {
+ if item == nil || item.Id == 0 {
+ continue
+ }
+ fmt.Fprintf(b, "- [%s](%s/actress/detail?id=%d)\n", item.Alias, baseURL, item.Id)
+ }
+ b.WriteString("\n")
+ }
+
+ // 最新内容(50条)
+ if len(recentVideos) > 0 {
+ fmt.Fprintf(b, "## 最新内容\n\n")
+ for _, v := range recentVideos {
+ if v == nil || v.Id == 0 {
+ continue
+ }
+ url := utils.JoinUrlPath(baseURL, fmt.Sprintf("/video/detail?id=%d", v.Id))
+ desc := v.Desc
+ if desc == "" {
+ desc = v.Title
+ }
+ desc = truncate(desc, 100)
+ fmt.Fprintf(b, "- [%s](%s): %s\n", v.Title, url, desc)
+ }
+ b.WriteString("\n")
+ }
+}
+
+func writeFull(
+ b *strings.Builder,
+ title, desc, baseURL string,
+ keywords []string,
+ categories videocategorymod.VideoCategoryList,
+ tags videotagmod.VideoTagInfoList,
+ actresses actress.ActressList,
+ videos videoinformationmod.VideoInformationList,
+) {
+ // llms-full.txt 先包含 brief 的所有内容(用全量视频列表替换最新50条)
+ // 再附加每条视频的完整详情
+
+ now := time.Now().Format("2006-01-02")
+
+ fmt.Fprintf(b, "# %s\n\n", title)
+ if desc != "" {
+ fmt.Fprintf(b, "> %s\n\n", desc)
+ }
+ if len(keywords) > 0 {
+ fmt.Fprintf(b, "> 关键词:%s\n\n", strings.Join(keywords, "、"))
+ }
+ fmt.Fprintf(b, "> 更新时间:%s\n\n", now)
+
+ // 重要页面
+ writeImportantPages(b, baseURL)
+
+ // 分类(含 SEO 描述)
+ if len(categories) > 0 {
+ fmt.Fprintf(b, "## 内容分类\n\n")
+ for _, c := range categories {
+ if c == nil || c.Id == 0 {
+ continue
+ }
+ name := c.Name
+ if c.Title != "" {
+ name = c.Title
+ }
+ catDesc := c.SeoInfo.Description
+ if catDesc == "" {
+ catDesc = c.Desc
+ }
+ if catDesc == "" {
+ catDesc = name + "相关视频内容"
+ }
+ fmt.Fprintf(b, "- [%s](%s/video/more/%d): %s\n", name, baseURL, c.Id, catDesc)
+ }
+ b.WriteString("\n")
+ }
+
+ // 全部标签
+ if len(tags) > 0 {
+ fmt.Fprintf(b, "## 全部标签\n\n")
+ for _, t := range tags {
+ if t == nil || t.Id == 0 {
+ continue
+ }
+ tagDesc := t.SeoInfo.Description
+ if tagDesc == "" {
+ tagDesc = t.Title
+ }
+ fmt.Fprintf(b, "- [%s](%s/video/all?pageNum=1&pageSize=40&tagsIds=%d): %s\n", t.Title, baseURL, t.Id, tagDesc)
+ }
+ b.WriteString("\n")
+ }
+
+ // 作者
+ if len(actresses) > 0 {
+ fmt.Fprintf(b, "## 作者\n\n")
+ for _, item := range actresses {
+ if item == nil || item.Id == 0 {
+ continue
+ }
+ itemDesc := item.SeoInfo.Description
+ if itemDesc == "" {
+ itemDesc = item.Desc
+ }
+ fmt.Fprintf(b, "- [%s](%s/actress/detail?id=%d): %s\n",
+ item.Alias, baseURL, item.Id, itemDesc)
+ }
+ b.WriteString("\n")
+ }
+
+ // 视频详情列表
+ if len(videos) > 0 {
+ fmt.Fprintf(b, "## 内容详情(最新 %d 条)\n\n", len(videos))
+ for _, v := range videos {
+ if v == nil || v.Id == 0 {
+ continue
+ }
+ url := utils.JoinUrlPath(baseURL, fmt.Sprintf("/video/detail?id=%d", v.Id))
+ fmt.Fprintf(b, "### [%s](%s)\n\n", v.Title, url)
+
+ if v.Desc != "" {
+ fmt.Fprintf(b, "%s\n\n", truncate(v.Desc, 500))
+ }
+
+ if !v.AddedTime.IsZero() {
+ fmt.Fprintf(b, "- 发布时间:%s\n", v.AddedTime.Format("2006-01-02"))
+ }
+ if v.CreatorName != "" {
+ fmt.Fprintf(b, "- 创作者:%s\n", v.CreatorName)
+ }
+ if len(v.Keyworld) > 0 {
+ fmt.Fprintf(b, "- 关键词:%s\n", strings.Join(v.Keyworld, "、"))
+ }
+ b.WriteString("\n")
+ }
+ }
+}
+
+func writeImportantPages(b *strings.Builder, baseURL string) {
+ fmt.Fprintf(b, "## 重要页面\n\n")
+ fmt.Fprintf(b, "- [首页](%s/): 网站主页\n", baseURL)
+ fmt.Fprintf(b, "- [热门搜索](%s/search/hot): 热门搜索关键词\n", baseURL)
+ fmt.Fprintf(b, "- [视频首页](%s/video): 高清视频内容入口\n", baseURL)
+ fmt.Fprintf(b, "- [全部视频](%s/video/all): 浏览全部视频\n", baseURL)
+ fmt.Fprintf(b, "- [视频排行榜](%s/video/rank): 热门视频排行\n", baseURL)
+ fmt.Fprintf(b, "- [线路导航](%s/nav): 最新可用线路入口\n", baseURL)
+ fmt.Fprintf(b, "- [应用导航](%s/application): 应用推荐与下载入口\n", baseURL)
+ fmt.Fprintf(b, "- [RSS订阅](%s/rss.xml): RSS Feed\n", baseURL)
+ fmt.Fprintf(b, "- [Sitemap](%s/sitemap.xml): 网站地图\n\n", baseURL)
+}
+
+// ── 工具函数 ──────────────────────────────────────────────────
+
+func truncate(s string, maxLen int) string {
+ runes := []rune(s)
+ if len(runes) <= maxLen {
+ return s
+ }
+ return string(runes[:maxLen]) + "…"
+}
+
+func writeTextFile(outDir, filename, content string) error {
+ if err := os.MkdirAll(outDir, 0755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", outDir, err)
+ }
+ tmpPath := filepath.Join(outDir, filename+".tmp")
+ f, err := os.Create(tmpPath)
+ if err != nil {
+ return fmt.Errorf("create %s: %w", tmpPath, err)
+ }
+ defer f.Close()
+ if _, err := f.WriteString(content); err != nil {
+ return fmt.Errorf("write %s: %w", filename, err)
+ }
+ return f.Sync()
+}
+
+func commitFile(outDir, filename string) error {
+ src := filepath.Join(outDir, filename+".tmp")
+ dst := filepath.Join(outDir, filename)
+ if err := os.Rename(src, dst); err != nil {
+ os.Remove(src)
+ return fmt.Errorf("rename %s: %w", filename, err)
+ }
+ return nil
+}
diff --git a/app/skd/job/llmstxt/llmstxt_test.go b/app/skd/job/llmstxt/llmstxt_test.go
new file mode 100644
index 0000000..129fb47
--- /dev/null
+++ b/app/skd/job/llmstxt/llmstxt_test.go
@@ -0,0 +1,31 @@
+package llmstxt
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestWriteImportantPagesUsesWebRoutes(t *testing.T) {
+ t.Parallel()
+
+ var b strings.Builder
+ writeImportantPages(&b, "https://91jxporn.com")
+ out := b.String()
+
+ for _, forbidden := range []string{"/tag/all", "/comicsList", "/search/hot/"} {
+ if strings.Contains(out, forbidden) {
+ t.Fatalf("important pages should not contain legacy path %q:\n%s", forbidden, out)
+ }
+ }
+
+ for _, required := range []string{
+ "/search/hot)",
+ "/video)",
+ "/nav)",
+ "/application)",
+ } {
+ if !strings.Contains(out, required) {
+ t.Fatalf("important pages missing %q:\n%s", required, out)
+ }
+ }
+}
diff --git a/app/skd/job/rss/rss.go b/app/skd/job/rss/rss.go
new file mode 100644
index 0000000..4b5223b
--- /dev/null
+++ b/app/skd/job/rss/rss.go
@@ -0,0 +1,217 @@
+package rss
+
+import (
+ "context"
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ maxRSSItems = 200 // RSS 条目上限
+)
+
+var generateMu sync.Mutex
+
+// RSS 2.0 结构定义
+type rssFeed struct {
+ XMLName xml.Name `xml:"rss"`
+ Version string `xml:"version,attr"`
+ Atom string `xml:"xmlns:atom,attr"`
+ Channel rssChannel `xml:"channel"`
+}
+
+type rssChannel struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ Description string `xml:"description"`
+ Language string `xml:"language"`
+ LastBuildDate string `xml:"lastBuildDate"`
+ AtomLink atomLink `xml:"atom:link"`
+ Items []rssItem `xml:"item"`
+}
+
+type atomLink struct {
+ Href string `xml:"href,attr"`
+ Rel string `xml:"rel,attr"`
+ Type string `xml:"type,attr"`
+}
+
+type rssItem struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ Description string `xml:"description"`
+ PubDate string `xml:"pubDate"`
+ GUID string `xml:"guid"`
+ Enclosure *rssEnclosure `xml:"enclosure,omitempty"`
+}
+
+type rssEnclosure struct {
+ URL string `xml:"url,attr"`
+ Type string `xml:"type,attr"`
+ Length string `xml:"length,attr"`
+}
+
+// GenerateRSS 生成 rss.xml,输出到 outDir/rss.xml(Web 侧通过 /rss.xml 静态暴露)
+func GenerateRSS(ctx context.Context, outDir string) {
+ generateMu.Lock()
+ defer generateMu.Unlock()
+
+ scfg, srvErr := configser.GetSystemConfig(ctx)
+ if srvErr != nil || scfg == nil {
+ log.Error("GenerateRSS GetSystemConfig", log.Any("err", srvErr))
+ return
+ }
+
+ // 查询最新的已上架视频
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ opt := options.Find().
+ SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1}).
+ SetLimit(int64(maxRSSItems))
+ videoList, vsErr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
+ if vsErr != nil {
+ log.Error("GenerateRSS FindManyVideoInformations", log.Any("err", vsErr))
+ return
+ }
+
+ items := buildRSSItems(scfg, videoList)
+ feedURL := utils.JoinUrlPath(scfg.WebAddr, "/rss.xml")
+ feed := rssFeed{
+ Version: "2.0",
+ Atom: "http://www.w3.org/2005/Atom",
+ Channel: rssChannel{
+ Title: scfg.WebTitle,
+ Link: scfg.WebAddr,
+ Description: scfg.WebDescription,
+ Language: "zh-cn",
+ LastBuildDate: time.Now().Format(time.RFC1123Z),
+ AtomLink: atomLink{
+ Href: feedURL,
+ Rel: "self",
+ Type: "application/rss+xml",
+ },
+ Items: items,
+ },
+ }
+
+ const filename = "rss.xml"
+ if err := writeRSSFile(outDir, filename, feed); err != nil {
+ log.Error("GenerateRSS write", log.Any("err", err))
+ return
+ }
+
+ // 写入成功,rename .tmp → 正式文件
+ src := filepath.Join(outDir, filename+".tmp")
+ dst := filepath.Join(outDir, filename)
+ if err := os.Rename(src, dst); err != nil {
+ log.Error("GenerateRSS rename", log.Any("err", err))
+ os.Remove(src)
+ return
+ }
+
+ log.Info("GenerateRSS done", log.Any("items", len(items)))
+}
+
+// absModIndex 根据 id 确定性选取切片索引,保证结果非负
+func absModIndex(id int64, length int) int {
+ idx := id % int64(length)
+ if idx < 0 {
+ idx = -idx
+ }
+ return int(idx)
+}
+
+func buildRSSItems(scfg *cfg.SysCfg, videoList videoinformationmod.VideoInformationList) []rssItem {
+ logoURL := utils.JoinUrlPath(scfg.WebAddr, "/static/images/logo.png")
+
+ items := make([]rssItem, 0, len(videoList))
+ for _, v := range videoList {
+ desc := v.Desc
+ if desc == "" {
+ desc = v.Title
+ }
+
+ pageURL := utils.JoinUrlPathKeepSlash(scfg.WebAddr, fmt.Sprintf("/video/detail?id=%d", v.Id))
+
+ pubDate := ""
+ if !v.AddedTime.IsZero() {
+ pubDate = v.AddedTime.Format(time.RFC1123Z)
+ }
+
+ item := rssItem{
+ Title: v.Title,
+ Link: pageURL,
+ Description: desc,
+ PubDate: pubDate,
+ GUID: pageURL,
+ }
+
+ // 封面图优先级:UnencryptedImage > SitemapDefaultImages 确定性选取 > logo
+ encURL := ""
+ encType := "image/jpeg"
+ if v.UnencryptedImage != "" && scfg.UnencryptedCdn != "" {
+ encURL = netutil.MergeURL(scfg.UnencryptedCdn, v.UnencryptedImage)
+ } else if len(scfg.SitemapDefaultImages) > 0 && scfg.UnencryptedCdn != "" {
+ img := scfg.SitemapDefaultImages[absModIndex(v.Id, len(scfg.SitemapDefaultImages))]
+ encURL = netutil.MergeURL(scfg.UnencryptedCdn, img)
+ } else {
+ encURL = logoURL
+ encType = "image/png"
+ }
+ if strings.HasSuffix(encURL, ".png") {
+ encType = "image/png"
+ } else if strings.HasSuffix(encURL, ".webp") {
+ encType = "image/webp"
+ }
+ item.Enclosure = &rssEnclosure{
+ URL: encURL,
+ Type: encType,
+ Length: "0",
+ }
+
+ items = append(items, item)
+ }
+ return items
+}
+
+// writeRSSFile 将数据写入 rssDir/filename.tmp,由调用方负责 rename
+func writeRSSFile(rssDir, filename string, data interface{}) error {
+ if err := os.MkdirAll(rssDir, 0755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", rssDir, err)
+ }
+ path := filepath.Join(rssDir, filename+".tmp")
+ file, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("create %s: %w", filename, err)
+ }
+ defer file.Close()
+
+ if _, err := file.WriteString(xml.Header); err != nil {
+ return fmt.Errorf("write header %s: %w", filename, err)
+ }
+ encoder := xml.NewEncoder(file)
+ encoder.Indent("", " ")
+ if err := encoder.Encode(data); err != nil {
+ return fmt.Errorf("encode %s: %w", filename, err)
+ }
+ if err := encoder.Flush(); err != nil {
+ return fmt.Errorf("flush %s: %w", filename, err)
+ }
+ return file.Sync()
+}
diff --git a/app/skd/job/sitemap/data.go b/app/skd/job/sitemap/data.go
new file mode 100644
index 0000000..19965af
--- /dev/null
+++ b/app/skd/job/sitemap/data.go
@@ -0,0 +1,312 @@
+package sitemap
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// idWithTime 携带 ID、slug 和实际更新时间
+type idWithTime struct {
+ Id int64
+ Slug string
+ LastMod time.Time
+}
+
+func hasAnyID(ids []int64, used map[int64]struct{}) bool {
+ for _, id := range ids {
+ if _, ok := used[id]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+func isSitemapChannelPath(path, prefix string, extras ...string) bool {
+ if path == prefix || strings.HasPrefix(path, prefix+"/") {
+ return true
+ }
+ for _, extra := range extras {
+ if path == extra {
+ return true
+ }
+ }
+ return false
+}
+
+func detailSegment(slug string, id int64) string {
+ if s := strings.TrimSpace(slug); s != "" {
+ return s
+ }
+ if id > 0 {
+ return strconv.FormatInt(id, 10)
+ }
+ return ""
+}
+
+func buildChannelKeyURLs(webAddr, channel, keyPrefix string, items []idWithTime, changeFreq string, priority float32) []URL {
+ urls := make([]URL, 0, len(items))
+ channel = strings.TrimRight(channel, "/")
+ for _, item := range items {
+ seg := detailSegment(item.Slug, item.Id)
+ if seg == "" {
+ continue
+ }
+ lastMod := item.LastMod.Format(time.DateOnly)
+ if item.LastMod.IsZero() {
+ lastMod = time.Now().Format(time.DateOnly)
+ }
+ urls = append(urls, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, channel+"/"+keyPrefix+seg),
+ LastMod: lastMod,
+ ChangeFreq: changeFreq,
+ Priority: priority,
+ })
+ }
+ return urls
+}
+
+// buildCategoryURLs 分类页:/{channel}/c-{slug},无 slug 时回退 c-{id}。
+func buildCategoryURLs(webAddr, channel string, items []idWithTime, changeFreq string, priority float32) []URL {
+ return buildChannelKeyURLs(webAddr, channel, "c-", items, changeFreq, priority)
+}
+
+// buildTagURLs 标签页:/{channel}/t-{slug},无 slug 时回退 t-{id}。
+func buildTagURLs(webAddr, channel string, items []idWithTime, changeFreq string, priority float32) []URL {
+ return buildChannelKeyURLs(webAddr, channel, "t-", items, changeFreq, priority)
+}
+
+// buildDetailURLs 详情页:/{prefix}/{slug},无 slug 时回退数字 id。
+func buildDetailURLs(webAddr, prefix string, items []idWithTime, changeFreq string, priority float32) []URL {
+ urls := make([]URL, 0, len(items))
+ for _, item := range items {
+ seg := detailSegment(item.Slug, item.Id)
+ if seg == "" {
+ continue
+ }
+ lastMod := item.LastMod.Format(time.DateOnly)
+ if item.LastMod.IsZero() {
+ lastMod = time.Now().Format(time.DateOnly)
+ }
+ urls = append(urls, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, prefix+"/"+seg),
+ LastMod: lastMod,
+ ChangeFreq: changeFreq,
+ Priority: priority,
+ })
+ }
+ return urls
+}
+
+// buildURLs 根据 ID+时间列表批量构建 URL 条目
+func buildURLs(webAddr string, pathFmt string, items []idWithTime, changeFreq string, priority float32) []URL {
+ urls := make([]URL, 0, len(items))
+ for _, item := range items {
+ lastMod := item.LastMod.Format(time.DateOnly)
+ if item.LastMod.IsZero() {
+ lastMod = time.Now().Format(time.DateOnly)
+ }
+ urls = append(urls, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, fmt.Sprintf(pathFmt, item.Id)),
+ LastMod: lastMod,
+ ChangeFreq: changeFreq,
+ Priority: priority,
+ })
+ }
+ return urls
+}
+
+// filterIdsWithTime 返回 source 中存在于 validMap 的条目(含 slug)。
+func filterIdsWithTime(source map[int64]struct{}, validMap map[int64]idWithTime) []idWithTime {
+ result := make([]idWithTime, 0, len(source))
+ for id := range source {
+ if item, ok := validMap[id]; ok {
+ result = append(result, item)
+ }
+ }
+ return result
+}
+
+func getVideoCategoryList(ctx context.Context, usedTagIds map[int64]struct{}, hasVideos bool) []idWithTime {
+ filter := bson.M{
+ videocategorymod.FN.VideoCategory.Status: true,
+ }
+ pageSize := int64(1000)
+ items := make([]idWithTime, 0)
+ for i := int64(0); i < 500; i++ {
+ skip := i * pageSize
+ list, _ := videocategorymod.FindManyVideoCategorys(ctx, filter, options.Find().SetSort(bson.M{videocategorymod.FN.VideoCategory.Id: -1}).SetSkip(skip).SetLimit(pageSize))
+ for _, info := range list {
+ if len(info.Tags) == 0 {
+ if !hasVideos {
+ continue
+ }
+ } else if !hasAnyID(info.Tags, usedTagIds) {
+ continue
+ }
+ if !videoCategoryHasContent(ctx, info) {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items
+}
+
+// videoCategoryContentFilter 与 app QueryVideoMore 使用相同的视频筛选条件。
+func videoCategoryContentFilter(category *videocategorymod.VideoCategory) bson.M {
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ }
+ if category != nil && len(category.Tags) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": category.Tags}
+ }
+ return filter
+}
+
+func videoCategoryHasContent(ctx context.Context, category *videocategorymod.VideoCategory) bool {
+ if category == nil || !category.Status {
+ return false
+ }
+ count, err := videoinformationmod.CountVideoInformations(ctx, videoCategoryContentFilter(category))
+ return err == nil && count > 0
+}
+
+func getActress(ctx context.Context) map[int64]idWithTime {
+ filter := bson.M{
+ actress.FN.Actress.Status: true,
+ }
+ pageSize := int64(1000)
+ pageIds := make(map[int64]idWithTime)
+ for i := int64(0); i < 100; i++ {
+ skip := i * pageSize
+ list, _ := actress.FindManyActresss(ctx, filter, options.Find().SetSkip(skip).SetLimit(pageSize).SetSort(bson.M{actress.FN.Actress.Id: -1}))
+ for _, info := range list {
+ pageIds[info.Id] = idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt}
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return pageIds
+}
+
+func videoEntriesToIDWithTime(entries []videoEntry) []idWithTime {
+ items := make([]idWithTime, len(entries))
+ for i, e := range entries {
+ items[i] = idWithTime{Id: e.Id, Slug: e.Slug, LastMod: e.LastMod}
+ }
+ return items
+}
+
+func getVideoList(ctx context.Context) ([]videoEntry, map[int64]struct{}, map[int64]struct{}) {
+ filter := bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK}
+ pageSize := int64(1000)
+ items := make([]videoEntry, 0)
+ tagIds := make(map[int64]struct{})
+ actressIds := make(map[int64]struct{})
+ for i := int64(0); i < 500; i++ {
+ skip := i * pageSize
+ list, _ := videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: -1}).SetLimit(pageSize).SetSkip(skip))
+ for _, info := range list {
+ items = append(items, videoEntry{
+ Id: info.Id,
+ Slug: info.Slug,
+ Title: info.Title,
+ LastMod: info.UpdatedAt,
+ UnencryptedPreMp4: info.Video.UnencryptedPreMp4,
+ })
+ for _, tagId := range info.Tags {
+ tagIds[tagId] = struct{}{}
+ }
+ for _, actressId := range info.ActressIds {
+ if actressId > 0 {
+ actressIds[actressId] = struct{}{}
+ }
+ }
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items, tagIds, actressIds
+}
+
+func getTranslation(config cfg.SysCfg, ctx context.Context) []string {
+ urlList := make([]string, 0)
+ wg := &sync.WaitGroup{}
+ wg.Add(2)
+ var translationCategoryList translationcategorymod.TranslationCategoryList
+ var translationList translationmod.TranslationInfoList
+ go func() {
+ defer wg.Done()
+ categoryFilter := bson.M{
+ translationcategorymod.FN.TranslationCategory.Status: true,
+ }
+ categoryOpt := &options.FindOptions{}
+ categoryOpt.SetLimit(1000).SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1})
+
+ translationCategoryList, _ = translationcategorymod.FindManyTranslationCategorys(ctx, categoryFilter, categoryOpt)
+ }()
+ go func() {
+ defer wg.Done()
+ translationFilter := bson.M{
+ translationmod.FN.TranslationInfo.Status: true,
+ }
+ translationOpt := &options.FindOptions{}
+ translationOpt.SetLimit(1000).SetSort(bson.M{translationmod.FN.TranslationInfo.Rank: -1})
+
+ translationList, _ = translationmod.FindManyTranslationInfos(ctx, translationFilter, translationOpt)
+ }()
+
+ wg.Wait()
+
+ // 按 categoryId 建索引,避免嵌套遍历
+ translationByCategory := make(map[int64][]*translationmod.TranslationInfo, len(translationList))
+ for _, t := range translationList {
+ translationByCategory[t.TransCategoryId] = append(translationByCategory[t.TransCategoryId], t)
+ }
+
+ for _, categoryInfo := range translationCategoryList {
+ if categoryInfo.CategoryType == pubdef.TranslationTypeUrl {
+ if strings.HasPrefix(categoryInfo.UrlValue, "http://") || strings.HasPrefix(categoryInfo.UrlValue, "https://") {
+ continue
+ }
+ urlList = append(urlList, utils.JoinUrlPathAppendEnd(config.WebAddr, categoryInfo.UrlValue))
+ } else if categoryInfo.CategoryType == pubdef.TranslationTypeSub || categoryInfo.CategoryType == pubdef.TranslationTypeBottom {
+ for _, translationInfo := range translationByCategory[categoryInfo.Id] {
+ if translationInfo.IsFriendAds {
+ continue
+ } else if translationInfo.HtmlId > 0 {
+ title := strings.TrimSpace(translationInfo.Name)
+ if title != "" {
+ urlList = append(urlList, utils.JoinUrlPathKeepSlash(config.WebAddr, "/richtext?title="+url.QueryEscape(title)))
+ }
+ } else if len(translationInfo.Link) > 0 {
+ if !strings.HasPrefix(translationInfo.Link, "http://") && !strings.HasPrefix(translationInfo.Link, "https://") {
+ urlList = append(urlList, utils.JoinUrlPathAppendEnd(config.WebAddr, translationInfo.Link))
+ }
+ }
+ }
+ }
+ }
+ return urlList
+}
diff --git a/app/skd/job/sitemap/data_test.go b/app/skd/job/sitemap/data_test.go
new file mode 100644
index 0000000..e18b64b
--- /dev/null
+++ b/app/skd/job/sitemap/data_test.go
@@ -0,0 +1,108 @@
+package sitemap
+
+import (
+ "testing"
+ "time"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func TestIsSitemapChannelPath(t *testing.T) {
+ if !isSitemapChannelPath("/drama", "/drama", "/drama-rank") {
+ t.Fatal("channel home should match")
+ }
+ if !isSitemapChannelPath("/drama/all", "/drama", "/drama-rank") {
+ t.Fatal("channel child path should match")
+ }
+ if !isSitemapChannelPath("/drama-rank", "/drama", "/drama-rank") {
+ t.Fatal("hyphen rank path should match")
+ }
+ if isSitemapChannelPath("/drama-rank", "/drama") {
+ t.Fatal("hyphen rank must not match prefix-only")
+ }
+ if isSitemapChannelPath("/video-rank", "/drama", "/drama-rank") {
+ t.Fatal("other channel rank should not match")
+ }
+}
+
+func TestBuildDetailURLs(t *testing.T) {
+ items := []idWithTime{
+ {Id: 53942, Slug: "sirozFs", LastMod: time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC)},
+ }
+ urls := buildDetailURLs("https://example.com/", "/video-detail", items, "weekly", 0.8)
+ if len(urls) != 1 {
+ t.Fatalf("buildDetailURLs() returned %d URLs, want 1", len(urls))
+ }
+ if want := "https://example.com/video-detail/sirozFs"; urls[0].Loc != want {
+ t.Fatalf("buildDetailURLs() loc = %q, want %q", urls[0].Loc, want)
+ }
+
+ fallback := buildDetailURLs("https://example.com/", "/video-detail", []idWithTime{{Id: 12}}, "weekly", 0.8)
+ if len(fallback) != 1 || fallback[0].Loc != "https://example.com/video-detail/12" {
+ t.Fatalf("empty slug should fall back to id, got %#v", fallback)
+ }
+}
+
+func TestBuildTagURLs(t *testing.T) {
+ slugURL := buildTagURLs("https://example.com/", "/video", []idWithTime{{Id: 9, Slug: "saduanfa"}}, "weekly", 0.6)
+ if len(slugURL) != 1 || slugURL[0].Loc != "https://example.com/video/t-saduanfa" {
+ t.Fatalf("slug tag loc = %#v", slugURL)
+ }
+ idURL := buildTagURLs("https://example.com/", "/drama", []idWithTime{{Id: 10142}}, "weekly", 0.6)
+ if len(idURL) != 1 || idURL[0].Loc != "https://example.com/drama/t-10142" {
+ t.Fatalf("id tag loc = %#v", idURL)
+ }
+}
+
+func TestBuildCategoryURLs(t *testing.T) {
+ slugURL := buildCategoryURLs("https://example.com/", "/video", []idWithTime{{Id: 37513, Slug: "hot-av"}}, "weekly", 0.6)
+ if len(slugURL) != 1 || slugURL[0].Loc != "https://example.com/video/c-hot-av" {
+ t.Fatalf("slug category loc = %#v", slugURL)
+ }
+ idURL := buildCategoryURLs("https://example.com/", "/drama", []idWithTime{{Id: 10001}}, "weekly", 0.6)
+ if len(idURL) != 1 || idURL[0].Loc != "https://example.com/drama/c-10001" {
+ t.Fatalf("id category loc = %#v", idURL)
+ }
+}
+
+func TestBuildPlayletSitemapURLs(t *testing.T) {
+ items := []idWithTime{{Id: 88, Slug: "chuanchengjiaofubei", LastMod: time.Date(2026, 9, 4, 0, 0, 0, 0, time.UTC)}}
+ detail := buildDetailURLs("https://example.com/", "/drama-detail", items, "weekly", 0.8)
+ if len(detail) != 1 || detail[0].Loc != "https://example.com/drama-detail/chuanchengjiaofubei" {
+ t.Fatalf("playlet detail loc = %#v", detail)
+ }
+ category := buildCategoryURLs("https://example.com/", "/drama", items, "weekly", 0.6)
+ if len(category) != 1 || category[0].Loc != "https://example.com/drama/c-chuanchengjiaofubei" {
+ t.Fatalf("playlet category loc = %#v", category)
+ }
+ idOnly := buildCategoryURLs("https://example.com/", "/video", []idWithTime{{Id: 37513}}, "weekly", 0.6)
+ if len(idOnly) != 1 || idOnly[0].Loc != "https://example.com/video/c-37513" {
+ t.Fatalf("category id fallback loc = %#v", idOnly)
+ }
+ tag := buildTagURLs("https://example.com/", "/drama", items, "weekly", 0.6)
+ if len(tag) != 1 || tag[0].Loc != "https://example.com/drama/t-chuanchengjiaofubei" {
+ t.Fatalf("playlet tag loc = %#v", tag)
+ }
+}
+
+func TestVideoCategoryContentFilter(t *testing.T) {
+ filter := videoCategoryContentFilter(&videocategorymod.VideoCategory{
+ Status: true,
+ Tags: []int64{11, 22},
+ })
+ tagsFilter, ok := filter[videoinformationmod.FN.VideoInformation.Tags].(bson.M)
+ if !ok {
+ t.Fatalf("tags filter = %#v, want bson.M", filter[videoinformationmod.FN.VideoInformation.Tags])
+ }
+ in, ok := tagsFilter["$in"].([]int64)
+ if !ok || len(in) != 2 || in[0] != 11 || in[1] != 22 {
+ t.Fatalf("tags $in = %#v, want [11 22]", tagsFilter["$in"])
+ }
+
+ emptyTags := videoCategoryContentFilter(&videocategorymod.VideoCategory{Status: true})
+ if _, exists := emptyTags[videoinformationmod.FN.VideoInformation.Tags]; exists {
+ t.Fatalf("empty tag category should not add tags filter: %#v", emptyTags)
+ }
+}
diff --git a/app/skd/job/sitemap/h5_sitemap.go b/app/skd/job/sitemap/h5_sitemap.go
new file mode 100644
index 0000000..fff10ab
--- /dev/null
+++ b/app/skd/job/sitemap/h5_sitemap.go
@@ -0,0 +1,281 @@
+package sitemap
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playlethomeconfigmod"
+ "videoweb/pub/models/playletthememod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ h5SortHot = "hot"
+ h5SortLatest = "latest"
+ h5TopicSort = "editorial"
+ h5RankPlaylet = "drama"
+ h5RankVideo = "video"
+)
+
+// getUrl 按 Next H5 路由生成 sitemap 分片:页面、短剧详情、视频详情。
+func getUrl(sg *SitemapGenerator) {
+ webAddr := strings.TrimRight(strings.TrimSpace(sg.WebAddr), "/")
+ if webAddr == "" {
+ return
+ }
+
+ ctx := context.Background()
+ var (
+ wg sync.WaitGroup
+
+ videoEntries []videoEntry
+ videoTag map[int64]struct{}
+
+ playletItems []idWithTime
+ playletTagIDs map[int64]struct{}
+
+ themes []idWithTime
+ homeModules []idWithTime
+ )
+
+ wg.Add(4)
+ go func() {
+ defer wg.Done()
+ videoEntries, videoTag, _ = getVideoList(ctx)
+ }()
+ go func() {
+ defer wg.Done()
+ playletItems, _, playletTagIDs = getPlayletList(ctx)
+ }()
+ go func() {
+ defer wg.Done()
+ themes = getEnabledPlayletThemes(ctx)
+ }()
+ go func() {
+ defer wg.Done()
+ homeModules = getEnabledHomeModules(ctx)
+ }()
+ wg.Wait()
+
+ videoCatItems := getVideoCategoryList(ctx, videoTag, len(videoEntries) > 0)
+ videoTagItems := getContentTagList(ctx, videoTag, contenttagmod.ContentTagTypeVideo)
+ playletTagItems := getContentTagList(ctx, playletTagIDs, contenttagmod.ContentTagTypePlaylet)
+
+ now := time.Now()
+ pages := make([]URL, 0, 64)
+ seen := make(map[string]struct{})
+ add := func(dst *[]URL, loc, changeFreq string, priority float32, lastMod time.Time) {
+ loc = strings.TrimSpace(loc)
+ if loc == "" {
+ return
+ }
+ if _, ok := seen[loc]; ok {
+ return
+ }
+ seen[loc] = struct{}{}
+ if lastMod.IsZero() {
+ lastMod = now
+ }
+ *dst = append(*dst, URL{
+ Loc: loc,
+ LastMod: lastMod.Format(time.DateOnly),
+ ChangeFreq: changeFreq,
+ Priority: priority,
+ })
+ }
+
+ for _, spec := range h5StaticPages() {
+ add(&pages, h5Abs(webAddr, spec.path), spec.changeFreq, spec.priority, now)
+ }
+ for _, sort := range []string{h5SortHot, h5SortLatest} {
+ add(&pages, h5Abs(webAddr, "/short-drama", sort), "daily", 0.85, now)
+ add(&pages, h5Abs(webAddr, "/video", sort), "daily", 0.85, now)
+ for _, theme := range themes {
+ seg := detailSegment(theme.Slug, theme.Id)
+ if seg == "" || isReservedListSegment(seg) {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/short-drama", sort, seg), "daily", 0.7, theme.LastMod)
+ }
+ for _, category := range videoCatItems {
+ seg := detailSegment(category.Slug, category.Id)
+ if seg == "" || isReservedListSegment(seg) {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/video", sort, seg), "daily", 0.7, category.LastMod)
+ }
+ }
+ for _, tag := range playletTagItems {
+ seg := detailSegment(tag.Slug, tag.Id)
+ if seg == "" {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/tag/playlet", seg), "weekly", 0.65, tag.LastMod)
+ }
+ for _, theme := range themes {
+ seg := detailSegment(theme.Slug, theme.Id)
+ if seg == "" {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/topic/playlet", seg, h5TopicSort), "weekly", 0.7, theme.LastMod)
+ }
+ for _, tag := range videoTagItems {
+ seg := detailSegment(tag.Slug, tag.Id)
+ if seg == "" {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/tag/video", seg), "weekly", 0.65, tag.LastMod)
+ }
+ for _, category := range videoCatItems {
+ seg := detailSegment(category.Slug, category.Id)
+ if seg == "" {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/topic/video", seg, h5TopicSort), "weekly", 0.7, category.LastMod)
+ }
+ for _, module := range homeModules {
+ if module.Id <= 0 {
+ continue
+ }
+ add(&pages, h5Abs(webAddr, "/more", moreSegment(module.Slug, module.Id)), "daily", 0.65, module.LastMod)
+ }
+ if len(pages) > 0 {
+ _ = sg.writeSitemapFile("sitemap-pages.xml", pages)
+ }
+
+ playletURLs := make([]URL, 0, len(playletItems))
+ for _, item := range playletItems {
+ seg := detailSegment(item.Slug, item.Id)
+ if seg == "" || isReservedListSegment(seg) {
+ continue
+ }
+ add(&playletURLs, h5Abs(webAddr, "/short-drama", seg), "weekly", 0.8, item.LastMod)
+ }
+ if len(playletURLs) > 0 {
+ _ = sg.writeSitemapFiles("sitemap-playletdetail", playletURLs)
+ }
+
+ videoURLs := make([]URL, 0, len(videoEntries))
+ for _, item := range videoEntries {
+ seg := detailSegment(item.Slug, item.Id)
+ if seg == "" || isReservedListSegment(seg) {
+ continue
+ }
+ add(&videoURLs, h5Abs(webAddr, "/video", seg), "weekly", 0.8, item.LastMod)
+ }
+ if len(videoURLs) > 0 {
+ _ = sg.writeSitemapFiles("sitemap-videodetail", videoURLs)
+ }
+}
+
+type h5StaticPage struct {
+ path string
+ changeFreq string
+ priority float32
+}
+
+func h5StaticPages() []h5StaticPage {
+ return []h5StaticPage{
+ {path: "/", changeFreq: "daily", priority: 1},
+ {path: "/welfare", changeFreq: "weekly", priority: 0.6},
+ {path: "/nav", changeFreq: "weekly", priority: 0.7},
+ {path: "/way-home", changeFreq: "weekly", priority: 0.7},
+ {path: "/about", changeFreq: "monthly", priority: 0.4},
+ {path: "/terms", changeFreq: "monthly", priority: 0.3},
+ {path: "/privacy", changeFreq: "monthly", priority: 0.3},
+ {path: "/help", changeFreq: "monthly", priority: 0.3},
+ {path: "/ranking/" + h5RankPlaylet, changeFreq: "daily", priority: 0.8},
+ {path: "/ranking/" + h5RankVideo, changeFreq: "daily", priority: 0.8},
+ }
+}
+
+func isReservedListSegment(seg string) bool {
+ return seg == h5SortHot || seg == h5SortLatest || seg == h5RankPlaylet || seg == h5RankVideo
+}
+
+// moreSegment 首页更多页使用配置 slug。历史数据没有 slug 时使用 backfill 的字母标识。
+func moreSegment(slug string, id int64) string {
+ if s := strings.TrimSpace(slug); s != "" {
+ return s
+ }
+ if id > 0 {
+ return fmt.Sprintf("home-config-%d", id)
+ }
+ return ""
+}
+
+// h5Abs 拼出与前端 encodeURIComponent 路径段一致的绝对地址。
+func h5Abs(webAddr string, parts ...string) string {
+ segments := make([]string, 0, len(parts))
+ for _, part := range parts {
+ part = strings.Trim(part, "/")
+ if part == "" {
+ continue
+ }
+ for _, seg := range strings.Split(part, "/") {
+ seg = strings.TrimSpace(seg)
+ if seg == "" {
+ continue
+ }
+ segments = append(segments, url.PathEscape(seg))
+ }
+ }
+ path := "/"
+ if len(segments) > 0 {
+ path = "/" + strings.Join(segments, "/")
+ }
+ return utils.JoinUrlPathKeepSlash(webAddr, path)
+}
+
+func getEnabledPlayletThemes(ctx context.Context) []idWithTime {
+ filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
+ pageSize := int64(1000)
+ items := make([]idWithTime, 0)
+ for i := int64(0); i < 100; i++ {
+ list, _ := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
+ SetSort(bson.M{playletthememod.FN.PlayletTheme.Rank: -1}).
+ SetSkip(i*pageSize).
+ SetLimit(pageSize).
+ SetProjection(bson.M{
+ playletthememod.FN.PlayletTheme.Id: 1,
+ playletthememod.FN.PlayletTheme.Slug: 1,
+ playletthememod.FN.PlayletTheme.UpdatedAt: 1,
+ }))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items
+}
+
+func getEnabledHomeModules(ctx context.Context) []idWithTime {
+ filter := bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true}
+ list, _ := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, filter, options.Find().
+ SetProjection(bson.M{
+ playlethomeconfigmod.FN.PlayletHomeConfig.Id: 1,
+ playlethomeconfigmod.FN.PlayletHomeConfig.Slug: 1,
+ playlethomeconfigmod.FN.PlayletHomeConfig.UpdatedAt: 1,
+ }))
+ items := make([]idWithTime, 0, len(list))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ return items
+}
diff --git a/app/skd/job/sitemap/playlet.go b/app/skd/job/sitemap/playlet.go
new file mode 100644
index 0000000..9ac25f8
--- /dev/null
+++ b/app/skd/job/sitemap/playlet.go
@@ -0,0 +1,136 @@
+package sitemap
+
+import (
+ "context"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// getPlayletList 拉取已上架短剧,并收集实际用到的分类/标签 ID。
+func getPlayletList(ctx context.Context) ([]idWithTime, map[int64]struct{}, map[int64]struct{}) {
+ filter := bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ pageSize := int64(1000)
+ items := make([]idWithTime, 0)
+ categoryIDs := make(map[int64]struct{})
+ tagIDs := make(map[int64]struct{})
+ for i := int64(0); i < 500; i++ {
+ skip := i * pageSize
+ list, _ := playletmod.FindManyPlaylet(ctx, filter, options.Find().
+ SetSort(bson.M{playletmod.FN.Playlet.Id: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize).
+ SetProjection(bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.Slug: 1,
+ playletmod.FN.Playlet.UpdatedAt: 1,
+ playletmod.FN.Playlet.OnlineAt: 1,
+ playletmod.FN.Playlet.CategoryIds: 1,
+ playletmod.FN.Playlet.TagIds: 1,
+ }))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 {
+ continue
+ }
+ lastMod := info.UpdatedAt
+ if lastMod.IsZero() {
+ lastMod = info.OnlineAt
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: lastMod})
+ for _, id := range info.CategoryIds {
+ if id > 0 {
+ categoryIDs[id] = struct{}{}
+ }
+ }
+ for _, id := range info.TagIds {
+ if id > 0 {
+ tagIDs[id] = struct{}{}
+ }
+ }
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items, categoryIDs, tagIDs
+}
+
+// getPlayletCategoryList 只收录有上架短剧的分类,避免空列表页被抓取。
+func getPlayletCategoryList(ctx context.Context, usedCategoryIDs map[int64]struct{}) []idWithTime {
+ if len(usedCategoryIDs) == 0 {
+ return nil
+ }
+ filter := bson.M{
+ playletcategorymod.FN.PlayletCategory.Status: true,
+ }
+ pageSize := int64(1000)
+ items := make([]idWithTime, 0)
+ for i := int64(0); i < 100; i++ {
+ skip := i * pageSize
+ list, _ := playletcategorymod.FindManyPlayletCategory(ctx, filter, options.Find().
+ SetSort(bson.M{playletcategorymod.FN.PlayletCategory.Rank: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize).
+ SetProjection(bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: 1,
+ playletcategorymod.FN.PlayletCategory.Slug: 1,
+ playletcategorymod.FN.PlayletCategory.UpdatedAt: 1,
+ }))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 {
+ continue
+ }
+ if _, ok := usedCategoryIDs[info.Id]; !ok {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items
+}
+
+// getContentTagList 只收录当前频道已绑定内容的统一标签。
+func getContentTagList(ctx context.Context, usedTagIDs map[int64]struct{}, tagType contenttagmod.ContentTagType) []idWithTime {
+ if len(usedTagIDs) == 0 {
+ return nil
+ }
+ filter := bson.M{
+ contenttagmod.FN.ContentTag.Status: true,
+ contenttagmod.FN.ContentTag.Types: tagType,
+ }
+ pageSize := int64(1000)
+ items := make([]idWithTime, 0)
+ for i := int64(0); i < 100; i++ {
+ skip := i * pageSize
+ list, _ := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
+ SetSort(bson.M{contenttagmod.FN.ContentTag.Rank: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize).
+ SetProjection(bson.M{
+ contenttagmod.FN.ContentTag.Id: 1,
+ contenttagmod.FN.ContentTag.Slug: 1,
+ contenttagmod.FN.ContentTag.UpdatedAt: 1,
+ }))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 {
+ continue
+ }
+ if _, ok := usedTagIDs[info.Id]; !ok {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items
+}
diff --git a/app/skd/job/sitemap/sitemap.go b/app/skd/job/sitemap/sitemap.go
new file mode 100644
index 0000000..eed5432
--- /dev/null
+++ b/app/skd/job/sitemap/sitemap.go
@@ -0,0 +1,245 @@
+package sitemap
+
+import (
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ // 浏览器用 XSLT 把 loc 渲成可点击链接;搜索引擎仍按 XML 解析
+ stylesheetIndex = `` + "\n"
+ stylesheetURLSet = `` + "\n"
+
+ // defaultMaxURLsPerFile 单文件 URL 上限;Google 上限 50000,留余量避免体积过大。
+ defaultMaxURLsPerFile = 40000
+)
+
+// URL 单个URL条目
+type URL struct {
+ Loc string `xml:"loc"`
+ LastMod string `xml:"lastmod"`
+ ChangeFreq string `xml:"changefreq"` // always/hourly/daily/weekly/monthly/yearly/never
+ Priority float32 `xml:"priority"` // 0.0 - 1.0
+}
+
+// Sitemap 站点地图文件
+type Sitemap struct {
+ XMLName xml.Name `xml:"urlset"`
+ Xmlns string `xml:"xmlns,attr"`
+ URLs []URL `xml:"url"`
+}
+
+// SitemapIndex 站点地图索引
+type SitemapIndex struct {
+ XMLName xml.Name `xml:"sitemapindex"`
+ Xmlns string `xml:"xmlns,attr"`
+ Sitemaps []SitemapIndexEntry `xml:"sitemap"`
+}
+
+// SitemapIndexEntry 站点地图索引条目
+type SitemapIndexEntry struct {
+ Loc string `xml:"loc"`
+ LastMod string `xml:"lastmod"`
+}
+
+// SitemapGenerator 站点地图生成器
+type SitemapGenerator struct {
+ MaxURLsPerFile int // 每个sitemap文件最大URL数量
+ OutputDir string // 分片输出目录(/sitemaps)
+ IndexFile string // 索引文件路径;空则写到 OutputDir/sitemap.xml
+ WebAddr string // 网站地址
+ SitemapFiles []string // 生成的sitemap文件列表
+}
+
+// NewSitemapGenerator 创建新的站点地图生成器
+func NewSitemapGenerator(webAddr, outputDir string) *SitemapGenerator {
+ return &SitemapGenerator{
+ MaxURLsPerFile: defaultMaxURLsPerFile,
+ OutputDir: outputDir,
+ WebAddr: webAddr,
+ SitemapFiles: make([]string, 0),
+ }
+}
+
+func (sg *SitemapGenerator) maxURLsPerFile() int {
+ if sg.MaxURLsPerFile > 0 {
+ return sg.MaxURLsPerFile
+ }
+ return defaultMaxURLsPerFile
+}
+
+// Generate 生成站点地图
+func (sg *SitemapGenerator) Generate() error {
+ // 清空之前的文件列表
+ sg.SitemapFiles = make([]string, 0)
+
+ // 创建输出目录
+ if err := os.MkdirAll(sg.OutputDir, 0755); err != nil {
+ return err
+ }
+ deprecatedFiles := []string{
+ "sitemap-createpostlist.xml",
+ "sitemap-postlist.xml",
+ "sitemap-posttag.xml",
+ "sitemap-createvideolist.xml",
+ "sitemap-tag.xml",
+ "sitemap-translation.xml",
+ "sitemap-actress.xml",
+ "sitemap-videocategory.xml",
+ "sitemap-playletcategory.xml",
+ "sitemap-videotag.xml",
+ "sitemap-playlettag.xml",
+ "sitemap-archives.xml",
+ "sitemap-videos.xml",
+ }
+ for _, filename := range deprecatedFiles {
+ if err := os.Remove(filepath.Join(sg.OutputDir, filename)); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ sg.cleanupSitemapShards("sitemap-archives", 0)
+ sg.cleanupSitemapShards("sitemap-videos", 0)
+ if sg.IndexFile != "" && filepath.Clean(sg.IndexFile) != filepath.Clean(filepath.Join(sg.OutputDir, "sitemap.xml")) {
+ if err := os.Remove(filepath.Join(sg.OutputDir, "sitemap.xml")); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ getUrl(sg)
+
+ // 生成sitemap索引文件
+ return sg.generateSitemapIndex()
+}
+
+// writeSitemapFile 写入sitemap文件
+func (sg *SitemapGenerator) writeSitemapFile(filename string, urls []URL) error {
+ sitemap := Sitemap{
+ Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
+ URLs: urls,
+ }
+
+ filePath := filepath.Join(sg.OutputDir, filename)
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ // 添加XML头
+ file.WriteString(xml.Header)
+ file.WriteString(stylesheetURLSet)
+ encoder := xml.NewEncoder(file)
+ encoder.Indent("", " ")
+
+ if err := encoder.Encode(sitemap); err != nil {
+ return err
+ }
+
+ // 记录生成的文件
+ sg.SitemapFiles = append(sg.SitemapFiles, filename)
+ return nil
+}
+
+// writeSitemapFiles 写入 sitemap 分片;超过上限时拆为 baseName-0.xml、baseName-1.xml ...
+func (sg *SitemapGenerator) writeSitemapFiles(baseName string, urls []URL) error {
+ baseName = strings.TrimSuffix(strings.TrimSpace(baseName), ".xml")
+ if baseName == "" {
+ return nil
+ }
+ if len(urls) == 0 {
+ sg.cleanupSitemapShards(baseName, 0)
+ return nil
+ }
+
+ limit := sg.maxURLsPerFile()
+ shardCount := (len(urls) + limit - 1) / limit
+ if shardCount == 1 {
+ sg.cleanupSitemapShards(baseName, 1)
+ return sg.writeSitemapFile(baseName+".xml", urls)
+ }
+
+ sg.cleanupSitemapShards(baseName, shardCount)
+ for i := 0; i < shardCount; i++ {
+ start := i * limit
+ end := start + limit
+ if end > len(urls) {
+ end = len(urls)
+ }
+ filename := fmt.Sprintf("%s-%d.xml", baseName, i)
+ if err := sg.writeSitemapFile(filename, urls[start:end]); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// cleanupSitemapShards 清理旧分片:shardCount=1 保留 baseName.xml;>1 保留 baseName-0..N-1.xml。
+func (sg *SitemapGenerator) cleanupSitemapShards(baseName string, shardCount int) {
+ entries, err := os.ReadDir(sg.OutputDir)
+ if err != nil {
+ return
+ }
+ prefix := baseName + "-"
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ name := entry.Name()
+ if strings.HasPrefix(name, prefix) && strings.HasSuffix(name, ".xml") {
+ idxText := strings.TrimSuffix(strings.TrimPrefix(name, prefix), ".xml")
+ idx, err := strconv.Atoi(idxText)
+ if err != nil {
+ continue
+ }
+ if shardCount <= 1 || idx >= shardCount {
+ _ = os.Remove(filepath.Join(sg.OutputDir, name))
+ }
+ continue
+ }
+ if shardCount > 1 && name == baseName+".xml" {
+ _ = os.Remove(filepath.Join(sg.OutputDir, name))
+ }
+ }
+}
+
+// generateSitemapIndex 生成sitemap索引文件
+func (sg *SitemapGenerator) generateSitemapIndex() error {
+ index := SitemapIndex{
+ Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
+ Sitemaps: make([]SitemapIndexEntry, 0, len(sg.SitemapFiles)),
+ }
+
+ now := time.Now().Format(time.DateOnly)
+ webAddr := strings.TrimRight(strings.TrimSpace(sg.WebAddr), "/")
+ for _, filename := range sg.SitemapFiles {
+ index.Sitemaps = append(index.Sitemaps, SitemapIndexEntry{
+ Loc: fmt.Sprintf("%s/sitemaps/%s", webAddr, filename),
+ LastMod: now,
+ })
+ }
+
+ filePath := sg.IndexFile
+ if strings.TrimSpace(filePath) == "" {
+ filePath = filepath.Join(sg.OutputDir, "sitemap.xml")
+ }
+ if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
+ return err
+ }
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ // 添加XML头
+ file.WriteString(xml.Header)
+ file.WriteString(stylesheetIndex)
+ encoder := xml.NewEncoder(file)
+ encoder.Indent("", " ")
+
+ return encoder.Encode(index)
+}
diff --git a/app/skd/job/sitemap/video_sitemap.go b/app/skd/job/sitemap/video_sitemap.go
new file mode 100644
index 0000000..c03a56a
--- /dev/null
+++ b/app/skd/job/sitemap/video_sitemap.go
@@ -0,0 +1,180 @@
+package sitemap
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/utils"
+)
+
+const (
+ videoSitemapShardPrefix = "sitemap-videos-"
+ videoSitemapDefaultThumb = "/static/images/og-default.jpg"
+ videoSitemapDefaultDesc = "高清成人视频在线观看。"
+ videoSitemapEmbedPathFmt = "/video/embed?id=%d"
+)
+
+type videoEntry struct {
+ Id int64
+ Slug string
+ Title string
+ LastMod time.Time
+ UnencryptedPreMp4 string
+ ContentLoc string
+}
+
+// prepareVideoSitemapEntries 为全部视频补齐 sitemap 字段;有 UnencryptedPreMp4 时额外设置 content_loc。
+func prepareVideoSitemapEntries(entries []videoEntry, cdn string) []videoEntry {
+ cdn = strings.TrimSpace(cdn)
+ out := make([]videoEntry, 0, len(entries))
+ for _, e := range entries {
+ if e.Id <= 0 {
+ continue
+ }
+ if cdn != "" {
+ if preMp4 := strings.TrimSpace(e.UnencryptedPreMp4); preMp4 != "" {
+ if contentLoc := netutil.MergeURL(cdn, preMp4); contentLoc != "" {
+ e.ContentLoc = contentLoc
+ }
+ }
+ }
+ out = append(out, e)
+ }
+ return out
+}
+
+func (sg *SitemapGenerator) writeVideoSitemapFiles(entries []videoEntry) error {
+ if len(entries) == 0 {
+ sg.cleanupVideoSitemapShards(0)
+ return nil
+ }
+
+ limit := sg.maxURLsPerFile()
+ shardCount := (len(entries) + limit - 1) / limit
+ sg.cleanupVideoSitemapShards(shardCount)
+
+ thumb := strings.TrimRight(sg.WebAddr, "/") + videoSitemapDefaultThumb
+ for i := 0; i < len(entries); i += limit {
+ end := i + limit
+ if end > len(entries) {
+ end = len(entries)
+ }
+ name := fmt.Sprintf("%s%d.xml", videoSitemapShardPrefix, i/limit)
+ if err := sg.writeOneVideoSitemap(name, entries[i:end], thumb); err != nil {
+ return err
+ }
+ sg.SitemapFiles = append(sg.SitemapFiles, name)
+ }
+ return nil
+}
+
+func (sg *SitemapGenerator) cleanupVideoSitemapShards(shardCount int) {
+ entries, err := os.ReadDir(sg.OutputDir)
+ if err != nil {
+ return
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ name := entry.Name()
+ if !strings.HasPrefix(name, videoSitemapShardPrefix) || !strings.HasSuffix(name, ".xml") {
+ continue
+ }
+ idxText := strings.TrimSuffix(strings.TrimPrefix(name, videoSitemapShardPrefix), ".xml")
+ idx, err := strconv.Atoi(idxText)
+ if err != nil || shardCount <= 0 || idx >= shardCount {
+ _ = os.Remove(filepath.Join(sg.OutputDir, name))
+ }
+ }
+}
+
+func (sg *SitemapGenerator) writeOneVideoSitemap(filename string, entries []videoEntry, thumb string) error {
+ var b bytes.Buffer
+ b.WriteString(xml.Header)
+ b.WriteString(stylesheetURLSet)
+ b.WriteString(`` + "\n")
+ for _, e := range entries {
+ if e.Id <= 0 {
+ continue
+ }
+ loc := videoEntryLoc(sg.WebAddr, e)
+ if loc == "" {
+ continue
+ }
+ playerLoc := videoEntryPlayerLoc(sg.WebAddr, e.Id)
+ title := xmlEscapeVideoSitemap(videoEntryTitle(e))
+ desc := xmlEscapeVideoSitemap(videoSitemapDefaultDesc)
+ lastMod := videoEntryLastMod(e)
+
+ b.WriteString(" \n")
+ fmt.Fprintf(&b, " %s\n", xmlEscapeVideoSitemap(loc))
+ fmt.Fprintf(&b, " %s\n", xmlEscapeVideoSitemap(lastMod))
+ b.WriteString(" weekly\n")
+ b.WriteString(" 0.8\n")
+ b.WriteString(" \n")
+ fmt.Fprintf(&b, " %s\n", xmlEscapeVideoSitemap(thumb))
+ fmt.Fprintf(&b, " %s\n", title)
+ fmt.Fprintf(&b, " %s\n", desc)
+ if contentLoc := strings.TrimSpace(e.ContentLoc); contentLoc != "" {
+ fmt.Fprintf(&b, " %s\n", xmlEscapeVideoSitemap(contentLoc))
+ }
+ fmt.Fprintf(&b, " %s\n", xmlEscapeVideoSitemap(playerLoc))
+ b.WriteString(" no\n")
+ b.WriteString(" \n")
+ b.WriteString(" \n")
+ }
+ b.WriteString("\n")
+
+ path := filepath.Join(sg.OutputDir, filename)
+ return os.WriteFile(path, b.Bytes(), 0644)
+}
+
+func videoEntryLoc(webAddr string, e videoEntry) string {
+ seg := detailSegment(e.Slug, e.Id)
+ if seg == "" {
+ return ""
+ }
+ return utils.JoinUrlPathKeepSlash(webAddr, "/video-detail/"+seg)
+}
+
+func videoEntryPlayerLoc(webAddr string, id int64) string {
+ return utils.JoinUrlPathKeepSlash(webAddr, fmt.Sprintf(videoSitemapEmbedPathFmt, id))
+}
+
+func videoEntryTitle(e videoEntry) string {
+ if title := strings.TrimSpace(e.Title); title != "" {
+ return truncateRunesVideoSitemap(title, 100)
+ }
+ return fmt.Sprintf("视频 %d", e.Id)
+}
+
+func videoEntryLastMod(e videoEntry) string {
+ if e.LastMod.IsZero() {
+ return time.Now().Format(time.DateOnly)
+ }
+ return e.LastMod.Format(time.DateOnly)
+}
+
+func xmlEscapeVideoSitemap(s string) string {
+ var buf bytes.Buffer
+ _ = xml.EscapeText(&buf, []byte(s))
+ return buf.String()
+}
+
+func truncateRunesVideoSitemap(s string, max int) string {
+ if max <= 0 || utf8.RuneCountInString(s) <= max {
+ return s
+ }
+ runes := []rune(s)
+ return string(runes[:max])
+}
diff --git a/app/skd/seo/seo.go b/app/skd/seo/seo.go
new file mode 100644
index 0000000..23e81c9
--- /dev/null
+++ b/app/skd/seo/seo.go
@@ -0,0 +1,101 @@
+package seo
+
+import (
+ "context"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/app/service/configser"
+ "videoweb/app/skd/job/dhsitemap"
+ "videoweb/app/skd/job/llmstxt"
+ "videoweb/app/skd/job/rss"
+ "videoweb/app/skd/job/sitemap"
+ "videoweb/pub/common/log"
+ "videoweb/pub/seo/sitemapdirty"
+
+ "github.com/robfig/cron/v3"
+)
+
+const (
+ // h5OutputDir 站点地图写在 h5 根目录,不进 public。
+ // 样式表仍由前端 public/static 提供,XML 里引用 /static/sitemap*.xsl。
+ h5OutputDir = "../h5"
+ pageTemplateOutputDir = "../h5/templates/page"
+)
+
+var seoCron *cron.Cron
+
+func init() {
+ seoCron = cron.New(cron.WithSeconds())
+}
+
+var (
+ dirtyMu sync.Mutex
+ // generatedDirtyAt 最近一次 sitemap 生成所对应的内容变更时间
+ generatedDirtyAt time.Time
+)
+
+// Start 启动 SEO 静态资源生成(sitemap / RSS / llms.txt / dhsitemap)定时任务。
+// 页面级搜索引擎推送由 web 进程的 seopush 任务负责,不在这里处理。
+func Start() {
+ generatedDirtyAt = sitemapdirty.LastDirtyAt(context.Background())
+ go generateSitemapFiles(context.Background())
+ go dhsitemap.GenerateDHSitemap()
+ go llmstxt.GenerateLLMSTxt(pageTemplateOutputDir)
+
+ _, _ = seoCron.AddFunc("0 0 */6 * * ?", func() {
+ generateSitemapFiles(context.Background())
+ dhsitemap.GenerateDHSitemap()
+ llmstxt.GenerateLLMSTxt(pageTemplateOutputDir)
+ })
+ // 短剧/视频等发布发生在 web 进程,这里检查它留下的变更标记,有变更才重新生成
+ _, _ = seoCron.AddFunc("0 */2 * * * ?", func() {
+ regenerateSitemapIfDirty()
+ })
+ seoCron.Start()
+ log.Info("seo skd started")
+}
+
+func generateSitemapFiles(ctx context.Context) {
+ config, serr := configser.GetSystemConfig(ctx)
+ if serr != nil || config == nil {
+ log.Error("gen sitemap GetSystemConfig failed", log.Any("err", serr))
+ return
+ }
+ webAddr := strings.TrimSpace(config.WebAddr)
+ if webAddr == "" {
+ log.Error("gen sitemap WebAddr empty")
+ return
+ }
+ generator := sitemap.NewSitemapGenerator(webAddr, filepath.Join(h5OutputDir, "sitemaps"))
+ generator.IndexFile = filepath.Join(h5OutputDir, "sitemap.xml")
+ if err := generator.Generate(); err != nil {
+ log.Error("gen sitemap.xml err", log.E(err))
+ return
+ }
+ rss.GenerateRSS(ctx, pageTemplateOutputDir)
+}
+
+func regenerateSitemapIfDirty() {
+ if !dirtyMu.TryLock() {
+ return
+ }
+ defer dirtyMu.Unlock()
+
+ dirtyAt := sitemapdirty.LastDirtyAt(context.Background())
+ if dirtyAt.IsZero() || !dirtyAt.After(generatedDirtyAt) {
+ return
+ }
+ log.Info("seo: content changed, regenerate sitemap", log.Any("dirtyAt", dirtyAt))
+ generateSitemapFiles(context.Background())
+ dhsitemap.GenerateDHSitemap()
+ llmstxt.GenerateLLMSTxt(pageTemplateOutputDir)
+ generatedDirtyAt = dirtyAt
+}
+
+func Stop() {
+ if seoCron != nil {
+ seoCron.Stop()
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..19ba33e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,130 @@
+module videoweb
+
+go 1.25.0
+
+require (
+ github.com/DeanThompson/ginpprof v0.0.0-20201112072838-007b1e56b2e1
+ github.com/PuerkitoBio/purell v1.2.1
+ github.com/aws/aws-sdk-go v1.34.28
+ github.com/ddliu/go-httpclient v0.6.9
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
+ github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3
+ github.com/elastic/go-elasticsearch/v7 v7.17.10
+ github.com/gin-gonic/gin v1.10.1
+ github.com/go-playground/locales v0.14.1
+ github.com/go-playground/universal-translator v0.18.1
+ github.com/go-playground/validator/v10 v10.27.0
+ github.com/go-redis/redis v6.15.9+incompatible
+ github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
+ github.com/google/uuid v1.6.0
+ github.com/grafov/m3u8 v0.11.1
+ github.com/karrick/godirwalk v1.10.3
+ github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20251124080701-096d68ea7706
+ github.com/mitchellh/mapstructure v1.4.1
+ github.com/moul/http2curl v1.0.0
+ github.com/nyaruka/phonenumbers v1.2.2
+ github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/robfig/cron/v3 v3.0.1
+ github.com/satori/go.uuid v1.2.0
+ github.com/shirou/gopsutil/v3 v3.24.5
+ github.com/shopspring/decimal v1.2.0
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
+ github.com/swaggo/files v1.0.1
+ github.com/swaggo/gin-swagger v1.6.0
+ github.com/swaggo/swag v1.16.6
+ github.com/xuri/excelize/v2 v2.11.0
+ github.com/xxjwxc/gowp v0.0.0-20200603141413-57c3ba7108be
+ go.mongodb.org/mongo-driver v1.17.3
+ go.uber.org/zap v1.16.0
+ golang.org/x/crypto v0.53.0
+ golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
+ google.golang.org/api v0.248.0
+ gopkg.in/go-playground/validator.v9 v9.31.0
+ gopkg.in/yaml.v2 v2.4.0
+)
+
+require (
+ cloud.google.com/go/auth v0.16.5 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
+ github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/bytedance/sonic v1.14.0 // indirect
+ github.com/bytedance/sonic/loader v0.3.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/go-openapi/jsonpointer v0.22.4 // indirect
+ github.com/go-openapi/jsonreference v0.21.4 // indirect
+ github.com/go-openapi/spec v0.22.3 // indirect
+ github.com/go-openapi/swag/conv v0.25.4 // indirect
+ github.com/go-openapi/swag/jsonname v0.25.4 // indirect
+ github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
+ github.com/go-openapi/swag/loading v0.25.4 // indirect
+ github.com/go-openapi/swag/stringutils v0.25.4 // indirect
+ github.com/go-openapi/swag/typeutils v0.25.4 // indirect
+ github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/golang/snappy v1.0.0 // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
+ github.com/googleapis/gax-go/v2 v2.15.0 // indirect
+ github.com/gosimple/slug v1.15.0 // indirect
+ github.com/gosimple/unidecode v1.0.1 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/montanaflynn/stats v0.7.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/richardlehane/mscfb v1.0.7 // indirect
+ github.com/richardlehane/msoleps v1.0.6 // indirect
+ github.com/shoenig/go-m1cpu v0.1.6 // indirect
+ github.com/smartystreets/goconvey v1.8.1 // indirect
+ github.com/technoweenie/multipartstreamer v1.0.1 // indirect
+ github.com/tiendc/go-deepcopy v1.7.2 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/xuri/efp v0.0.1 // indirect
+ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
+ github.com/xxjwxc/public v0.0.0-20200603141144-4001846f9957 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
+ go.opentelemetry.io/otel v1.39.0 // indirect
+ go.opentelemetry.io/otel/metric v1.39.0 // indirect
+ go.opentelemetry.io/otel/trace v1.39.0 // indirect
+ go.uber.org/atomic v1.6.0 // indirect
+ go.uber.org/multierr v1.5.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/arch v0.20.0 // indirect
+ golang.org/x/mod v0.36.0 // indirect
+ golang.org/x/net v0.56.0 // indirect
+ golang.org/x/oauth2 v0.34.0 // indirect
+ golang.org/x/sync v0.21.0 // indirect
+ golang.org/x/sys v0.47.0 // indirect
+ golang.org/x/text v0.38.0 // indirect
+ golang.org/x/tools v0.45.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
+ google.golang.org/grpc v1.79.3 // indirect
+ google.golang.org/protobuf v1.36.10 // indirect
+ gopkg.in/eapache/queue.v1 v1.1.0 // indirect
+ gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..ff5cf4e
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,546 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
+cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DeanThompson/ginpprof v0.0.0-20201112072838-007b1e56b2e1 h1:IIOiH2YkFyyHCImuX7YWlHpc7wHZTQVxZwADs5jfggQ=
+github.com/DeanThompson/ginpprof v0.0.0-20201112072838-007b1e56b2e1/go.mod h1:kMi/fSDAgvjo9TYfYwYeQ2vkyj+VTR/tB6u/Tjh39t0=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
+github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/ant0ine/go-json-rest v3.3.2+incompatible/go.mod h1:q6aCt0GfU6LhpBsnZ/2U+mwe+0XB5WStbmwyoPfc+sk=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk=
+github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
+github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
+github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
+github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/ddliu/go-httpclient v0.6.9 h1:/3hsBVpcgCJwqm1dkVlnAJ9NWuYInbRc+i9FyUXX/LE=
+github.com/ddliu/go-httpclient v0.6.9/go.mod h1:zM9P0OxV4OGGz1pt/ibuj0ooX2SWH9a6MvXZLbT0JMc=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc=
+github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
+github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
+github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
+github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
+github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
+github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
+github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
+github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
+github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
+github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
+github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
+github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
+github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
+github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
+github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
+github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
+github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
+github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
+github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
+github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
+github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
+github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
+github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
+github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
+github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
+github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
+github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
+github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
+github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
+github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
+github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
+github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
+github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jander/golog v0.0.0-20150917071935-954a5be801fc/go.mod h1:uWhWXOR4dpfk9J8fegnMY7sP2GFXxe3PFI9Ps+TRXJs=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kardianos/service v1.0.0/go.mod h1:8CzDhVuCuugtsHyZoTvsOBuvonN/UDBvl0kH+BUxvbo=
+github.com/karrick/godirwalk v1.10.3 h1:lOpSw2vJP0y5eLBW906QwKsUK/fe/QDyoqM5rnnuPDY=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20251124080701-096d68ea7706 h1:yi4Usm9TUC5itUsw6DPqWXZnfSui0Y3qWEoeFlq778s=
+github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20251124080701-096d68ea7706/go.mod h1:+mNMTBuDMdEGhWzoQgc6kBdqeaQpWh5ba8zqmp2MxCU=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
+github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
+github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f/go.mod h1:414R+qZrt4f9S2TO/s6YVQMNAXR2KdwqQ7pW+O4oYzU=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
+github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
+github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
+github.com/nyaruka/phonenumbers v1.2.2 h1:OwVjf7Y4uHoK9VJUrA8ebR0ha2yc6sEYbfrwkq0asCY=
+github.com/nyaruka/phonenumbers v1.2.2/go.mod h1:wzk2qq7qwsaBKrfbkWKdgHYOOH+QFTesSpIq53ELw8M=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olivere/elastic v6.2.31+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
+github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
+github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
+github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
+github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
+github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
+github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
+github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
+github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
+github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
+github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
+github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
+github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
+github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
+github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
+github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
+github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
+github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88=
+github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak=
+github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
+github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/xxjwxc/gowp v0.0.0-20200603130651-4d7368b0e285/go.mod h1:yJ/fY5BorWARfDDsxBU/MyQTHc5MVyNcqBQQYD6MN0k=
+github.com/xxjwxc/gowp v0.0.0-20200603141413-57c3ba7108be h1:v4Ws2Pd0HNxegMWiZTgaSBfeLtfyFP/eWc50o2CFFX8=
+github.com/xxjwxc/gowp v0.0.0-20200603141413-57c3ba7108be/go.mod h1:mstQ3GY2ZYdP9fCUJJmsq5hwhP0LKoR4ZVTq0XaxK0U=
+github.com/xxjwxc/public v0.0.0-20200603115833-341beff27850/go.mod h1:fp3M+FEQrCgWD1fZ/PLwZkCTglf086OEhC9LcydAUnc=
+github.com/xxjwxc/public v0.0.0-20200603141144-4001846f9957 h1:1JDmDFVQFpty205Kh9lxBAjjn9rG1eTYtdF/Q0dTh5w=
+github.com/xxjwxc/public v0.0.0-20200603141144-4001846f9957/go.mod h1:0BFWVHqt7nKW8MtIx7R7bOkoGQFFnKsaJeeVbkzY88E=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
+go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
+go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
+golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
+golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
+golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
+google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
+google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
+google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
+google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/eapache/queue.v1 v1.1.0 h1:EldqoJEGtXYiVCMRo2C9mePO2UUGnYn2+qLmlQSqPdc=
+gopkg.in/eapache/queue.v1 v1.1.0/go.mod h1:wNtmx1/O7kZSR9zNT1TTOJ7GLpm3Vn7srzlfylFbQwU=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
+gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
+gopkg.in/go-with/wxpay.v1 v1.3.0/go.mod h1:12lWy92n19pAUSSE3BrOiEZbWRkl+9tneOd/aU/LU6g=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
diff --git a/make.sh b/make.sh
new file mode 100755
index 0000000..9d2ef5a
--- /dev/null
+++ b/make.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+export GOPATH=$(cd $(dirname $0)/../;pwd)
+export PATH=${PATH}:${GOPATH}/bin
+
+curDir=$(cd $(dirname $0);pwd)
+cd ${curDir}
+
+which swag
+if [ $? -ne 0 ]; then
+ echo "未发现swag,拉取最新swag"
+ go get github.com/swaggo/swag/cmd/swag
+fi
+
+prepare=(
+ "github.com/swaggo/gin-swagger"
+ "github.com/swaggo/gin-swagger/swaggerFiles"
+)
+for goget in ${prepare[@]};do
+ go get ${goget}
+ if [ $? -ne 0 ]; then
+ echo "拉取${goget}失败,终止构建"
+ exit 1
+ fi
+done
+
+echo "package main;func main() {}" > main.go
+
+swag init --exclude web --output app/docs
+if [ $? -ne 0 ]; then
+ echo "执行swag init失败,终止构建"
+ rm main.go
+ exit 1
+fi
+
+swag init --exclude app --output web/docs
+if [ $? -ne 0 ]; then
+ echo "执行swag init失败,终止构建"
+ rm main.go
+ exit 1
+fi
+
+rm main.go
+
diff --git a/project.md b/project.md
new file mode 100644
index 0000000..721d656
--- /dev/null
+++ b/project.md
@@ -0,0 +1,86 @@
+---
+项目名称: 海角社区
+一级部门: 北斗
+二级部门: 瑞升
+
+DMP编码:
+
+项目类型: 服务
+ # 可选值:
+ # H5 / APP / PC / 服务 / 组件 / 工具 / AI / 中间件
+
+开发语言: GO
+ # 可选值:
+ # GO / Java / PHP / Node
+
+项目负责人: DN1425 # 员工编号, 多个用逗号隔开,这里的负责是这套代码的负责人
+
+运维负责人: DN9460, DN9651, DN4792 # 负责部署、监控、K8S、发布、证书等
+
+产品负责人: YY1166 # 项目产品经理
+
+
+项目成员: DN2185, DN8431
+
+项目状态: 维护中
+ # 可选值:
+ # 开发中 / 维护中 / 已停止 / 已归档
+
+项目级别: 中
+ # 可选值:
+ # 高 / 中 / 低 , 北斗体系 s和a为 高 b为中 c d为低
+
+核心分支: main
+ # 即部署时候使用的分支,可选值:
+ # master / main / dev / release
+
+代码仓库:
+ ssh://git@bd-git-sh.jsyyds.com:8082/ruisheng/jha-1021-91jhs/backend/jha-1046-hjsq-server.git
+
+最后审计人:
+
+最后审计时间:
+
+告警群:
+
+备注:
+
+---
+
+# 1. 项目说明
+
+- 海角社区
+- app 包含前端API
+- web 包含管理后端API
+
+
+# 2. 基础设施与中间件
+
+- MongoDB 主从
+- Redis 单节点
+- Elasticsearch 搜索(v7)
+
+
+# 3. 运维部署
+
+- Shell脚本部署(scp + supervisorctl)
+- Telegram Bot 部署通知
+
+
+# 4. 风险说明
+
+- Redis 热Key风险
+
+
+# 5. 历史事故(可选)
+
+-
+
+
+# 6. 交接说明(可选)
+
+- appid 代码固定:1046
+- ES的索引KEY:webhjsq_video_index_v1 等
+- /wbapi/hjsq api路径及名称
+- ReportAppId 埋点APPID需要注意
+- seoPushEnabled SEO推送开关(刚开始建项目关闭,等上线后看情况开启)
diff --git a/pub/adcenter/adcenter.go b/pub/adcenter/adcenter.go
new file mode 100644
index 0000000..155586f
--- /dev/null
+++ b/pub/adcenter/adcenter.go
@@ -0,0 +1,298 @@
+package adcenter
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "path"
+ "sort"
+ "strings"
+ "time"
+ "videoweb/pub/common/crypt"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/httputil"
+ "videoweb/pub/common/log"
+ "videoweb/pub/redis"
+)
+
+/*
+测试:
+部门CODE:1991129769626804224
+商户CODE:1991129808640122880
+应用CODE:jh_appid01
+客户CODE:1991129948159451136
+*/
+
+type JtAdvertiseReq struct {
+ MerchantCode string `json:"merchantCode"` // 商户 code,必填
+ AppCode string `json:"appCode"` // 应用 code,必填
+ AdStatus int `json:"adStatus"` // 广告状态 1-上架,0-下架,2-未开始,3-已过期,4-即将到期 默认查询全部
+ //AdvertiseLocationCode string `json:"advertiseLocationCode,omitempty"` // 广告位唯一标识,选填
+ //AdvertiseCode string `json:"advertiseCode,omitempty"` // 广告唯一标识,选填
+ //PageNo int `json:"pageNo,omitempty"` // 页码,全量查询不需要传,选填
+ //PageSize int `json:"pageSize,omitempty"` // 每页条数,全量查询不需要传,选填
+}
+
+type JtAdvertiseRes struct {
+ Code int `json:"code"` // 返回状态码
+ Msg string `json:"msg"` // 返回消息
+ Data string `json:"data"` // 广告信息 - 加密
+}
+
+// AdSlot 广告位信息
+type AdSlot struct {
+ DeptCode string `json:"deptCode"` // 部门 code
+ MerchantCode string `json:"merchantCode"` // 商户 code
+ AppCode string `json:"appCode"` // 应用 code
+ LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
+ AdvertiseLocationCode string `json:"advertiseLocationCode"` // 广告位标识
+ AdvertiseLocationName string `json:"advertiseLocationName"` // 广告位名称
+ DisplayMode int `json:"displayMode"` // 展示模式:0 → 多个轮播, 1 → 单体, 2 → 三分屏, 3 → 四分屏, 4 → 小图标, 5 → 大图标
+ AdvertiseHeight int `json:"advertiseHeight"` // 高
+ AdvertiseWidth int `json:"advertiseWidth"` // 宽
+ MaterialType int `json:"materialType"` // 素材类型(视频/图片):1 → 视频, 2 → 图片, 3 → 视频、图片
+ AdLimit int `json:"adLimit"` // 广告限制数量
+ AdDetailInfoList []AdDetailInfo `json:"adDetailInfoList"` // 广告详情信息列表
+}
+
+// AdDetailInfo 广告详情信息
+type AdDetailInfo struct {
+ AdvertiseCode string `json:"advertiseCode"` // 广告业务code
+ CustomerCode string `json:"customerCode"` // 客户code
+ AdvertiseName string `json:"advertiseName"` // 广告名称
+ AdvertiseUrl string `json:"advertiseUrl"` // 广告链接
+ AdvertiseIcon string `json:"advertiseIcon"` // 广告图片地址
+ AdvertiseType int `json:"advertiseType"` // 广告类型:1 → 播放器, 2 → 药台, 3 → 炮台, 4 → 黄游, 5 → 直播, 6 → BC
+ AdMode string `json:"adMode"` // 广告模式
+ AppCode string `json:"appCode"` // 应用 code
+ LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
+ AdvertiseDesc string `json:"advertiseDesc"` // 广告描述
+ PageType string `json:"pageType"` // 内部路由页面类型:如 rechange_page → 会员充值页, gold_rechange_page → 金币充值页, invite_page → 邀请分享页
+ StartTimeStamp int64 `json:"startTimeStamp"` // 广告开始时间 时间戳
+ EndTimeStamp int64 `json:"endTimeStamp"` // 广告结束时间 时间戳
+ Sort int `json:"sort"` // 排序
+ AdExtData string `json:"adExtData"` // 扩展字段1 = CoverImgSize
+}
+type AdCacheInfo struct {
+ AdvertiseCode string `json:"advertiseCode"` // 广告业务code
+ CustomerCode string `json:"customerCode"` // 客户code
+ AdvertiseName string `json:"advertiseName"` // 广告名称
+ AdvertiseUrl string `json:"advertiseUrl"` // 广告链接
+ AdvertiseIcon string `json:"advertiseIcon"` // 广告图片地址
+ AdvertiseType int `json:"advertiseType"` // 广告类型:1 → 播放器, 2 → 药台, 3 → 炮台, 4 → 黄游, 5 → 直播, 6 → BC
+ AdMode string `json:"adMode"` // 广告模式
+ AppCode string `json:"appCode"` // 应用 code
+ LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
+ AdvertiseDesc string `json:"advertiseDesc"` // 广告描述
+ PageType string `json:"pageType"` // 内部路由页面类型:如 rechange_page → 会员充值页, gold_rechange_page → 金币充值页, invite_page → 邀请分享页
+ StartTimeStamp int64 `json:"startTimeStamp"` // 广告开始时间 时间戳
+ EndTimeStamp int64 `json:"endTimeStamp"` // 广告结束时间 时间戳
+ Sort int `json:"sort"` // 排序
+ AdExtData string `json:"adExtData"` // 扩展字段1 = CoverImgSize
+ DeptCode string `json:"deptCode"` // 部门 code
+ MerchantCode string `json:"merchantCode"` // 商户 code
+ AdvertiseLocationCode string `json:"advertiseLocationCode"` // 广告位标识
+ AdvertiseLocationName string `json:"advertiseLocationName"` // 广告位名称
+ DisplayMode int `json:"displayMode"` // 展示模式:0 → 多个轮播, 1 → 单体, 2 → 三分屏, 3 → 四分屏, 4 → 小图标, 5 → 大图标
+ AdvertiseHeight int `json:"advertiseHeight"` // 高
+ AdvertiseWidth int `json:"advertiseWidth"` // 宽
+ MaterialType int `json:"materialType"` // 素材类型(视频/图片):1 → 视频, 2 → 图片, 3 → 视频、图片
+ AdLimit int `json:"adLimit"` // 广告限制数量
+}
+
+type Extra struct {
+ ImgType int64 `json:"imgType"` // 图片展示类型 0:应用图标 1.小图标 2.大图标
+ DownloadCount int64 `json:"downloadCount"`
+}
+
+func (ad AdDetailInfo) GetExtraData() (res Extra) {
+ _ = json.Unmarshal([]byte(ad.AdExtData), &res)
+ return
+}
+
+func (ad AdDetailInfo) GetRealLink() string {
+ return strings.ReplaceAll(ad.AdvertiseUrl, "inner://yinseinner/", "yinseinner://")
+}
+
+func (ad AdDetailInfo) GetCoverLsj() string {
+ if ad.AdvertiseIcon == "" {
+ return ""
+ }
+
+ return path.Join("laosiji", ad.AdvertiseIcon)
+}
+
+func (ad AdDetailInfo) GetAdvertiseType() int {
+ // 内部顺序:1 应用 2 游戏 3 炮台 4 棋牌 5 直播
+ // 外部顺序:1:应用 2:药台、3:炮台、4:游戏、5:直播、6:棋牌
+ if ad.AdvertiseType == 4 {
+ return 2
+ }
+ if ad.AdvertiseType == 6 {
+ return 4
+ }
+
+ return ad.AdvertiseType
+}
+
+// JtAdvertiseThreeServer 集团广告中心
+func JtAdvertiseThreeServer(apiDomain, merchantCode, appCode, aesKey string) ([]AdSlot, error) {
+ var foreverCacheKey = fmt.Sprintf("jtAdForever-%s-%s", merchantCode, appCode)
+ redisKey := fmt.Sprintf("jtAd-%s-%s", merchantCode, appCode)
+ str, err := redis.Get(redisKey)
+ if err != nil {
+ log.Error(fmt.Sprintf("JtAdvertiseThreeServer 缓存获取广告列表信息异常:%v", err))
+ }
+ var resp []AdSlot
+ if str != nil {
+ if err = json.Unmarshal([]byte(*str), &resp); err == nil {
+ return resp, nil
+ }
+ log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析缓存数据异常:%v", err))
+ }
+ req := JtAdvertiseReq{
+ MerchantCode: merchantCode,
+ AppCode: appCode,
+ AdStatus: 1,
+ }
+
+ bodyStr, _ := json.Marshal(req)
+ headers := map[string]string{"Content-Type": "application/json"}
+ serverResp, err := fetchJtAdvertiseWithFallback(apiDomain, headers, bodyStr)
+ if err != nil {
+ return getFromLastTime(foreverCacheKey, err)
+ }
+
+ adsData, err := crypt.AdDecrypt(serverResp.Data, aesKey)
+ if err != nil {
+ log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解密广告数据异常:%v", err))
+ return nil, errors.New("response data err")
+ }
+ err = json.Unmarshal([]byte(adsData), &resp)
+ if err != nil {
+ log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析 JSON 失败:%v", err))
+ return nil, errors.New("data format error")
+ }
+
+ // 对广告进行升序排序
+ for i := range resp {
+ sort.Slice(resp[i].AdDetailInfoList, func(j, k int) bool {
+ return resp[i].AdDetailInfoList[j].Sort < resp[i].AdDetailInfoList[k].Sort
+ })
+ }
+
+ // 将最后一次请求成功的数据更新到二级缓存
+ godef.Go(func() {
+ setAdsCache(foreverCacheKey, resp)
+ })
+ // 写入缓存
+ godef.Go(func() {
+ data, err := json.Marshal(&resp)
+ if err != nil {
+ log.Error(fmt.Sprintf("JtAdvertiseThreeServer json序列化缓存数据异常:%v", err))
+ return
+ }
+ if err = redis.Set(redisKey, data, time.Minute); err != nil {
+ log.Error(fmt.Sprintf("JtAdvertiseThreeServer 保存缓存数据异常:%v", err))
+ }
+ })
+
+ return resp, nil
+}
+
+var jtAdvertiseFallbackDomains = []string{
+ "https://apiad01.cc",
+ "https://apiad02.cc",
+ "https://apiad03.cc",
+ "https://awsapiad01.cc",
+ "https://d3o2peztwy6jno.cloudfront.net",
+}
+
+func jtAdvertiseRequestDomains(apiDomain string) []string {
+ seen := make(map[string]struct{}, 1+len(jtAdvertiseFallbackDomains))
+ domains := make([]string, 0, 1+len(jtAdvertiseFallbackDomains))
+ add := func(domain string) {
+ domain = strings.TrimRight(strings.TrimSpace(domain), "/")
+ if domain == "" {
+ return
+ }
+ if _, ok := seen[domain]; ok {
+ return
+ }
+ seen[domain] = struct{}{}
+ domains = append(domains, domain)
+ }
+ add(apiDomain)
+ for _, domain := range jtAdvertiseFallbackDomains {
+ add(domain)
+ }
+ return domains
+}
+
+func fetchJtAdvertiseWithFallback(apiDomain string, headers map[string]string, body []byte) (*JtAdvertiseRes, error) {
+ var lastErr error
+ for _, domain := range jtAdvertiseRequestDomains(apiDomain) {
+ resp, err := fetchJtAdvertise(domain, headers, body)
+ if err == nil {
+ return resp, nil
+ }
+ lastErr = err
+ log.Error("JtAdvertiseThreeServer 请求异常,尝试下一个域名", log.Any("domain", domain), log.E(err))
+ }
+ if lastErr == nil {
+ lastErr = errors.New("adcenter api domain empty")
+ }
+ return nil, lastErr
+}
+
+func fetchJtAdvertise(apiDomain string, headers map[string]string, body []byte) (*JtAdvertiseRes, error) {
+ url := apiDomain + "/openapi/getAdvertiseList"
+ var serverResp *JtAdvertiseRes
+ code, err := httputil.Client().POSTJsonWithJResp(&serverResp, url, headers, body)
+ if err != nil {
+ return nil, err
+ }
+ if code != http.StatusOK {
+ return nil, fmt.Errorf("response status %d", code)
+ }
+ if serverResp == nil {
+ return nil, errors.New("empty response")
+ }
+ if serverResp.Code != 0 {
+ return nil, fmt.Errorf("response code %d: %s", serverResp.Code, serverResp.Msg)
+ }
+ return serverResp, nil
+}
+
+// getFromLastTime 返回最后一次请求的广告数据
+func getFromLastTime(foreverCacheKey string, e error) ([]AdSlot, error) {
+ str, err := redis.Get(foreverCacheKey)
+ if err != nil {
+ log.Error(fmt.Sprintf("getFromLastTime 缓存获取广告列表信息异常:%v", err))
+ return nil, e
+ }
+
+ var resp []AdSlot
+ if str != nil {
+ if err = json.Unmarshal([]byte(*str), &resp); err == nil {
+ return resp, nil
+ }
+ log.Error(fmt.Sprintf("getFromLastTime 解析缓存数据异常:%v", err))
+ }
+
+ return nil, e
+}
+
+// setAdsCache 更新最后一次请求的广告数据
+func setAdsCache(foreverCacheKey string, resp []AdSlot) {
+ data, err := json.Marshal(&resp)
+ if err != nil {
+ log.Error(fmt.Sprintf("setAdsCache json序列化缓存数据异常:%v", err))
+ return
+ }
+
+ if err := redis.Set(foreverCacheKey, data, time.Hour*72); err != nil {
+ log.Error(fmt.Sprintf("setAdsCache 保存缓存数据异常:%v", err))
+ }
+}
diff --git a/pub/common/comm/comm.go b/pub/common/comm/comm.go
new file mode 100644
index 0000000..f5e3c65
--- /dev/null
+++ b/pub/common/comm/comm.go
@@ -0,0 +1,138 @@
+package comm
+
+import (
+ "encoding/json"
+ "sync"
+ "time"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/lock"
+ "videoweb/pub/common/log"
+
+ "github.com/go-redis/redis"
+ "go.uber.org/zap"
+)
+
+type MessageType int64
+
+const (
+ MessageTypeError MessageType = iota // 心跳消息
+ MessageTypeAlive
+ MessageTypeMediaAdd // 添加一个媒体
+ MessageTypeMediaDel // 删除一个媒体
+ _ // 已移除帖子新增消息,保留原数值
+ _ // 已移除帖子删除消息,保留原数值
+)
+
+type message struct {
+ Type MessageType
+ Payload []byte
+}
+
+type OnReceive func(data []byte)
+
+type Communicate struct {
+ sendChannel string // 发送消息的频道
+ recvChannel string // 接收消息的频道
+ client *redis.Client // Redis的客户端
+ lock sync.RWMutex
+ callbacks map[MessageType][]OnReceive
+ publish *lock.Queue[*message]
+}
+
+func NewCommunicate(client *redis.Client, sendChannel string, recvChannel string) *Communicate {
+ return &Communicate{
+ sendChannel: sendChannel,
+ recvChannel: recvChannel,
+ client: client,
+ callbacks: make(map[MessageType][]OnReceive),
+ publish: lock.NewQueue[*message](),
+ }
+}
+
+func (c *Communicate) RegisterReceiver(mType MessageType, receive OnReceive) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ c.callbacks[mType] = append(c.callbacks[mType], receive)
+}
+
+func (c *Communicate) findEvent(mType MessageType) []OnReceive {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ return c.callbacks[mType]
+}
+
+func (c *Communicate) dispatchMessage(mType MessageType, payload []byte) {
+ events := c.findEvent(mType)
+ for _, event := range events {
+ event(payload)
+ }
+}
+
+func (c *Communicate) handleRecv(data *redis.Message) {
+ if data.Channel == "error" {
+ log.Error("recv from channel %s failed", zap.Any("error", data.Payload))
+ return
+ }
+ if data.Channel == c.recvChannel {
+ msg := &message{}
+ err := json.Unmarshal([]byte(data.Payload), msg)
+ if err != nil {
+ log.Error("recv from channel %s failed", zap.Any("error", err))
+ }
+ c.dispatchMessage(msg.Type, msg.Payload)
+ }
+}
+
+func (c *Communicate) Send(mType MessageType, payload []byte) error {
+ msg := &message{
+ Type: mType,
+ Payload: payload,
+ }
+ c.publish.Enqueue(msg)
+ return nil
+}
+
+func (c *Communicate) Start() {
+ // 启动接收线程
+ godef.Go(func() {
+ sub := c.client.Subscribe(c.recvChannel)
+ defer sub.Close()
+ // 循环接收消息
+ for {
+ msg, err := sub.ReceiveMessage()
+ if err != nil {
+ msg = &redis.Message{
+ Channel: "error",
+ Payload: err.Error(),
+ }
+ c.handleRecv(msg)
+ time.Sleep(time.Second)
+ } else {
+ c.handleRecv(msg)
+ }
+ }
+ })
+ // 启动发送线程
+ godef.Go(func() {
+ for {
+ time.Sleep(time.Second)
+ for !c.publish.IsEmpty() {
+ msg, ok := c.publish.Dequeue()
+ if !ok {
+ break
+ }
+ data, err := json.Marshal(msg)
+ if err != nil {
+ log.Error("Communicate msg marshal failed", zap.Any("error", err))
+ break
+ }
+ err = c.client.Publish(c.sendChannel, data).Err()
+ if err != nil {
+ c.publish.Enqueue(msg) // 再写回队列
+ log.Error("Communicate send msg failed", zap.Any("error", err))
+ break
+ }
+ }
+ }
+ })
+}
diff --git a/pub/common/common.go b/pub/common/common.go
new file mode 100644
index 0000000..f799130
--- /dev/null
+++ b/pub/common/common.go
@@ -0,0 +1,184 @@
+package common
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+var (
+ cryptSecret string
+ env consts.RunMod
+ responseDataTransform func(*gin.Context, interface{}) interface{}
+ //routineCount int64
+)
+
+// SetResponseDataTransform 设置当前服务的成功响应数据转换函数
+func SetResponseDataTransform(transform func(*gin.Context, interface{}) interface{}) {
+ responseDataTransform = transform
+}
+
+// InitGinSecret InitResponseParam
+func InitGinSecret(secret string, env_ consts.RunMod) {
+ cryptSecret = secret
+ env = env_
+}
+
+// JSONWriterCache 返回数据并标记需要写入缓存
+func JSONWriterCache(c *gin.Context, codeOrErr srvdef.ErrInfoer, data interface{}) {
+ code := codeOrErr.GetCode()
+ if code == srvdef.Success && responseDataTransform != nil {
+ data = responseDataTransform(c, data)
+ }
+ tip := codeOrErr.GetTip()
+ msg := codeOrErr.GetMsg()
+ var hash bool
+ if code != srvdef.Success { //发生错误,记录日志
+ var version, sysType, devType string
+ id := GetUserId(c)
+ ua, _ := GetUA(c)
+ if ua.Ver != "" {
+ version = ua.Ver
+ }
+ if ua.SysType != "" {
+ sysType = string(ua.SysType)
+ }
+ if devType != "" {
+ devType = ua.DevType
+ }
+ log.Warn("Error:",
+ log.Any("UID", strconv.FormatInt(id, 10)),
+ log.Any("IP", c.ClientIP()),
+ log.Any("Version", version),
+ log.Any(consts.CtxSysType, sysType),
+ log.Any("DevType", devType),
+ log.Any("Router", c.Request.RequestURI),
+ log.Any("PID", os.Getpid()),
+ log.Any("Error", data),
+ log.Any("Code", code))
+ }
+ c.Header("writer-cache", "HIT")
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "hash": hash,
+ "msg": msg,
+ "tip": tip,
+ "data": data,
+ "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
+ })
+}
+
+// ServerJSON 返回数据并处理多语言
+func ServerJSON(c *gin.Context, codeOrErr srvdef.ErrInfoer, data interface{}) {
+ code := codeOrErr.GetCode()
+ if code == srvdef.Success && responseDataTransform != nil {
+ data = responseDataTransform(c, data)
+ }
+ tip := codeOrErr.GetTip()
+ msg := codeOrErr.GetMsg()
+ var hash bool
+ if code != srvdef.Success { //发生错误,记录日志
+ var version, sysType, devType string
+ id := GetUserId(c)
+ ua, _ := GetUA(c)
+ if ua.Ver != "" {
+ version = ua.Ver
+ }
+ if ua.SysType != "" {
+ sysType = string(ua.SysType)
+ }
+ if devType != "" {
+ devType = ua.DevType
+ }
+ log.Warn("Error:",
+ log.Any("UID", strconv.FormatInt(id, 10)),
+ log.Any("IP", c.ClientIP()),
+ log.Any("Version", version),
+ log.Any(consts.CtxSysType, sysType),
+ log.Any("DevType", devType),
+ log.Any("Router", c.Request.RequestURI),
+ log.Any("PID", os.Getpid()),
+ log.Any("Error", data),
+ log.Any("Code", code))
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "hash": hash,
+ "msg": msg,
+ "tip": tip,
+ "data": data,
+ "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
+ })
+}
+
+// GetUID 获取用户uid
+// oauth.Auth执行后uid有效
+func GetUID(ctx *gin.Context) (uid uint64, err error) {
+ val, exists := ctx.Get(consts.CtxUserID)
+ if !exists {
+ err = errors.New("userId not exist")
+ return
+ }
+ uid, ok := val.(uint64)
+ if !ok {
+ return 0, errors.New("userId type error")
+ }
+ return
+}
+
+func GetUserId(ctx *gin.Context) int64 {
+ return ctx.GetInt64(consts.CtxUserID)
+}
+
+// GetUA
+func GetUA(ctx *gin.Context) (u useragent.UA, err error) {
+ val, exists := ctx.Get(consts.CtxUA)
+ if !exists {
+ err = errors.New("user-agent not exists")
+ return
+ }
+ u, ok := val.(useragent.UA)
+ if !ok {
+ err = errors.New("user-agent type error")
+ return
+ }
+ return u, nil
+}
+
+// GetIP 获取真实IP
+func GetIP(ctx *gin.Context) string {
+ relIP, exists := ctx.Get(consts.CtxIP)
+ if !exists {
+ if relIP == "" {
+ relIP = ctx.ClientIP()
+ }
+ }
+ v, ok := relIP.(string)
+ if !ok {
+ return ""
+ }
+ log.Info(fmt.Sprintf("[IP-ROUTER] %s,PID %d", ctx.GetHeader("X-Forwarded-For"), os.Getpid()))
+ return v
+}
+
+func IsNilOrEmpty(in interface{}) bool {
+ if in == nil || in == "" {
+ return true
+ }
+ return false
+}
+
+func AlphabeticColl() []string {
+ return []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
+}
diff --git a/pub/common/convUtil.go b/pub/common/convUtil.go
new file mode 100644
index 0000000..0a99f82
--- /dev/null
+++ b/pub/common/convUtil.go
@@ -0,0 +1,135 @@
+package common
+
+import (
+ "encoding/json"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ID = primitive.ObjectID
+
+// ToJsonM 将struct转换为 Json Map
+func ToJsonM(obj interface{}) (map[string]interface{}, error) {
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+func ToBsonM(s interface{}) (bson.M, error) {
+ data, err := bson.Marshal(s)
+ if err != nil {
+ return nil, err
+ }
+ m := make(bson.M)
+ if err := bson.Unmarshal(data, &m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func MapToJsonString(m map[string]string) (string, error) {
+ if data, err := json.Marshal(m); err != nil {
+ return "", err
+ } else {
+ return string(data), nil
+ }
+}
+
+// MapToStruct s is struct ptr
+func MapToStruct(s interface{}, m map[string]interface{}) error {
+ bsonBytes, err := bson.Marshal(m)
+ if err != nil {
+ return err
+ }
+ return bson.Unmarshal(bsonBytes, s)
+}
+
+// JSONStruct2Map 将struct转换为Map
+func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+// Map2JSONStruct 将map转换为Json struct
+func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
+ j, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(j, v); err != nil {
+ return err
+ }
+ return err
+}
+
+func IDArray(ids []string) ([]ID, error) {
+ var idArray []ID
+ for _, id := range ids {
+ _id, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ return nil, err
+ }
+ idArray = append(idArray, _id)
+ }
+
+ return idArray, nil
+}
+
+func ObjectIDs2String(obj []primitive.ObjectID) []string {
+ arr := []string{}
+ if len(obj) == 0 {
+ return arr
+ }
+ for _, o := range obj {
+ arr = append(arr, o.Hex())
+ }
+ return arr
+}
+
+func String2ObjectID(obj []string) []primitive.ObjectID {
+ arr := []primitive.ObjectID{}
+ for _, o := range obj {
+ oid, err := primitive.ObjectIDFromHex(o)
+ if err != nil {
+ continue
+ }
+ arr = append(arr, oid)
+ }
+ return arr
+}
+
+// PercentOf - calculate what percent [number1] is of [number2].
+// ex. 300 is 12.5% of 2400
+func PercentOf(part int, total int) float64 {
+ return (float64(part) * float64(100)) / float64(total)
+}
+
+func MergeMap(src, dest map[string]interface{}) map[string]interface{} {
+ if len(src) == 0 {
+ return dest
+ }
+ if len(dest) == 0 {
+ return src
+ }
+ if len(src) == 0 && len(dest) == 0 {
+ return make(map[string]interface{})
+ }
+ for k, v := range dest {
+ src[k] = v
+ }
+ return src
+}
diff --git a/pub/common/cronmgr/crommgr.go b/pub/common/cronmgr/crommgr.go
new file mode 100644
index 0000000..2494c63
--- /dev/null
+++ b/pub/common/cronmgr/crommgr.go
@@ -0,0 +1,119 @@
+package cronmgr
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "videoweb/pub/common/log"
+
+ "github.com/robfig/cron/v3"
+)
+
+var c *cron.Cron
+
+// runIngs 正在运行中的任务
+var runIngs map[*context.Context]struct{}
+var r_lock sync.Mutex
+
+// RegisterCron 注册一个定时任务
+func RegisterCron(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
+ if len(spec) == 0 || cmd == nil {
+ return -1, errors.New("spec or cmd is nil")
+ }
+ return registerCron(spec, func() {
+ defer func() {
+ if err := recover(); err != nil {
+ if errfunc != nil {
+ errfunc(err)
+ }
+ log.Errorf("cron task error.%v", err)
+ }
+ }()
+ cmd()
+ })
+}
+
+// RegisterCronAsyncOne 注册一个协程执行的定时任务 限制了cmd同时只有一个协程在执行
+func RegisterCronAsyncOne(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
+ if len(spec) == 0 || cmd == nil {
+ return -1, errors.New("spec or cmd is nil")
+ }
+ ctx := context.Background()
+ return registerCron(spec, func() {
+ go func(c *context.Context) {
+ defer func() {
+ if err := recover(); err != nil {
+ if errfunc != nil {
+ errfunc(err)
+ }
+ log.Errorf("cron task error.%v", err)
+ }
+ }()
+ if taskRunIng(c) {
+ cmd()
+ taskRunEnd(c)
+ } else {
+ log.Warnf("Limits cmd to only one coroutine executing at the same time")
+ }
+ }(&ctx)
+ })
+}
+
+// RegisterCronAsync 注册一个协程执行的定时任务
+func RegisterCronAsync(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
+ if len(spec) == 0 || cmd == nil {
+ return -1, errors.New("spec or cmd is nil")
+ }
+ return registerCron(spec, func() {
+ go func() {
+ defer func() {
+ if err := recover(); err != nil {
+ if errfunc != nil {
+ errfunc(err)
+ }
+ log.Errorf("cron task error.%v", err)
+ }
+ }()
+ cmd()
+ }()
+ })
+}
+func registerCron(spec string, cmd func()) (cron.EntryID, error) {
+ if c == nil {
+ c = cron.New(cron.WithSeconds())
+ runIngs = make(map[*context.Context]struct{})
+ }
+ id, err := c.AddFunc(spec, cmd)
+ if err != nil {
+ log.Warnf("add cron [%s] fail", spec)
+ return -1, err
+ }
+ c.Start()
+ log.Infof("add cron [%d] spec:[%s].", id, spec)
+ return id, nil
+}
+
+// UnregisterCron 注销一个定时任务
+func UnregisterCron(id cron.EntryID) bool {
+ if id > 0 {
+ c.Remove(id)
+ log.Infof("remove cron id:[%d]", id)
+ }
+ log.Infof("remove cron [%d] not found", id)
+ return false
+}
+
+func taskRunIng(ctx *context.Context) bool {
+ r_lock.Lock()
+ defer r_lock.Unlock()
+ if _, ok := runIngs[ctx]; ok {
+ return false
+ }
+ runIngs[ctx] = struct{}{}
+ return true
+}
+func taskRunEnd(ctx *context.Context) {
+ r_lock.Lock()
+ defer r_lock.Unlock()
+ delete(runIngs, ctx)
+}
diff --git a/pub/common/cronx/cron.go b/pub/common/cronx/cron.go
new file mode 100644
index 0000000..5228ccf
--- /dev/null
+++ b/pub/common/cronx/cron.go
@@ -0,0 +1,148 @@
+package cronx
+
+import (
+ "context"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+
+ "github.com/robfig/cron/v3"
+ "go.uber.org/zap"
+)
+
+type CronJob interface {
+ Run() error
+ Desc() string
+ Name() string
+}
+
+type CronJobAndSpec struct {
+ CronJob CronJob
+ Spec string
+}
+
+type BaseJob struct {
+ ctx context.Context
+ spec string
+ name string
+ desc string
+ l *zap.SugaredLogger
+}
+
+type cronFunc struct {
+ *BaseJob
+ fn func(ctx context.Context, l *zap.SugaredLogger) error
+}
+
+func addError(fn func(ctx context.Context, l *zap.SugaredLogger)) func(context.Context, *zap.SugaredLogger) error {
+ return func(ctx context.Context, l *zap.SugaredLogger) error {
+ l.Info("start")
+ defer l.Info("end")
+ fn(ctx, l)
+ return nil
+ }
+}
+
+func (f cronFunc) Run() error { return f.fn(f.ctx, f.l) }
+func NewCronFunc(ctx context.Context, name, desc string, fn func(ctx context.Context, l *zap.SugaredLogger)) cronFunc {
+ return cronFunc{
+ BaseJob: NewBaseJob(ctx, name, desc),
+ fn: addError(fn),
+ }
+}
+
+func NewCronFuncWithError(ctx context.Context, name, desc string, fn func(ctx context.Context, l *zap.SugaredLogger) error) cronFunc {
+ return cronFunc{
+ BaseJob: NewBaseJob(ctx, name, desc),
+ fn: fn,
+ }
+}
+
+func NewBaseJob(ctx context.Context, name, desc string) *BaseJob {
+ return &BaseJob{
+ ctx: ctx,
+ name: name,
+ desc: desc,
+ l: log.ZapLogS.Named(name),
+ }
+}
+
+func (j *BaseJob) Name() string { return j.name }
+func (j *BaseJob) Desc() string { return j.desc }
+func (j *BaseJob) L() *zap.SugaredLogger { return j.l }
+func (j *BaseJob) Context() context.Context { return j.ctx }
+
+type CronLogger struct {
+ l *zap.SugaredLogger
+}
+
+func NewCronLogger() *CronLogger {
+ return &CronLogger{
+ l: log.ZapLogS.Named("cron"),
+ }
+}
+
+func (l *CronLogger) Info(msg string, kvs ...interface{}) {
+ l.l.Infow(msg, kvs)
+}
+func (l *CronLogger) Error(err error, msg string, kvs ...interface{}) {
+ l.l.Errorw(err.Error()+msg, kvs)
+}
+
+func StartJobs(jobs ...CronJobAndSpec) (closer func()) {
+ var once sync.Once
+ l := NewCronLogger()
+ c := cron.New(cron.WithSeconds(), cron.WithLocation(time.Local), cron.WithChain(cron.Recover(l)))
+ for i := range jobs {
+ j := jobs[i].CronJob
+ spec := jobs[i].Spec
+ name := j.Name()
+ _, err := c.AddFunc(spec, func() {
+ defer log.Infof("%s end", name)
+ log.Infof("%s start", name)
+ if err := j.Run(); err != nil {
+ log.Errorf("%s run failed: %s", name, err.Error())
+ }
+ })
+ if err != nil {
+ log.Fatalf("%s add failed: %s", err)
+ }
+ }
+ c.Start()
+ return func() {
+ once.Do(func() {
+ c.Stop()
+ })
+ }
+}
+
+type CronFuncType func(ctx context.Context, l *zap.SugaredLogger)
+
+func TraceTime(fn CronFuncType) CronFuncType {
+ return func(ctx context.Context, l *zap.SugaredLogger) {
+ now := time.Now()
+ defer func() {
+ elapse := time.Since(now)
+ if elapse > time.Second {
+ l.Infof("takes %s", elapse.Truncate(time.Millisecond))
+ } else {
+ l.Infof("takes %d ms", elapse/time.Millisecond)
+ }
+ }()
+ l.Info("start run")
+ fn(ctx, l)
+ l.Info("end run")
+ }
+}
+
+// MustSecondSpec is a helper method to validate spec at compile time
+func MustSecondSpec(spec string) string {
+ parser := cron.NewParser(
+ cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
+ )
+ _, err := parser.Parse(spec)
+ if err != nil {
+ panic(err)
+ }
+ return spec
+}
diff --git a/pub/common/crypt/aesecb.go b/pub/common/crypt/aesecb.go
new file mode 100644
index 0000000..a75f817
--- /dev/null
+++ b/pub/common/crypt/aesecb.go
@@ -0,0 +1,43 @@
+package crypt
+
+import (
+ "bytes"
+ "crypto/aes"
+)
+
+func PKCS7PaddingEx(ciphertext []byte, blockSize int) []byte {
+ padding := blockSize - len(ciphertext)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ return append(ciphertext, padtext...)
+}
+
+func PKCS7UnPaddingEx(origData []byte) []byte {
+ length := len(origData)
+ unpadding := int(origData[length-1])
+ return origData[:(length - unpadding)]
+}
+
+func EcbDecrypt(data, key []byte) []byte {
+ block, _ := aes.NewCipher(key)
+ decrypted := make([]byte, len(data))
+ size := block.BlockSize()
+
+ for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
+ block.Decrypt(decrypted[bs:be], data[bs:be])
+ }
+
+ return PKCS7UnPaddingEx(decrypted)
+}
+
+func EcbEncrypt(data, key []byte) []byte {
+ block, _ := aes.NewCipher(key)
+ data = PKCS7PaddingEx(data, block.BlockSize())
+ decrypted := make([]byte, len(data))
+ size := block.BlockSize()
+
+ for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
+ block.Encrypt(decrypted[bs:be], data[bs:be])
+ }
+
+ return decrypted
+}
diff --git a/pub/common/crypt/crypt.go b/pub/common/crypt/crypt.go
new file mode 100644
index 0000000..c7f4f71
--- /dev/null
+++ b/pub/common/crypt/crypt.go
@@ -0,0 +1,615 @@
+package crypt
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/md5"
+ "crypto/rand"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "io"
+ "net/url"
+ "os"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+// CreateToken 生成Token算法
+func CreateToken(secret string, tokenClaims map[string]interface{}) (tokeness string, err error) {
+ if secret == "" {
+ return "", errors.New("secret is empty")
+ }
+ claims := jwt.MapClaims(tokenClaims)
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokeness, err = token.SignedString([]byte(secret))
+ if err != nil {
+ return "", err
+ }
+ return tokeness, nil
+}
+
+func keyFunc(secret string) jwt.Keyfunc {
+ return func(token *jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ }
+}
+
+// ParseToken 解析Token
+func ParseToken(secret string, tokeness string) (map[string]interface{}, error) {
+ if secret == "" || tokeness == "" {
+ return nil, errors.New("secret or tokeness is empty")
+ }
+ token, err := jwt.Parse(tokeness, keyFunc(secret))
+ if err != nil {
+ return nil, err
+ }
+ //验证token,如果token被修改过则为false
+ if !token.Valid {
+ return nil, errors.New("token is invalid")
+ }
+ claimsToken, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return nil, errors.New("cannot convert claim to MapClaim")
+ }
+ return claimsToken, nil
+}
+
+// StructToStr 结构体转json 先对结构体按照字典顺序排序 并返回字符串
+func StructToStr(obj interface{}) (string, error) {
+ if obj == nil {
+ return "", errors.New("obj is not nil")
+ }
+ data := make(map[string]interface{})
+ bytes, err := json.Marshal(obj)
+ if err != nil {
+ return "", err
+ }
+ err = json.Unmarshal(bytes, &data)
+ if err != nil {
+ return "", err
+ }
+ str, err := MapToStr(data)
+ if err != nil {
+ return "", err
+ }
+ return str, nil
+}
+
+// MapToStr 对map按照字典顺序排序 并返回字符串
+func MapToStr(data map[string]interface{}) (string, error) {
+ if data == nil || len(data) == 0 {
+ return "", errors.New("data is nil or empty")
+ }
+ newData := make(map[string]interface{})
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ newData[k] = data[k]
+ }
+ b, err := json.Marshal(newData)
+ if err != nil {
+ return "", err
+ }
+ str := string(b)
+ return str, nil
+}
+
+// JSON2Struct json转struct
+func JSON2Struct(str string, obj interface{}) {
+ if err := json.Unmarshal([]byte(str), &obj); err != nil {
+ return
+ }
+}
+
+// JSONArray2Struct json转struct
+func JSONArray2Struct(str []string, obj interface{}) []interface{} {
+ var data []interface{}
+ for _, v := range str {
+ JSON2Struct(v, &obj)
+ data = append(data, obj)
+ }
+ return data
+}
+
+// JSON2Map json转map
+func JSON2Map(str string) (map[string]interface{}, error) {
+ var mapResult map[string]interface{}
+ if err := json.Unmarshal([]byte(str), &mapResult); err != nil {
+ return nil, err
+ }
+ return mapResult, nil
+}
+
+// StructToStrNormal 结构体转str
+func StructToStrNormal(obj interface{}) (string, error) {
+ str, err := json.Marshal(obj)
+ if err != nil {
+ return "", err
+ }
+ return string(str), nil
+}
+
+func UrlValueToStr(values url.Values, exclude string) (string, string) {
+ if values == nil {
+ return "", ""
+ }
+ var buf bytes.Buffer
+ var sign string
+ keys := make([]string, 0, len(values))
+ for k, _ := range values {
+ if k == exclude {
+ sign = strings.Join(values[k], "")
+ continue
+ }
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for i, k := range keys {
+ if i != 0 {
+ buf.WriteString("&")
+ }
+ buf.WriteString(k)
+ buf.WriteString("=")
+ buf.WriteString(strings.Join(values[k], ""))
+ }
+ return buf.String(), sign
+}
+
+// 对参数进行排序后 拼接成url字符串 只对一级字符串类型的参数做处理
+func MapToUrlOnlyStr(data map[string]interface{}) string {
+ if data == nil || len(data) == 0 {
+ return ""
+ }
+ var buf bytes.Buffer
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for i, k := range keys {
+ if i != 0 {
+ buf.WriteString("&")
+ }
+ buf.WriteString(k)
+ buf.WriteString("=")
+ if v, ok := data[k].(string); ok {
+ buf.WriteString(v)
+ }
+ }
+ return buf.String()
+}
+
+// MapToStr 对map按照字典顺序排序 并返回字符串url 拼接的字段
+func MapToUrlStr(data map[string]interface{}) string {
+ if data == nil || len(data) == 0 {
+ return ""
+ }
+ var buf bytes.Buffer
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for i, k := range keys {
+ if i != 0 {
+ buf.WriteString("&")
+ }
+ buf.WriteString(k)
+ buf.WriteString("=")
+ if v, ok := data[k].(string); ok {
+ buf.WriteString(v)
+ } else {
+ t := reflect.TypeOf(data[k])
+ if t != nil && t.Kind() == reflect.Map {
+ if v, ok := data[k].(map[string]interface{}); ok {
+ buf.WriteString("{")
+ str := MapToUrlStr(v)
+ buf.WriteString(str)
+ buf.WriteString("}")
+ }
+ }
+ if t != nil && (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) {
+ arraybyte, _ := json.Marshal(data[k])
+ buf.WriteString(string(arraybyte))
+ }
+ }
+ }
+ return buf.String()
+}
+
+// MapToURL 集合转URL
+func MapToURL(m map[string]string) string {
+ if m == nil || len(m) == 0 {
+ return ""
+ }
+ v := url.Values{}
+ for k := range m {
+ v.Set(k, string(m[k]))
+ }
+ return v.Encode()
+}
+
+func Strcut2UrlValue(obj interface{}) (string, error) {
+ t := reflect.TypeOf(obj)
+ v := reflect.ValueOf(obj)
+ if t.Kind() != reflect.Struct {
+ return "", errors.New("obj must be struct type")
+ }
+ u := url.Values{}
+ for i := 0; i < t.NumField(); i++ {
+ var value string
+ tag := t.Field(i).Tag.Get("json")
+ switch t.Field(i).Type.Kind() {
+ case reflect.String:
+ value = v.Field(i).String()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ value = strconv.Itoa(int(v.Field(i).Int()))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ value = strconv.FormatUint(v.Field(i).Uint(), 10)
+ case reflect.Slice, reflect.Array:
+ buf := bytes.Buffer{}
+ b, _ := json.Marshal(v.Field(i).Interface())
+ t := strings.TrimRight(strings.TrimLeft(string(b), "["), "]")
+ array := strings.Split(t, ",")
+ if len(array) == 0 {
+ value = ""
+ }
+ if len(array) == 1 {
+ value = array[0]
+ } else {
+ for i, v := range array {
+ if i == 0 {
+ buf.WriteString(v)
+ continue
+ }
+ buf.WriteString("&")
+ buf.WriteString(tag)
+ buf.WriteString("=")
+ buf.WriteString(v)
+ value = buf.String()
+ }
+ }
+ }
+ u.Add(tag, value)
+ }
+ dcodeurl, err := url.QueryUnescape(u.Encode())
+ return dcodeurl, err
+}
+
+// StrToMd5 字符串转md5
+func StrToMd5(str string) string {
+ data := []byte(str)
+ return ByteToMd5(data)
+}
+
+// ByteToMd5 数组转md5
+func ByteToMd5(data []byte) string {
+ md5Ctx := md5.New()
+ md5Ctx.Write(data)
+ cipherStr := md5Ctx.Sum(nil)
+ return hex.EncodeToString(cipherStr)
+}
+
+// FileToMd5 文件转MD5
+func FileToMd5(fileName string) string {
+ file, err := os.Open(fileName)
+ if err != nil {
+ return ""
+ }
+ defer file.Close()
+ md5Ctx := md5.New()
+ if _, err := io.Copy(md5Ctx, file); err != nil {
+ return ""
+ }
+ cipherStr := md5Ctx.Sum(nil)
+ return hex.EncodeToString(cipherStr)
+}
+
+// HashCode 对一个字符串生成唯一的hasHcode 码
+func HashCode(src string) int {
+ v := int(crc32.ChecksumIEEE([]byte(src)))
+ if v > 0 {
+ return v
+ }
+ if -v > 0 {
+ return -v
+ }
+ return 0
+}
+
+// HexToString converts
+func HexToString(data []byte) string {
+ return hex.EncodeToString(data)
+}
+
+// StrToSha256 字符串转sha256
+func StrToSha256(str string) string {
+ shaCtx := sha256.New()
+ shaCtx.Write([]byte(str))
+ shaBytes := shaCtx.Sum(nil)
+ cipherStr := hex.EncodeToString(shaBytes[:])
+ return cipherStr
+}
+
+// StrToSha1 字符串转sha1
+func StrToSha1(str string, secret string) []byte {
+ h := hmac.New(sha1.New, []byte(secret))
+ h.Write([]byte(str))
+ return h.Sum(nil)
+}
+
+// StrToSha1 字符串转sha1 转16进制后
+func StrToHmacSha1(str, secret string) string {
+ return hex.EncodeToString(StrToSha1(str, secret))
+}
+
+// StrToHmacSha256 加密 字符串转sha256加密字符串
+func StrToHmacSha256(str string, secret string) string {
+ h := hmac.New(sha256.New, []byte(secret))
+ h.Write([]byte(str))
+ cipherStr := hex.EncodeToString(h.Sum(nil))
+ return cipherStr
+}
+
+func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
+ padding := blockSize - len(ciphertext)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ return append(ciphertext, padtext...)
+}
+
+func PKCS7UnPadding(origData []byte) []byte {
+ length := len(origData)
+ unpadding := int(origData[length-1])
+ return origData[:(length - unpadding)]
+}
+
+// AesEncrypt AES加密
+func AesEncrypt(origData, key string) ([]byte, error) {
+ origDataByte := []byte(origData)
+ return CoreAesEncrypt(origDataByte, key)
+}
+
+// AesDecrypt AES解密
+func AesDecrypt(crypted, key string) (string, error) {
+ cryptedByte := []byte(crypted)
+ return CoreAesDecrypt(cryptedByte, key)
+}
+
+// AesDecrypt AES解密
+func AesDecryptByte(cryptedByte []byte, key string) (string, error) {
+ return CoreAesDecrypt(cryptedByte, key)
+}
+
+// CoreAesEncrypt AES加密
+func CoreAesEncrypt(origData []byte, key string) ([]byte, error) {
+ keybyte := []byte(key)
+ block, err := aes.NewCipher(keybyte)
+ if err != nil {
+ return nil, err
+ }
+ blockSize := block.BlockSize()
+ origData = PKCS7Padding(origData, blockSize)
+ blockMode := cipher.NewCBCEncrypter(block, keybyte[:blockSize])
+ crypted := make([]byte, len(origData))
+ blockMode.CryptBlocks(crypted, origData)
+ return crypted, nil
+}
+
+// CoreAesDecrypt AES解密
+func CoreAesDecrypt(crypted []byte, key string) (string, error) {
+ keybyte := []byte(key)
+ block, err := aes.NewCipher(keybyte)
+ if err != nil {
+ return "", err
+ }
+ blockSize := block.BlockSize()
+ blockMode := cipher.NewCBCDecrypter(block, keybyte[:blockSize])
+ origData := make([]byte, len(crypted))
+ blockMode.CryptBlocks(origData, crypted)
+ origData = PKCS7UnPadding(origData)
+ return string(origData), nil
+}
+
+// XorEnc
+func XorEnc(src string, xorKey string) string {
+ var result string
+ j := 0
+ bt := []rune(src)
+ xor := []rune(xorKey)
+ for i := 0; i < len(bt); i++ {
+ s := strconv.FormatInt(int64(bt[i]^xor[j]), 10)
+ result = result + s
+ j = 1 % len(xor)
+ }
+ return result
+}
+
+// XorDec
+func XorDec(src string, xorKey string) string {
+ var result string
+ j := 0
+ bt := []rune(src)
+ xor := []rune(xorKey)
+ for i := 0; i < len(bt); i++ {
+ s := strconv.FormatInt(int64(bt[i]^xor[j]), 10)
+ result = result + s
+ j = i % len(xor)
+ }
+ return result
+}
+
+func XorDecode(src, key string) string {
+ srcByte, _ := base64.StdEncoding.DecodeString(src)
+ keyByte, _ := base64.StdEncoding.DecodeString(key)
+ srcByte = xor(srcByte, keyByte)
+ return base64.StdEncoding.EncodeToString(srcByte)
+}
+
+func XorEncode(src, key string) string {
+ return XorDecode(src, key)
+}
+
+func xor(src []byte, key []byte) []byte {
+ for i := 0; i < len(src); i++ {
+ src[i] ^= key[i%len(key)]
+ }
+ return src
+}
+
+func XORLength(src []byte, key []byte, length int) []byte {
+ for i := 0; i < length; i++ {
+ src[i] ^= key[i%len(key)]
+ }
+ return src[:length]
+}
+
+// https://core.telegram.org/api/end-to-end#sending-and-receiving-messages-in-a-secret-chat
+func CoreAesEncryptEx(plain []byte, nonceLen int, key string) ([]byte, error) {
+ nonce := make([]byte, nonceLen)
+ _, err := io.ReadFull(rand.Reader, nonce)
+ if err != nil {
+ return nil, err
+ }
+ var largeShaRaw []byte
+ largeShaRaw = append(largeShaRaw, []byte(key)...)
+ largeShaRaw = append(largeShaRaw, nonce...)
+ largeShaRawMid := len(largeShaRaw) / 2
+ msgKeyLarge := sha256.Sum256(largeShaRaw)
+ msgKey := msgKeyLarge[8:24] //16 bytes
+
+ var shaRawA []byte
+ shaRawA = append(shaRawA, msgKey...)
+ shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...)
+ sha256a := sha256.Sum256(shaRawA) //32 bytes
+
+ var shaRawB []byte
+ shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...)
+ shaRawB = append(shaRawB, msgKey...)
+ sha256b := sha256.Sum256(shaRawB) //32 bytes
+
+ var aesKey []byte //32 bytes AES-256
+ aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes
+ aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes
+ aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes
+
+ var aesIV []byte // 16 bytes
+ aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes
+ aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes
+ aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes
+
+ block, err := aes.NewCipher(aesKey)
+ if err != nil {
+ return nil, err
+ }
+ blockSize := block.BlockSize()
+ ciphertext := PKCS7Padding(plain, blockSize)
+ mode := cipher.NewCBCEncrypter(block, aesIV)
+ cipher := make([]byte, nonceLen+len(ciphertext))
+ copy(cipher, nonce)
+ mode.CryptBlocks(cipher[nonceLen:], ciphertext)
+ return cipher, nil
+}
+
+func CoreAesDecryptEx(crypt []byte, nonceLen int, key string) ([]byte, error) {
+ nonce := make([]byte, nonceLen)
+ copy(nonce, crypt)
+ var largeShaRaw []byte
+ largeShaRaw = append(largeShaRaw, []byte(key)...)
+ largeShaRaw = append(largeShaRaw, nonce...)
+ largeShaRawMid := len(largeShaRaw) / 2
+ msgKeyLarge := sha256.Sum256(largeShaRaw)
+ msgKey := msgKeyLarge[8:24] //16 bytes
+
+ var shaRawA []byte
+ shaRawA = append(shaRawA, msgKey...)
+ shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...)
+ sha256a := sha256.Sum256(shaRawA) //32 bytes
+
+ var shaRawB []byte
+ shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...)
+ shaRawB = append(shaRawB, msgKey...)
+ sha256b := sha256.Sum256(shaRawB) //32 bytes
+
+ var aesKey []byte //32 bytes AES-256
+ aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes
+ aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes
+ aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes
+
+ var aesIV []byte // 16 bytes
+ aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes
+ aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes
+ aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes
+
+ block, err := aes.NewCipher(aesKey)
+ if err != nil {
+ return nil, err
+ }
+ realData := crypt[nonceLen:]
+ blockMode := cipher.NewCBCDecrypter(block, aesIV)
+ origData := make([]byte, len(realData))
+ blockMode.CryptBlocks(origData, realData)
+ return PKCS7UnPadding(origData), nil
+}
+
+// base64补全
+func padding(origin string) string {
+ missing := len(origin) % 4
+ if missing != 0 {
+ origin += strings.Repeat("=", 4-missing)
+ }
+
+ return origin
+}
+
+// AdDecrypt 集团广告中心-广告数据解密专用
+func AdDecrypt(encryptedData, keyBase64 string) (res string, err error) {
+ // 1. Base64 解码获取密钥
+ keyBytes, err := base64.StdEncoding.DecodeString(padding(keyBase64))
+ if err != nil {
+ return "", fmt.Errorf("密钥 Base64 解码失败: %v", err)
+ }
+ // 2. Base64 解码获取加密数据
+ data, err := base64.StdEncoding.DecodeString(padding(encryptedData))
+ if err != nil {
+ return "", fmt.Errorf("数据 Base64 解码失败: %v", err)
+ }
+
+ // 3. 拆分 IV 与密文(前 12 字节为 IV)
+ if len(data) < 12 {
+ return "", errors.New("数据长度不足,无法提取 IV")
+ }
+ iv := data[:12]
+ ciphertext := data[12:]
+ // 4. 初始化 AES 密码块
+ block, err := aes.NewCipher(keyBytes)
+ if err != nil {
+ return "", fmt.Errorf("创建 AES 密码块失败: %v", err)
+ }
+ // 5. 采用 GCM 模式
+ aesGCM, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", fmt.Errorf("创建 GCM 失败: %v", err)
+ }
+ // 6. 解密数据
+ plainText, err := aesGCM.Open(nil, iv, ciphertext, nil)
+ if err != nil {
+ return "", fmt.Errorf("解密失败: %v", err)
+ }
+
+ return string(plainText), nil
+}
diff --git a/pub/common/crypt/crypt_test.go b/pub/common/crypt/crypt_test.go
new file mode 100644
index 0000000..f3b5c6b
--- /dev/null
+++ b/pub/common/crypt/crypt_test.go
@@ -0,0 +1,27 @@
+package crypt
+
+import (
+ "testing"
+ "videoweb/pub/common/log"
+)
+
+var (
+ PlatPublicKey = `
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWtWnWmTgLemO+GBMWIX9N5yx2
+YMc+4MczBogNdTFjF4rrd/+Y1G9qJ8ZDHR7qjBQgJjzcZrGYRUf2wiY6T9ble9+h
+s14pYPpXqkSoD2VOQI4+idb6nRgVQ6rXyMQps7ORcshmEtIS/Ra3aG3XaAFAUCfO
+E3nHGyldtNIGvuBTAQIDAQAB
+-----END PUBLIC KEY-----`
+)
+
+func TestRSA(t *testing.T) {
+ //data := make([]byte, 0)
+ testData := "是对的发送打赏打赏打赏打赏的"
+ rawContext, err := RSAEncryptByPublicKey(PlatPublicKey, []byte(testData))
+ if err != nil {
+ log.Error("wnth RSAEncryptByPublicKey err", log.Any("err", err))
+ return
+ }
+ log.Info(string(rawContext))
+}
diff --git a/pub/common/crypt/rsa.go b/pub/common/crypt/rsa.go
new file mode 100644
index 0000000..0ae93f7
--- /dev/null
+++ b/pub/common/crypt/rsa.go
@@ -0,0 +1,145 @@
+package crypt
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "videoweb/pub/common/log"
+)
+
+func splitBytesSlice(data []byte, n int) [][]byte {
+ var chunk []byte
+ chunks := make([][]byte, 0, len(data)/n+1)
+ for len(data) >= n {
+ chunk, data = data[:n], data[n:]
+ chunks = append(chunks, chunk)
+ }
+ if len(data) > 0 {
+ chunks = append(chunks, data[:])
+ }
+ return chunks
+}
+
+func rsaBytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
+ // fmt.Printf("pubKey is %s", string(pub))
+ block, _ := pem.Decode(pub)
+ enc := x509.IsEncryptedPEMBlock(block)
+ b := block.Bytes
+ var err error
+ if enc {
+ b, err = x509.DecryptPEMBlock(block, nil)
+ if err != nil {
+ log.Error("RSABytesToPublicKey DecryptPEMBlock err", log.E(err))
+ return nil, err
+ }
+ }
+
+ pk, err := x509.ParsePKIXPublicKey(b)
+ if err != nil {
+ log.Error("RSABytesToPublicKey ParsePKIXPublicKey err", log.E(err))
+ return nil, err
+ }
+ if pk_, ok := pk.(*rsa.PublicKey); ok {
+ return pk_, nil
+ }
+ log.Error("RSABytesToPublicKey error input not PublicKey")
+ return nil, errors.New("Not RSA PublicKey format")
+}
+
+func rsaBytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
+ block, _ := pem.Decode(priv)
+ enc := x509.IsEncryptedPEMBlock(block)
+ b := block.Bytes
+ var err error
+ if enc {
+ b, err = x509.DecryptPEMBlock(block, nil)
+ if err != nil {
+ log.Error("RSABytesToPrivateKey DecryptPEMBlock err", log.E(err))
+ return nil, err
+ }
+ }
+ key, err := x509.ParsePKCS8PrivateKey(b)
+ if err != nil {
+ log.Error("RSABytesToPrivateKey ParsePKCS8PrivateKey err", log.E(err))
+ return nil, err
+ }
+ if pk_, ok := key.(*rsa.PrivateKey); ok {
+ return pk_, nil
+ }
+ log.Error("RSABytesToPrivateKey error input not PrivateKey")
+ return nil, errors.New("Not RSA PrivateKey format")
+}
+
+// 公钥加密
+func RSAEncryptByPublicKey(pemPubKey string, data []byte) ([]byte, error) {
+ pubKey, err := rsaBytesToPublicKey([]byte(pemPubKey))
+ if err != nil {
+ log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err))
+ return nil, err
+ }
+ partLen := pubKey.N.BitLen()/8 - 11
+ chunks := splitBytesSlice(data, partLen)
+ result := bytes.Buffer{}
+ for _, chunk := range chunks {
+ cipherSilce, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, chunk)
+ if err != nil {
+ log.Error("RSAEncryptByPublicKey EncryptPKCS1v15 err", log.E(err))
+ return nil, err
+ }
+ result.Write(cipherSilce)
+ }
+ return result.Bytes(), nil
+}
+
+// 私玥签名
+func RSASignByPrivateKey(pemPrivateKey string, data []byte, hash crypto.Hash) ([]byte, error) {
+ privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey))
+ if err != nil {
+ log.Error("RSASignByPrivateKey rsaBytesToPrivateKey err", log.E(err))
+ return nil, err
+ }
+ h := hash.New()
+ h.Write(data)
+ hashed := make([]byte, 0)
+ hashed = h.Sum(hashed)
+
+ return rsa.SignPKCS1v15(rand.Reader, privateKey, hash, hashed)
+
+}
+func RSAVerifyByPublicKey(publicKey string, data, sign []byte, hash crypto.Hash) (err error) {
+ pubKey, err := rsaBytesToPublicKey([]byte(publicKey))
+ if err != nil {
+ log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err))
+ return err
+ }
+ h := hash.New()
+ h.Write(data)
+ hashed := make([]byte, 0)
+ hashed = h.Sum(hashed)
+ return rsa.VerifyPKCS1v15(pubKey, hash, hashed, sign)
+}
+
+// 公钥解密
+func RSADecryptByPrivateKey(pemPrivateKey string, cipherData []byte) ([]byte, error) {
+ privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey))
+ if err != nil {
+ log.Error("RSADecryptByPrivateKey rsaBytesToPrivateKey err", log.E(err))
+ return nil, err
+ }
+ blockLen := privateKey.N.BitLen() / 8
+ chunks := splitBytesSlice(cipherData, blockLen)
+ result := bytes.Buffer{}
+ for _, chunk := range chunks {
+ plain, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, chunk)
+ if err != nil {
+ log.Error("RSADecryptByPrivateKey DecryptPKCS1v15 err", log.E(err))
+ return nil, err
+ }
+ result.Write(plain)
+ }
+ return result.Bytes(), nil
+}
diff --git a/pub/common/enum/consts/consts.go b/pub/common/enum/consts/consts.go
new file mode 100644
index 0000000..13cdd03
--- /dev/null
+++ b/pub/common/enum/consts/consts.go
@@ -0,0 +1,126 @@
+package consts
+
+import (
+ "strings"
+ "time"
+)
+
+type RunMod string
+
+const (
+ RunModeDev RunMod = "dev"
+ RunModeTest RunMod = "test"
+ RunModeProd RunMod = "prod"
+ RunModRelease RunMod = "release"
+)
+
+// gin中ctx使用的key
+const (
+ CtxUserID = "USER_ID"
+ CtxUpUserID = "UP_USER_ID"
+ CtxUpInfo = "UP_INFO"
+ CtxUA = "UA"
+ CtxUAH5 = "UAH5"
+ CtxIP = "IP"
+ CtxDevId = "DEVID"
+ CtxDevType = "DEVTYPE"
+ CtxSysType = "SYSTYPE"
+ CtxVersion = "VERSION"
+ CtxBuildId = "BUILDID"
+ CtxToken = "TOKEN"
+ CtxOnlineTime = "ONLINE_TIME"
+)
+
+const (
+ AppSysType_And = ""
+ AppSysType_Ios = ""
+)
+
+// Etcd 的Key
+const (
+ EtcdWarmupDB = "/micro/config/com.sz.api.server/warmupdb"
+ EtcdSzDB = "/micro/config/com.sz.api.server/db"
+ EtcdRds = "/micro/config/com.sz.api.server/redis"
+)
+
+const (
+ AdminJwtTokenExpire = 30 * 24 * time.Hour
+ UserJwtTokenExpire = 30 * 24 * time.Hour
+ UserSignInExpire = 60 * 24 * time.Hour
+)
+
+const (
+ UAHeader = "User-Agent"
+)
+
+const (
+ IOS = "ios"
+ Android = "android"
+ H5 = "h5"
+ PC = "pc"
+ H5IOS = "h5_ios"
+ H5Android = "h5_android"
+ H5PC = "h5_pc"
+ Light = "light"
+)
+
+const (
+ CaptchaLen = 6 //验证码长度
+)
+
+const (
+ TypeIOS = "ios"
+ TypeAndroid = "and"
+ TypeWeb = "h5"
+)
+
+// 机型
+const (
+ WebTypeIOS = "ios"
+ WebTypeAndroid = "android"
+ WebSysTypeH5 = "h5"
+)
+
+// func HandleSysType(sysType string) (sys string) {
+// if strings.Contains(strings.ToLower(sysType), TypeIOS) {
+// sys = IOS
+// }
+//
+// if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
+// sys = Android
+// }
+//
+// if strings.Contains(strings.ToLower(sysType), TypeWeb) {
+// sys = H5
+// }
+// return
+// }
+
+func ToWebSysType(sysType string) (sys string) {
+ if strings.Contains(strings.ToLower(sysType), TypeIOS) {
+ sys = WebTypeIOS
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
+ sys = WebTypeAndroid
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeWeb) {
+ sys = WebSysTypeH5
+ }
+ return
+}
+func ToNewSysType(sysType string) (sys string) {
+ if strings.Contains(strings.ToLower(sysType), TypeIOS) {
+ sys = WebTypeIOS
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
+ sys = WebTypeAndroid
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeWeb) {
+ sys = WebSysTypeH5
+ }
+ return
+}
\ No newline at end of file
diff --git a/pub/common/enum/redisconst/const.go b/pub/common/enum/redisconst/const.go
new file mode 100644
index 0000000..2286237
--- /dev/null
+++ b/pub/common/enum/redisconst/const.go
@@ -0,0 +1,226 @@
+package redisconst
+
+import (
+ "fmt"
+ "strings"
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+const (
+ visitLogFmt = "bd_visitlog:%s:%d"
+ FiveMVisit = "bd_visitFmt:%s"
+ BloomUser = "bd_bloom_user:%d"
+ AppUserToken = "bd_app_user_token:%d"
+ captchaKey = "bd_captcha_mobile_%s"
+ CaptchaExpire = 5 * time.Minute
+ replayNonceKey = "bd_replay:nonce:_%s"
+ mediaM3U8Fmt = "m3u8cache:%s"
+ MediaM3U8CacheExpire = time.Hour * 24 * 7
+
+ //禁止登陆设置
+ userForbit = "userForbit:%d"
+ ipForbit = "ipForbit:%s"
+
+ //每分钟Ip注册次数
+ registerLimist = "registerLimit:%s"
+ RegisterLimistExpire = time.Minute * 30
+
+ //用户关注限制
+ careLimitKey = "carelimit:%d"
+ CareLimitKeyExpire = 15 * time.Second
+
+ //用户评论限制
+ commentLimitKey = "commentlimit:%s"
+ CommentLimitKeyExpire = 5 * time.Second
+ CommentDayLimitKeyExpire = 10 * time.Hour
+
+ ContactUsLimitKey = "contactuslimit:%s"
+ ContactusLimitKeyExpire = 1 * time.Minute
+ ContactusLimitKeyDayExpire = 10
+ //每用户访问次数限制
+ userCallLimit = "userCall:%d"
+ ipCallLimit = "ipCall:%s"
+ CallLimitExpire = time.Second * 10
+
+ userSidKey = "userSid:%d"
+ UserSidKeyExpire = time.Minute * 30
+
+ // 评论缓存key和过期时间
+ CommentListKey = "commentList_%d_%d"
+ CommentListKeyExpire = time.Minute * 5
+
+ // 视频收藏列表分页缓存key、版本key和过期时间
+ FavoriteListVersionKeyExpire = time.Hour * 24
+
+ FavoriteVideoListKey = "favoriteVideoList:v%d:%d:%d:%d"
+ FavoriteVideoListVersionKey = "favoriteVideoListVersion:%d"
+ FavoriteVideoListKeyExpire = time.Minute * 30
+
+ // 问卷提交频率限制(同一 IP / 用户每分钟只能提交一次)
+ questionnaireLimitIp = "questionnaireLimit:ip:%s"
+ questionnaireLimitUser = "questionnaireLimit:user:%d"
+ QuestionnaireLimitExpire = time.Minute
+
+ // m3u8 / enkey 防盗链
+ // IP 自然日正片去重计数(key 含日期,次日自动换桶)
+ m3u8DayRateLimitIp = "m3u8dayrate:ip:%s:%s" // %s=ip, %s=日期 20060102
+ m3u8DailyMediaIpFmt = "m3u8:daily:media:ip:%s:%d:%s" // ip, mediaId, 日期
+ M3U8DayRateLimitMax = int64(80) // 每 IP 每天最多 80 个不同正片(sys_cfg 未配置时的默认值)
+ // enkey 一次性票据:/h5/m3u8 鉴权通过后写入,/enkey 消费后删除
+ enkeyTicketKey = "enkey:ticket:%s" // %s = token 前16位
+ EnkeyTicketExpire = 15 * time.Minute
+
+ // 访问IP去重统计:每个 appId 每天一个 SET
+ ipStatKey = "ipstat:%d:%s" // %d=appId, %s=日期 "20060102"
+ IpStatExpire = 49 * time.Hour // 保留49小时,确保昨日数据在今天仍可读
+)
+
+func BloomUserKey(userId int64) string {
+ return fmt.Sprintf(BloomUser, userId)
+}
+
+func FiveMVisitKey(time string) string {
+ return fmt.Sprintf(FiveMVisit, time)
+}
+
+func UserVisitLogKey(uid int64, t time.Time) string {
+ return fmt.Sprintf(visitLogFmt, t.Format("20060102"), uid)
+}
+
+func MobileCaptchaKey(mobile string) string {
+ return fmt.Sprintf(captchaKey, mobile)
+}
+
+func AppUserTokenKey(userId int64) string {
+ return fmt.Sprintf(AppUserToken, userId)
+}
+
+// AppPlayletHomeCacheVersionKey 用于后台变更后切换短剧首页缓存版本。
+func AppPlayletHomeCacheVersionKey() string {
+ return fmt.Sprintf("app:playlet:home:version:%d", pubdef.AppId)
+}
+
+// AppPlayletRankCacheVersionKey 用于短剧删除后刷新榜单缓存
+func AppPlayletRankCacheVersionKey() string {
+ return fmt.Sprintf("app:playlet:rank:version:%d", pubdef.AppId)
+}
+
+func MediaM3U8Key(name string) string {
+ return fmt.Sprintf(mediaM3U8Fmt, name)
+}
+func UserForbitKey(userId int64) string {
+ return fmt.Sprintf(userForbit, userId)
+}
+
+func RegisterLimitKey(ip string) string {
+ ip = strings.ReplaceAll(ip, ":", "-")
+ return fmt.Sprintf(registerLimist, ip)
+}
+
+func CareLimitKey(userId int64) string {
+ return fmt.Sprintf(careLimitKey, userId)
+}
+func ReplayNonceKey(nonce string) string {
+ return fmt.Sprintf(replayNonceKey, nonce)
+}
+
+func CommentLimitKey(ip string) string {
+ return fmt.Sprintf(commentLimitKey, ip)
+}
+func CommentOneDayLimitKey(ip string) string {
+ return fmt.Sprintf("commentdaylimit:%s", ip)
+}
+
+func UserCallLimitKey(userId int64) string {
+ return fmt.Sprintf(userCallLimit, userId)
+}
+
+func IpCallLimitKey(ip string) string {
+ return fmt.Sprintf(ipCallLimit, ip)
+}
+
+func IpForbitKey(ip string) string {
+ ip = strings.ReplaceAll(ip, ":", "-")
+ return fmt.Sprintf(ipForbit, ip)
+}
+
+func UserSidKey(userId int64) string {
+ return fmt.Sprintf(userSidKey, userId)
+}
+
+// CommentListCacheKey 返回评论列表缓存key
+func CommentListCacheKey(objectId int64, objectType pubdef.CommentObjectType) string {
+ return fmt.Sprintf(CommentListKey, objectId, objectType)
+}
+
+func FavoriteVideoListCacheKey(userId, version, pageNum, pageSize int64) string {
+ return fmt.Sprintf(FavoriteVideoListKey, version, userId, pageNum, pageSize)
+}
+
+func FavoriteVideoListVersionCacheKey(userId int64) string {
+ return fmt.Sprintf(FavoriteVideoListVersionKey, userId)
+}
+
+func QuestionnaireLimitIpKey(ip string) string {
+ ip = strings.ReplaceAll(ip, ":", "-")
+ return fmt.Sprintf(questionnaireLimitIp, ip)
+}
+
+func QuestionnaireLimitUserKey(userId int64) string {
+ return fmt.Sprintf(questionnaireLimitUser, userId)
+}
+
+// M3U8DayRateLimitIpKey 按自然日(Asia/Shanghai)生成 IP 日限流 key
+func M3U8DayRateLimitIpKey(ip string) string {
+ ip = normalizeM3U8Ip(ip)
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ day := time.Now().In(loc).Format("20060102")
+ return fmt.Sprintf(m3u8DayRateLimitIp, ip, day)
+}
+
+// M3U8DayRateLimitExpire 返回距下一个自然日 0 点的时长,用于 key TTL
+func M3U8DayRateLimitExpire() time.Duration {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ now := time.Now().In(loc)
+ next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc)
+ d := next.Sub(now)
+ if d < time.Minute {
+ return time.Minute
+ }
+ return d
+}
+
+// EnkeyTicketKey token 的前 16 个字符作为 key,避免存完整 token 太长
+func EnkeyTicketKey(token string) string {
+ if len(token) > 16 {
+ token = token[:16]
+ }
+ return fmt.Sprintf(enkeyTicketKey, token)
+}
+
+func normalizeM3U8Ip(ip string) string {
+ return strings.ReplaceAll(ip, ":", "-")
+}
+
+// M3U8DailyMediaIpKey 同一 IP 同一正片当天只计一次
+func M3U8DailyMediaIpKey(ip string, mediaId int64) string {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ day := time.Now().In(loc).Format("20060102")
+ return fmt.Sprintf(m3u8DailyMediaIpFmt, normalizeM3U8Ip(ip), mediaId, day)
+}
+
+// IpStatKey 返回指定 appId 在某天的访问IP集合 key
+// date 格式: "20060102"
+func IpStatKey(appId int64, date string) string {
+ return fmt.Sprintf(ipStatKey, appId, date)
+}
diff --git a/pub/common/enum/video/video.go b/pub/common/enum/video/video.go
new file mode 100644
index 0000000..219758d
--- /dev/null
+++ b/pub/common/enum/video/video.go
@@ -0,0 +1,27 @@
+package video
+
+type VideoType int64
+type videoStatus int64
+
+// 0-上架、1-未上架
+const (
+ Enable videoStatus = iota
+ Disable
+)
+
+// 1是AV、2是SP
+const (
+ _ VideoType = iota
+ AV
+ SP
+)
+
+const AVLimit = 1
+
+type recordType int64
+
+const (
+ _ recordType = iota
+ Watch
+ Downlaoad
+)
diff --git a/pub/common/errors/code.go b/pub/common/errors/code.go
new file mode 100644
index 0000000..48e466d
--- /dev/null
+++ b/pub/common/errors/code.go
@@ -0,0 +1,169 @@
+package errors
+
+type CodeType int64
+
+const NotFind = "mongo: no documents in result"
+
+// 错误级别
+const (
+ ParamLevel = 1000 // 参数级
+ StoreLevel = 3000 // 存储级
+ ServerLevel = 4000 // 服务级
+ BusinessLevel = 6000 // 业务逻辑级
+)
+
+// app 端用到了
+// 1000, 重新登陆
+// 4002 短信发送失败
+// 6017 手机号码已绑定
+// 6023 user_video操作错误
+// 这里作出兼容
+
+func (c CodeType) Error() string {
+ return ""
+}
+
+const (
+ InvalidateToken CodeType = ParamLevel + iota // 不可用的token,客户端需要重新登陆
+ MethodNotFound // 方法为找到,指gin方法未找到
+ ApiNotFound // api未找到
+ Limiting // 限流
+ NoToken // 无token 1004
+)
+
+const (
+ DBQuery CodeType = StoreLevel + iota
+ DBInsert
+ DBUpdate
+ DBDelete
+ RedisSet
+ RedisGet
+ RedisDel
+ ProxyPutObject
+ EtcdGet
+ EtcdPut
+ RedisRPush
+ RedisLPop
+)
+
+const (
+ HttpClient CodeType = ServerLevel + iota // httpclient
+ TypePointer // 指针类型
+ SmsLimit // 短信限制
+ SmsSend // 短信发送失败
+ SmsTmp // 短信模版
+ SmsTmpParse // 短信模版解析
+ UserId // 用户ID
+ OpenFile // 打开文件
+ FsErr // seaweedfs
+ CtxTimeout // context超时
+ UnmarshalJson // json
+ FSFiler // fs filer
+ HttpGet //
+ ReadData // read data
+ IOCopy // io copy
+ Pass // pass
+ PassConfID // pass config id
+ Tips // 提示错误
+)
+
+const (
+ // 用户权限
+ UserPermission CodeType = BusinessLevel + iota
+ Unknown // 未知错误
+ InvalidKeyword // 无效的关键字
+ UserName // 用户名称
+ Mobile // 手机号码
+ Captcha // 验证码
+ CaptchaErr // 验证码错误
+ Psw // 密码
+ PayCode // 用户支付密码
+ Number // 数字类型
+ String // 字符串类型
+ VideoId // 视频ID
+ Progress // 进度
+ Ids // id数组
+ CatagoryExist // 视频种类已存在
+ MobileBinded // 手机号码已绑定
+ Rate // 返佣金比例
+ Brokerage // 返佣
+ Logo // 用户logo
+ UploadFile //
+ EmptyFile //
+ Operate // user_video操作错误
+ UserDisable //
+ NickName //
+ Gender //
+ InviteCode //
+ Coat //
+ ReLogin //
+ PopType //
+ AgentBinded //
+ M3u8FileName //
+ M3u8Read //
+ VideoName //
+ ExchangeCodeErr // 兑换码错误
+ ChanAcount //
+ Recharge //
+ Withdraw //
+ ExportErr // 导出参数错误
+ DomainErr // 主域名错误
+ ForbiddenWord // 禁言
+ ForbiddenLogin // 禁止登陆
+ DuplicateLike // 重复点赞
+ AliPayUnBind // 支付宝未绑定
+ BankCardUnBind // 银行卡未绑定
+ VideoUploadExist // 视频重复上传
+ Task //
+ _ // 已移除帖子参数错误码,保留原数值
+ _ // 已移除帖子重复发送错误码,保留原数值
+ _ // 已移除帖子不存在错误码,保留原数值
+ _ // 已移除帖子重复点赞错误码,保留原数值
+ _ // 已移除帖子取消点赞错误码,保留原数值
+ _ // 已移除帖子重复收藏错误码,保留原数值
+ _ // 已移除帖子取消收藏错误码,保留原数值
+ _ // 已移除帖子重复屏蔽错误码,保留原数值
+ _ // 已移除帖子取消屏蔽错误码,保留原数值
+ UserDoNotForSelf // 用户不能对自己进行此项操作
+ UserWatchDupi // 不可重复关注同一用户
+ UserNoWatch // 没有关注该用户,不可取消关注
+ _ // 已移除帖子回复参数错误码,保留原数值
+ _ // 已移除帖子评论不存在错误码,保留原数值
+ _ // 已移除帖子评论已存在错误码,保留原数值
+ NoWatchableUsers // 没有可以关注的用户池
+ _ // 已移除帖子评论点赞错误码,保留原数值
+ _ // 已移除帖子评论取消点赞错误码,保留原数值
+ _ // 已移除帖子定价错误码,保留原数值
+ _ // 已移除帖子已购买错误码,保留原数值
+ NoBalance // 余额不足
+ BalanceErr // 编辑余额错误
+ NoAccount // 没账户
+ AccountDisabled // 账户已被封禁
+ BuySelf // 不能购买自己的内容
+ RedisError // 服务器繁忙
+ Button // 落地页按钮
+ ReplyTimeShort // 回复间隔时间太短
+ OperateLog // 日志出错
+ RoleErr // 角色权限不够
+ DocExists // 查找的文档已存在
+ _ // 已移除帖子未知错误码,保留原数值
+ SeekScoreConfirm // 商人追分确认
+ GetOrderInfo // 商人充值订单报错
+ PromSeq // 商区错误
+ SignErr // 签名错误
+ BadRequest // 传参错误
+ ReplyPoolExisted // 评论已存在
+ LimitIPErr // 限制IP登陆
+)
+
+// 代充专用
+const (
+ TakeChatFail CodeType = iota + 18000 //发起代充聊天失败 18000
+ GetOrderInfoFail //获取代理充值订单信息失败 18001
+ AppIDNotExist //代充AppID 不存在 18002
+ TraderNotExist //此商人不存在 18003
+ TraderBalanceNotEnough //商人余额不足 18004
+ TraderHasLocked //商人被禁止登陆 18005
+ TraderSeekScoreFail //商人追分失败 18006
+ TraderSeekScoreFailDueNotEnough //商人追分失败 18007
+)
diff --git a/pub/common/errors/error.go b/pub/common/errors/error.go
new file mode 100644
index 0000000..1421715
--- /dev/null
+++ b/pub/common/errors/error.go
@@ -0,0 +1,39 @@
+package errors
+
+import (
+ "fmt"
+)
+
+// SrvErr 错误定义
+type SrvErr struct {
+ Code CodeType `json:"code"`
+ Msg string `json:"msg"`
+ Tips string `json:"tip,omitempty"`
+ Cause string `json:"-"`
+}
+
+// New create an error
+func New(code CodeType, msg string, cause string) *SrvErr {
+ return &SrvErr{
+ Code: code,
+ Msg: msg,
+ Cause: cause,
+ }
+}
+
+// New create an error
+func NewTips(code CodeType, tips string, cause string) *SrvErr {
+ return &SrvErr{
+ Code: code,
+ Msg: tips,
+ Tips: tips,
+ Cause: cause,
+ }
+}
+
+func (s *SrvErr) Error() string {
+ if s == nil {
+ return ""
+ }
+ return fmt.Sprintf("code = %d, msg = %s ,case = %v", s.Code, s.Msg, s.Cause)
+}
diff --git a/pub/common/errors/make.go b/pub/common/errors/make.go
new file mode 100644
index 0000000..4d6cf54
--- /dev/null
+++ b/pub/common/errors/make.go
@@ -0,0 +1,24 @@
+package errors
+
+import (
+ "fmt"
+ "runtime"
+)
+
+func MakeWithErr(code CodeType, err error) error {
+ pc, fn, line, _ := runtime.Caller(1)
+ cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, err.Error())
+ return New(code, err.Error(), cause)
+}
+
+func MakeWithMsg(code CodeType, msg string) error {
+ pc, fn, line, _ := runtime.Caller(1)
+ cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, msg)
+ return New(code, msg, cause)
+}
+
+func MakeWithTips(code CodeType, tips string) error {
+ pc, fn, line, _ := runtime.Caller(1)
+ cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, tips)
+ return NewTips(code, tips, cause)
+}
diff --git a/pub/common/errors/msg_app.go b/pub/common/errors/msg_app.go
new file mode 100644
index 0000000..a74ccfe
--- /dev/null
+++ b/pub/common/errors/msg_app.go
@@ -0,0 +1,19 @@
+package errors
+
+// error tips
+const (
+
+ //基础错误提示
+ ET_Store = "存储错误"
+ ET_DBOp = "DB操作错误"
+ ET_RdsOp = "Rds操作错误"
+ ET_Param = "参数错误"
+ ET_DBQury = "DB查询错误"
+ ET_DBNoResult = "DB查询数据不存在"
+ ET_TransactionsErr = "事务运行错误"
+ //----------------------------------------------
+ ET_UserNoExist = "用户不存在"
+ ET_BloggerApply = "博主申请条件不满足"
+ ET_BloggerAlreadyIs = "用户已经是博主"
+ ET_BalanceNotEnough = "余额不足"
+)
diff --git a/pub/common/fconst/fconst.go b/pub/common/fconst/fconst.go
new file mode 100644
index 0000000..e100ba5
--- /dev/null
+++ b/pub/common/fconst/fconst.go
@@ -0,0 +1,6 @@
+package fconst
+
+const (
+ FileDir = "file-tmp"
+ FsDir = "/fs"
+)
diff --git a/pub/common/godef/godef.go b/pub/common/godef/godef.go
new file mode 100644
index 0000000..4a1f48e
--- /dev/null
+++ b/pub/common/godef/godef.go
@@ -0,0 +1,26 @@
+package godef
+
+import (
+ "runtime"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/tgbot"
+)
+
+func Go(f func()) {
+ //atomic.AddInt64(&routineCount, 1)
+ go func() {
+ defer func() {
+ //atomic.AddInt64(&routineCount, -1)
+ if r := recover(); r != nil {
+ buf := make([]byte, 2048)
+ length := runtime.Stack(buf, false)
+ info := string(buf[:length])
+ log.Error("cat a unkown excetion", log.Any("excetion :", info), log.Any("e", r))
+ //发送tg消息
+ msg := "goroutine crash,the stack:\n" + info
+ tgbot.SendToProgram("程序异常", msg)
+ }
+ }()
+ f()
+ }()
+}
diff --git a/pub/common/googauth/googauth.go b/pub/common/googauth/googauth.go
new file mode 100644
index 0000000..3c086b6
--- /dev/null
+++ b/pub/common/googauth/googauth.go
@@ -0,0 +1,34 @@
+package googauth
+
+import (
+ "github.com/dgryski/dgoogauth"
+)
+
+const windowSize = 30
+const hotpWindowSize = 5
+
+var HotpCfg = dgoogauth.OTPConfig{
+ Secret: "MRSHM6LTMFYHAMJSGM2DKNQ=",
+ HotpCounter: 1,
+ WindowSize: hotpWindowSize,
+}
+
+func New(secret string, name string) string {
+ cfg := &dgoogauth.OTPConfig{
+ Secret: secret,
+ WindowSize: windowSize,
+ }
+ return cfg.ProvisionURI(name)
+}
+
+func Verify(secret string, pwd string) (bool, error) {
+ cfg := &dgoogauth.OTPConfig{
+ Secret: secret,
+ WindowSize: windowSize,
+ }
+ return cfg.Authenticate(pwd)
+}
+
+func VerifyHotp(cfg *dgoogauth.OTPConfig, authCode string, counter int) (bool, error) {
+ return cfg.Authenticate(authCode)
+}
diff --git a/pub/common/googauth/htop.go b/pub/common/googauth/htop.go
new file mode 100644
index 0000000..fa1e02b
--- /dev/null
+++ b/pub/common/googauth/htop.go
@@ -0,0 +1,67 @@
+package googauth
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/binary"
+ "strconv"
+)
+
+// HOTP implementation
+type HOTP struct {
+ secret []byte
+ digits int
+}
+
+// At generate code
+func (h HOTP) At(counter uint64) string {
+ counterBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(counterBytes, counter)
+ hash := hmac.New(sha1.New, h.secret)
+ hash.Write(counterBytes)
+ hs := hash.Sum(nil)
+ offset := hs[19] & 0x0f
+ binCodeBytes := make([]byte, 4)
+ binCodeBytes[0] = hs[offset] & 0x7f
+ binCodeBytes[1] = hs[offset+1] & 0xff
+ binCodeBytes[2] = hs[offset+2] & 0xff
+ binCodeBytes[3] = hs[offset+3] & 0xff
+ binCode := binary.BigEndian.Uint32(binCodeBytes)
+ mod := uint32(1)
+ for i := 0; i < h.digits; i++ {
+ mod *= 10
+ }
+ code := binCode % mod
+ codeString := strconv.FormatUint(uint64(code), 10)
+ if len(codeString) < h.digits {
+ paddingByteLength := h.digits - len(codeString)
+ paddingBytes := make([]byte, paddingByteLength)
+ for i := 0; i < paddingByteLength; i++ {
+ paddingBytes[i] = '0'
+ }
+ codeString = string(paddingBytes) + codeString
+ }
+ return codeString
+}
+
+// Verify verify OTP code
+func (h HOTP) VerifyCode(code string, counter uint64) bool {
+ realCode := h.At(counter)
+ if realCode == code {
+ return true
+ }
+ return false
+}
+
+// Verify verify OTP code
+func (h HOTP) GenerateCode(counter uint64) string {
+ return h.At(counter)
+}
+
+// NewHOTP generate new HOTP instance
+func NewHOTP(secret []byte, digits int) (h *HOTP) {
+ h = new(HOTP)
+ h.secret = secret
+ h.digits = digits
+ return
+}
diff --git a/pub/common/http/http.go b/pub/common/http/http.go
new file mode 100644
index 0000000..6bf8a60
--- /dev/null
+++ b/pub/common/http/http.go
@@ -0,0 +1,202 @@
+package http
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "reflect"
+ "time"
+)
+
+var (
+ client = http.Client{
+ Timeout: 30 * time.Second,
+ Transport: &http.Transport{
+ MaxIdleConnsPerHost: 5,
+ MaxConnsPerHost: 100,
+ },
+ }
+)
+
+type ByteSize int64
+
+const (
+ _ = iota // ignore first value by assigning to blank identifier
+ KB ByteSize = 1 << (10 * iota)
+ MB
+)
+
+var (
+ outOfBodySizeErr = errors.New("out of body size limit")
+)
+
+type ResponseBody struct {
+ StatusCode int
+ Data []byte
+}
+
+type RequestFormData map[string]string
+
+// Post
+// Note: 只会读取小于1M的数据
+func Post(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodPost, url, header, body, int64(1024*MB))
+ return
+}
+
+func PostJson(url string, body io.Reader) (fr *ResponseBody, err error) {
+ header := map[string]string{}
+ header["Content-Type"] = "application/json"
+ fr, err = do(http.MethodPost, url, header, body, int64(1024*KB))
+ return
+}
+
+// PostFormData 提交标准的form表单
+func PostFormData(url string, fromdata RequestFormData) (fr *ResponseBody, err error) {
+ var b bytes.Buffer
+ writer := multipart.NewWriter(&b)
+
+ for k, v := range fromdata {
+ if err = writer.WriteField(k, v); err != nil {
+ return
+ }
+ }
+ if err = writer.Close(); err != nil {
+ return
+ }
+
+ header := map[string]string{"Content-Type": writer.FormDataContentType()}
+ fr, err = do(http.MethodPost, url, header, &b, 2*int64(KB))
+ return
+}
+
+// Get
+// Note: 只会读取小于1M的数据
+func Get(url string, header map[string]string) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodGet, url, header, nil, int64(KB))
+ return
+}
+
+// GetLargeFile 获取大文件。2M
+func GetLargeFile(url string, header map[string]string) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodGet, url, header, nil, int64(20*MB))
+ return
+}
+
+func do(method string, url string, header map[string]string, body io.Reader, size int64) (fr *ResponseBody, err error) {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return
+ }
+ for k, v := range header {
+ req.Header.Set(k, v)
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.ContentLength > size {
+ err = outOfBodySizeErr
+ return
+ }
+
+ data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size))
+ if err != nil {
+ return
+ }
+ fr = &ResponseBody{
+ StatusCode: resp.StatusCode,
+ Data: data,
+ }
+ return
+}
+
+// 推送对象到文件服务器
+func PostObject(ctx context.Context, url string, header map[string]string, body []byte, objName string) (string, error) {
+ base64data := base64.StdEncoding.EncodeToString(body)
+
+ req := struct {
+ Name string `json:"fileName"`
+ Data string `json:"fileData"`
+ }{
+ Name: objName,
+ Data: base64data,
+ }
+
+ jsonBytes, _ := json.Marshal(req)
+
+ resp, err := Post(url, header, bytes.NewReader(jsonBytes))
+ if err != nil {
+ return "", err
+ }
+
+ type Obj struct {
+ Domain string `json:"domain"`
+ Name string `json:"fileName"`
+ }
+
+ fsResp := struct {
+ Code int `json:"code"`
+ Object Obj `json:"data"`
+ Msg string `json:"msg"`
+ }{}
+
+ if err = json.Unmarshal(resp.Data, &fsResp); err != nil {
+ return "", err
+ }
+
+ if fsResp.Code != 200 || fsResp.Object.Name == "" {
+ return "", fmt.Errorf("上传fs失败")
+ }
+
+ return fsResp.Object.Name, nil
+}
+
+func PostWithBind(ctx context.Context, bind interface{}, url string, body io.Reader) error {
+ if err := verifyBind(bind); err != nil {
+ return err
+ }
+ req, err := http.NewRequestWithContext(ctx, "POST", url, body)
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("response status code:%d", resp.StatusCode)
+ }
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(data, bind)
+}
+
+func verifyBind(bind interface{}) error {
+ bType := reflect.TypeOf(bind)
+ if bType.Kind() != reflect.Ptr {
+ return errors.New("bind must be a Ptr")
+ }
+ return nil
+}
+
+func GetWithBody(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodGet, url, header, body, int64(20*MB))
+ return
+}
\ No newline at end of file
diff --git a/pub/common/http/http_test.go b/pub/common/http/http_test.go
new file mode 100644
index 0000000..d99de2f
--- /dev/null
+++ b/pub/common/http/http_test.go
@@ -0,0 +1,16 @@
+package http
+
+import "fmt"
+
+func ExampleGet() {
+ urlstr := "https://palce.hzbeisheng.com/api/sms"
+ data, err := Get(urlstr, nil)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println(string(data.Data))
+
+ // Output:
+
+}
diff --git a/pub/common/httputil/httputil.go b/pub/common/httputil/httputil.go
new file mode 100644
index 0000000..bfadd58
--- /dev/null
+++ b/pub/common/httputil/httputil.go
@@ -0,0 +1,321 @@
+package httputil
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/url"
+ "strconv"
+ "videoweb/pub/common/log"
+
+ "github.com/ddliu/go-httpclient"
+)
+
+// HTTPClient htttp客户端
+type HTTPClient struct {
+ client *httpclient.HttpClient
+}
+
+type ProxyCfg struct {
+ Host string
+ Timeout int //秒
+ Source string
+}
+
+func (p *ProxyCfg) Build(rawurl string, rawHeaders map[string]string) (string, map[string]string, error) {
+ u, err := url.Parse(rawurl)
+ if err != nil {
+ return "", nil, err
+ }
+ target := url.URL{Scheme: u.Scheme, Host: u.Host}
+ u.Host = p.Host
+ u.Scheme = "http"
+ m := make(map[string]string)
+ m["X-Proxy-Target-Host"] = target.String()
+ m["X-Proxy-Source"] = "ys"
+ if p.Timeout > 0 {
+ m["X-Proxy-Timeout"] = strconv.Itoa(p.Timeout)
+ }
+ if len(rawHeaders) > 0 {
+ for k, v := range rawHeaders {
+ m[k] = v
+ }
+ }
+ return u.String(), m, nil
+}
+
+type httpClientPool struct {
+ cliPool map[string]*httpclient.HttpClient
+ poolSize int
+ offset int
+}
+
+// HTTPResponse http响应
+type HTTPResponse struct {
+ StatusCode int `json:"statusCode"`
+ Body *httpclient.Response `json:"boby"`
+}
+
+var httpCliPool = httpClientPool{cliPool: map[string]*httpclient.HttpClient{}, poolSize: 50}
+
+func New() *HTTPClient {
+ h := httpclient.NewHttpClient().Defaults(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 120,
+ })
+ return &HTTPClient{client: h}
+}
+
+func NewCtx(ctx context.Context) *HTTPClient {
+ h := httpclient.NewHttpClient().WithOptions(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 10,
+ httpclient.OPT_CONTEXT: ctx,
+ })
+ return &HTTPClient{client: h.Begin()}
+}
+
+func Begin() *HTTPClient {
+ h := httpclient.NewHttpClient().Defaults(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 60,
+ })
+ return &HTTPClient{client: h.Begin()}
+}
+
+// GetHTTPClient 获取httpclient
+func Client() *HTTPClient {
+ cliID := "CLIENT-ID-"
+ if len(httpCliPool.cliPool) < httpCliPool.poolSize {
+ h := httpclient.NewHttpClient().Defaults(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 120,
+ })
+ cliID = cliID + strconv.FormatInt(int64(len(httpCliPool.cliPool)+1), 10)
+ httpCliPool.cliPool[cliID] = h
+ httpCliPool.offset = httpCliPool.poolSize
+ return &HTTPClient{client: h}
+ }
+ if httpCliPool.offset == httpCliPool.poolSize+1 {
+ httpCliPool.offset = 1
+ }
+ cliID = cliID + strconv.FormatInt(int64(httpCliPool.offset), 10)
+ httpCliPool.offset = httpCliPool.offset + 1
+ h, ok := httpCliPool.cliPool[cliID]
+ if !ok || h == nil {
+ h = httpclient.NewHttpClient().WithOption(httpclient.OPT_TIMEOUT, 60)
+ httpCliPool.cliPool[cliID] = h
+ return &HTTPClient{client: h}
+ }
+ return &HTTPClient{client: h}
+}
+
+func jsonUnmarshalResp(bind interface{}, resp *HTTPResponse) error {
+ defer func() { _ = resp.Body.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ b, err := resp.Body.ReadAll()
+ if err != nil {
+ log.Error("httputil jsonUnmarshalResp readall err", log.E(err))
+ return err
+ }
+ err = json.Unmarshal(b, bind)
+ if err != nil {
+ log.Error("httputil jsonUnmarshalResp Unmarshal err", log.E(err))
+ }
+ return err
+}
+
+// Get Get方法
+func (h *HTTPClient) Get(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Get(url, params...)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Get method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+func (h *HTTPClient) PGet(p *ProxyCfg, url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return nil, err
+ }
+ return h.Get(purl, pheaders, params)
+}
+
+// Get GetBytes
+func (h *HTTPClient) GetBytes(url string, headers map[string]string, params ...interface{}) ([]byte, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Get(url, params...)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Get method failed %+v:", err))
+ return nil, err
+ }
+ data, err := response.ReadAll()
+ if err != nil {
+ log.ZapLog.Error("resp readAll errror", log.Any("Error", err))
+ return data, err
+ }
+ defer func() {
+ _ = response.Body.Close()
+ }()
+ return data, nil
+}
+
+// GetWithJResp 结果json.Unmarshal到bind中
+func (h *HTTPClient) GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ resp, err := h.Get(url, headers, params...)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+func (h *HTTPClient) PGetWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return 0, err
+ }
+ return h.GetWithJResp(bind, purl, pheaders, params...)
+}
+
+// Get Get方法
+func (h *HTTPClient) Delete(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Delete(url, params...)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Delete method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// DeleteWithJResp 结果json.Unmarshal到bind中
+func (h *HTTPClient) DeleteWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ resp, err := h.Delete(url, headers, params...)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+// Post Post方法
+func (h *HTTPClient) Post(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Post(url, params)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Post method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+func (h *HTTPClient) PPost(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return nil, err
+ }
+ return h.Post(purl, pheaders, params)
+}
+
+// POSTJson PostJson方法
+func (h *HTTPClient) POSTJson(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.PostJson(url, data)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client POSTJson method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// POSTJson PostJson方法, bind json.Unmarshal
+func (h *HTTPClient) POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
+ resp, err := h.POSTJson(url, headers, data)
+ if err != nil {
+ log.Error("POSTJsonWithResp err", log.E(err))
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+func (h *HTTPClient) PPOSTJsonWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
+ if p == nil {
+ return 0, fmt.Errorf("ProxyCfg is nil")
+ }
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return 0, err
+ }
+ return h.POSTJsonWithJResp(bind, purl, pheaders, data)
+}
+
+// PostMultipart PostMultipart 上传文件
+func (h *HTTPClient) PostMultipart(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.PostMultipart(url, params)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// Put 流式传输 可用于上传文件
+func (h *HTTPClient) Put(url string, headers map[string]string, body io.Reader) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Put(url, body)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// PutJSON PutJSON方法
+func (h *HTTPClient) PutJSON(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.PutJson(url, data)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// Header Header
+func (h *HTTPClient) Header(url string) (*HTTPResponse, error) {
+ response, err := h.client.Head(url)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件
+func (h *HTTPClient) POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ resp, err := h.Post(url, headers, params)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件
+func (h *HTTPClient) PPOSTWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ resp, err := h.PPost(p, bind, url, headers, params)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
diff --git a/pub/common/httputil/httputil_test.go b/pub/common/httputil/httputil_test.go
new file mode 100644
index 0000000..3c2ea11
--- /dev/null
+++ b/pub/common/httputil/httputil_test.go
@@ -0,0 +1,105 @@
+package httputil
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strconv"
+ "testing"
+)
+
+type ResponseInfo struct {
+ Gzipped bool `json:"gzipped"`
+ Method string `json:"method"`
+ Origin string `json:"origin"`
+ Useragent string `json:"user-agent"` // http://httpbin.org/user-agent
+ Form map[string]string `json:"form"`
+ Files map[string]string `json:"files"`
+ Headers map[string]string `json:"headers"`
+ Cookies map[string]string `json:"cookies"`
+}
+
+var info ResponseInfo
+
+type Resport struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data interface{} `json:"data"`
+}
+
+func Test_Get_Httpcli(t *testing.T) {
+ client := Client()
+ var headers = map[string]string{"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjZTJiNzNmZTI3Y2YyMmE4ZjQyMTVkMCIsInVzZXJOYW1lIjoidHVib2xpbmcxNjgiLCJpc0xvZ2luIjp0cnVlLCJsb2dpblRpbWUiOjE1NjQxNDYwNzI5MDUsImlhdCI6MTU2NDE0NjA3Mn0.VQICBk71KDITus83pZEq3C1y_WbKPZc0HUfmIm_eby8"}
+ respo, err := client.Get("https://no5.xiexiaojia.com/appjmall/api/showCore/appShowList", headers)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ resport := Resport{}
+ body, err := respo.Body.ReadAll()
+ //反序列化
+ err = json.Unmarshal(body, &resport)
+ fmt.Println("response", resport)
+ //案例获取数据
+ fmt.Println("response comName:", resport.Data.([]interface{})[0].(map[string]interface{})["comName"])
+}
+
+func Test_Post_Httpcli(t *testing.T) {
+ client := Client()
+ fmt.Println("http 客户端", client)
+ var headers = map[string]string{"auth_token": "xxxxx"}
+ var params = map[string]string{"userId": "5ce2b73fe27cf22a8f4215d0", "price": "5000", "count": "1", "commId": "5ce40e03e27cf22a8f4215d8",
+ "comName": "日本ceruru.b 脐带血面膜保湿补水修复5片",
+ "comImageCar": "http://oam.szbslhb.com/4,01fb06b4adf1.jpg",
+ "attributeCar": "",
+ }
+ respo, err := client.Post("https://no5.xiexiaojia.com/appjmall/api/cart/addCommToCart", headers, params)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ resport := Resport{}
+ body, err := respo.Body.ReadAll()
+ if err != nil {
+ res, _ := respo.Body.ToString()
+ fmt.Println("response", res)
+ return
+ }
+ //反序列化
+ err = json.Unmarshal(body, &resport)
+ fmt.Println("response", resport)
+}
+
+func Test_PostJson_Httpcli(t *testing.T) {
+ client := Client()
+ var headers = map[string]string{"Content-Type": "application/json"}
+ fmt.Println("http 客户端", client)
+ respo, err := client.Get("https://www.kancloud.cn/cattong/go_command_tutorial/261347", headers)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ body, err := respo.Body.ToString()
+ fmt.Println("response", body)
+}
+
+func Test_PutFile_Httpcli(t *testing.T) {
+ client := Client()
+ file, err := os.Open("/Users/mac/Downloads/1567069549ABBQCTFC.mp4")
+ info, _ := file.Stat()
+
+ var headers = map[string]string{
+ "Content-Type": "application/octet-stream",
+ "Authorization": "bucketName=bucket-01;accessKey=FSd58ed59fccc16812903c37068de8969b;ttl=1h",
+ "Content-Disposition": "fileName=test02.mp4;size=" + strconv.FormatInt(info.Size(), 10),
+ "Signature": "9cea21bf95f235e7d9c5a70888cf742cfb9c946ed25aef8b70ce77c953ccbff0",
+ }
+ fmt.Println("http 客户端", client)
+ respo, err := client.Put("http://localhost:8888/fs/api/uploadStream", headers, file)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ body, err := respo.Body.ToString()
+ fmt.Println("response", body)
+}
diff --git a/pub/common/imgutil/decrypt.go b/pub/common/imgutil/decrypt.go
new file mode 100644
index 0000000..95565d6
--- /dev/null
+++ b/pub/common/imgutil/decrypt.go
@@ -0,0 +1,74 @@
+package imgutil
+
+import "bytes"
+
+// 与管理后台 ShowImg / imgCode.js 保持一致。
+var (
+ encryptMagicNumber = []byte{0x88, 0xA8, 0x30, 0xCB, 0x10, 0x76}
+ decryptKey = []byte{50, 48, 49, 57, 121, 115, 97, 112, 112, 55, 53, 50, 55} // 2019ysapp7527
+ plainImageHeaders = [][]byte{
+ {0xFF, 0xD8, 0xFF}, // jpeg
+ {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, // png
+ {0x47, 0x49, 0x46}, // gif
+ }
+)
+
+const (
+ encryptedPrefixLen = 100
+ encryptXORAllKey = 0xA3
+)
+
+// DecryptImage 按后台 handleVerAutoImg 的规则解密图床图片。
+// 已是 jpg/png/gif 则原样返回。
+func DecryptImage(data []byte) []byte {
+ if len(data) == 0 {
+ return data
+ }
+ out := append([]byte(nil), data...)
+ if hasEncryptMagic(out) {
+ return xorAllLength(out)
+ }
+ if !isPlainImage(out) {
+ xorPrefix(out, decryptKey, encryptedPrefixLen)
+ }
+ return out
+}
+
+func hasEncryptMagic(data []byte) bool {
+ return len(data) >= len(encryptMagicNumber) && bytes.Equal(data[:len(encryptMagicNumber)], encryptMagicNumber)
+}
+
+func isPlainImage(data []byte) bool {
+ for _, header := range plainImageHeaders {
+ if len(data) >= len(header) && bytes.Equal(data[:len(header)], header) {
+ return true
+ }
+ }
+ return false
+}
+
+func xorPrefix(src, key []byte, length int) {
+ if len(key) == 0 || len(src) == 0 {
+ return
+ }
+ if length <= 0 || length > len(src) {
+ length = len(src)
+ }
+ for i := 0; i < length; {
+ for j := 0; j < len(key) && i < length; j++ {
+ src[i] ^= key[j]
+ i++
+ }
+ }
+}
+
+func xorAllLength(src []byte) []byte {
+ out := make([]byte, 0, len(src))
+ for i, b := range src {
+ if i < len(encryptMagicNumber) && b == encryptMagicNumber[i] {
+ continue
+ }
+ out = append(out, b^encryptXORAllKey)
+ }
+ return out
+}
diff --git a/pub/common/imgutil/resize.go b/pub/common/imgutil/resize.go
new file mode 100644
index 0000000..d7960ae
--- /dev/null
+++ b/pub/common/imgutil/resize.go
@@ -0,0 +1,151 @@
+package imgutil
+
+import (
+ "bytes"
+ "crypto/sha1"
+ "fmt"
+ "image"
+ _ "image/gif"
+ "image/jpeg"
+ _ "image/png"
+ "net/url"
+ "path"
+ "path/filepath"
+ "strings"
+ "unicode"
+)
+
+const (
+ // CoverResizeLimitKB 与 media utils.Resize 一致:超过该大小才缩放
+ CoverResizeLimitKB = 60
+ // CoverMaxWidth 与 media Thumbnail 一致
+ CoverMaxWidth = 750
+ // CoverMaxHeight 与 media Thumbnail 一致
+ CoverMaxHeight = 1560
+ // CoverJPEGQuality 压缩后统一输出 JPEG
+ CoverJPEGQuality = 75
+)
+
+// IsGIF 按文件头判断是否为 GIF,不依赖扩展名。
+func IsGIF(data []byte) bool {
+ return len(data) >= 3 && bytes.Equal(data[:3], []byte{0x47, 0x49, 0x46})
+}
+
+// NeedCompress 原图是否超过 media Resize 阈值,未超过则无需压缩重传。
+// GIF 保持原格式和动效,不参与压缩。
+func NeedCompress(data []byte) bool {
+ if IsGIF(data) {
+ return false
+ }
+ return int64(len(data))/1024 > CoverResizeLimitKB
+}
+
+// CompressCover 按 media Resize(60KB, 750×1560) 压缩封面。
+// GIF 原样返回;其它格式未超过阈值时原样返回,超过则缩放后统一输出 JPEG。
+func CompressCover(data []byte, fileName string) ([]byte, string, error) {
+ if len(data) == 0 {
+ return nil, "", fmt.Errorf("图片内容为空")
+ }
+ outName := compressFileName(fileName)
+ if IsGIF(data) {
+ return data, withExt(outName, ".gif"), nil
+ }
+ if !NeedCompress(data) {
+ return data, outName, nil
+ }
+ img, _, err := image.Decode(bytes.NewReader(data))
+ if err != nil {
+ return nil, "", fmt.Errorf("解码图片失败: %w", err)
+ }
+ thumb := thumbnail(img, CoverMaxWidth, CoverMaxHeight)
+ var buf bytes.Buffer
+ if err = jpeg.Encode(&buf, thumb, &jpeg.Options{Quality: CoverJPEGQuality}); err != nil {
+ return nil, "", err
+ }
+ return buf.Bytes(), withExt(outName, ".jpg"), nil
+}
+
+// SafeCoverFileName 生成仅含 ASCII 的上传文件名。
+// 原封面路径含中文时,直接用 path.Base 会把中文带进上传名,图床返回地址会异常。
+func SafeCoverFileName(src string) string {
+ src = strings.TrimSpace(src)
+ ext := asciiImageExt(src)
+ if src == "" {
+ return "playlet_cover" + ext
+ }
+ sum := sha1.Sum([]byte(src))
+ return fmt.Sprintf("playlet_cover_%x%s", sum[:8], ext)
+}
+
+func thumbnail(img image.Image, maxW, maxH int) image.Image {
+ b := img.Bounds()
+ w, h := b.Dx(), b.Dy()
+ if w <= maxW && h <= maxH {
+ return img
+ }
+ wr := float64(maxW) / float64(w)
+ hr := float64(maxH) / float64(h)
+ scale := wr
+ if hr < wr {
+ scale = hr
+ }
+ nw := int(float64(w) * scale)
+ nh := int(float64(h) * scale)
+ if nw < 1 {
+ nw = 1
+ }
+ if nh < 1 {
+ nh = 1
+ }
+ dst := image.NewRGBA(image.Rect(0, 0, nw, nh))
+ for y := 0; y < nh; y++ {
+ sy := b.Min.Y + y*h/nh
+ for x := 0; x < nw; x++ {
+ sx := b.Min.X + x*w/nw
+ dst.Set(x, y, img.At(sx, sy))
+ }
+ }
+ return dst
+}
+
+func compressFileName(fileName string) string {
+ fileName = filepath.Base(strings.TrimSpace(fileName))
+ if fileName == "" || fileName == "." || fileName == string(filepath.Separator) || !isASCIIFileName(fileName) {
+ return "playlet_cover.jpg"
+ }
+ return fileName
+}
+
+func asciiImageExt(src string) string {
+ name := src
+ if u, err := url.Parse(src); err == nil && u.Path != "" {
+ name = u.Path
+ }
+ name = strings.Split(path.Base(name), "?")[0]
+ if dec, err := url.PathUnescape(name); err == nil {
+ name = dec
+ }
+ ext := strings.ToLower(filepath.Ext(name))
+ switch ext {
+ case ".jpg", ".jpeg", ".png", ".gif", ".webp":
+ return ext
+ default:
+ return ".jpg"
+ }
+}
+
+func isASCIIFileName(name string) bool {
+ if !strings.Contains(name, ".") {
+ return false
+ }
+ for _, r := range name {
+ if r > unicode.MaxASCII || r < 32 || r == '"' || r == '/' || r == '\\' {
+ return false
+ }
+ }
+ return true
+}
+
+func withExt(fileName, ext string) string {
+ return strings.TrimSuffix(fileName, filepath.Ext(fileName)) + ext
+}
diff --git a/pub/common/ipaddr/ip.go b/pub/common/ipaddr/ip.go
new file mode 100644
index 0000000..0568f83
--- /dev/null
+++ b/pub/common/ipaddr/ip.go
@@ -0,0 +1,420 @@
+package ipaddr
+
+import (
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/lionsoul2014/ip2region/binding/golang/xdb"
+)
+
+// XDBDetector 基于 xdb 的海外IP检测器
+type XDBDetector struct {
+ searcher *xdb.Searcher
+ cache sync.Map
+ cacheTTL time.Duration
+}
+
+// RegionInfo 区域信息
+type RegionInfo struct {
+ Country string `json:"country"`
+ Region string `json:"region"`
+ Province string `json:"province"`
+ City string `json:"city"`
+ ISP string `json:"isp"`
+ IsOverseas bool `json:"is_overseas"`
+ IsChinaMainland bool `json:"is_china_mainland"`
+}
+
+type IPVersion string
+
+const (
+ IPv4 IPVersion = "IPv4"
+ IPv6 IPVersion = "IPv6"
+ InvalidIP IPVersion = "Invalid"
+)
+
+// GetIPVersion 获取IP版本
+func GetIPVersion(ip string) IPVersion {
+ parsedIP := net.ParseIP(ip)
+ if parsedIP == nil {
+ return InvalidIP
+ }
+
+ if parsedIP.To4() != nil {
+ return IPv4
+ }
+ return IPv6
+}
+
+// IsIPv4 判断是否为IPv4
+func IsIPv4(ip string) bool {
+ return GetIPVersion(ip) == IPv4
+}
+
+// NewXDBDetector 创建 xdb 检测器
+func NewXDBDetectorV6(dbPath string) (*XDBDetector, error) {
+ // 自动查找数据库文件
+ if dbPath == "" {
+ dbPath = findXDBFile()
+ if dbPath == "" {
+ return nil, fmt.Errorf("未找到 ip2region.xdb 数据库文件")
+ }
+ }
+
+ // 检查文件是否存在
+ if _, err := os.Stat(dbPath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("数据库文件不存在: %s", dbPath)
+ }
+
+ // 读取整个 xdb 文件到内存
+ cBuff, err := xdb.LoadContentFromFile(dbPath)
+ if err != nil {
+ return nil, fmt.Errorf("加载 xdb 内容失败: %v", err)
+ }
+
+ // 创建搜索器
+ searcher, err := xdb.NewWithBuffer(xdb.IPv6, cBuff)
+ if err != nil {
+ return nil, fmt.Errorf("创建搜索器失败: %v", err)
+ }
+
+ detector := &XDBDetector{
+ searcher: searcher,
+ cacheTTL: 24 * time.Hour,
+ }
+
+ log.Printf("XDBDetector 初始化成功,数据库: %s", dbPath)
+ return detector, nil
+}
+
+// NewXDBDetector 创建 xdb 检测器
+func NewXDBDetectorV4(dbPath string) (*XDBDetector, error) {
+ // 自动查找数据库文件
+ if dbPath == "" {
+ dbPath = findXDBFile()
+ if dbPath == "" {
+ return nil, fmt.Errorf("未找到 ip2region.xdb 数据库文件")
+ }
+ }
+
+ // 检查文件是否存在
+ if _, err := os.Stat(dbPath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("数据库文件不存在: %s", dbPath)
+ }
+
+ // 读取整个 xdb 文件到内存
+ cBuff, err := xdb.LoadContentFromFile(dbPath)
+ if err != nil {
+ return nil, fmt.Errorf("加载 xdb 内容失败: %v", err)
+ }
+
+ // 创建搜索器
+ searcher, err := xdb.NewWithBuffer(xdb.IPv4, cBuff)
+ if err != nil {
+ return nil, fmt.Errorf("创建搜索器失败: %v", err)
+ }
+
+ detector := &XDBDetector{
+ searcher: searcher,
+ cacheTTL: 24 * time.Hour,
+ }
+
+ log.Printf("XDBDetector 初始化成功,数据库: %s", dbPath)
+ return detector, nil
+}
+
+// Search 搜索IP信息
+func (d *XDBDetector) Search(ip string) (string, error) {
+ if d.searcher == nil {
+ return "", fmt.Errorf("搜索器未初始化")
+ }
+
+ // 验证IP格式
+ if net.ParseIP(ip) == nil {
+ return "", fmt.Errorf("无效的IP地址: %s", ip)
+ }
+
+ return d.searcher.SearchByStr(ip)
+}
+
+// SearchRegion 搜索并解析区域信息
+func (d *XDBDetector) SearchRegion(ip string) (*RegionInfo, error) {
+ regionStr, err := d.Search(ip)
+ if err != nil {
+ return nil, err
+ }
+
+ return d.parseRegionString(regionStr), nil
+}
+
+// parseRegionString 解析区域字符串
+// 格式: 国家|区域|省份|城市|ISP
+func (d *XDBDetector) parseRegionString(regionStr string) *RegionInfo {
+ parts := strings.Split(regionStr, "|")
+
+ // 确保有足够的字段
+ for len(parts) < 5 {
+ parts = append(parts, "")
+ }
+
+ country := strings.TrimSpace(parts[0])
+ region := strings.TrimSpace(parts[1])
+ province := strings.TrimSpace(parts[2])
+ city := strings.TrimSpace(parts[3])
+ isp := strings.TrimSpace(parts[4])
+
+ isChinaMainland := d.isChinaMainland(country)
+ isOverseas := !isChinaMainland
+
+ // 特别行政区处理
+ if !isChinaMainland && d.isChinaSpecialRegion(country) {
+ // 可以根据业务需求调整特别行政区是否视为海外
+ isOverseas = false
+ }
+
+ return &RegionInfo{
+ Country: country,
+ Region: region,
+ Province: province,
+ City: city,
+ ISP: isp,
+ IsOverseas: isOverseas,
+ IsChinaMainland: isChinaMainland,
+ }
+}
+
+// IsOverseasIP 判断是否为海外IP
+func (d *XDBDetector) IsOverseasIP(ip string) (bool, error) {
+ // 检查缓存
+ if cached, ok := d.getFromCache(ip); ok {
+ return cached, nil
+ }
+
+ // 内网IP默认为国内
+ if d.isPrivateIP(ip) {
+ d.setCache(ip, false)
+ return false, nil
+ }
+
+ regionInfo, err := d.SearchRegion(ip)
+ if err != nil {
+ return false, err
+ }
+
+ d.setCache(ip, regionInfo.IsOverseas)
+ return regionInfo.IsOverseas, nil
+}
+
+// isChinaMainland 判断是否为中国大陆
+func (d *XDBDetector) isChinaMainland(country string) bool {
+ if country == "" {
+ return false
+ }
+
+ chinaKeywords := []string{"中国", "China", "CN"}
+ for _, keyword := range chinaKeywords {
+ if country == keyword {
+ return true
+ }
+ }
+
+ lowerCountry := strings.ToLower(country)
+ for _, keyword := range []string{"china", "cn", "中国"} {
+ if strings.Contains(lowerCountry, keyword) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// isChinaSpecialRegion 判断是否为中国特别行政区
+func (d *XDBDetector) isChinaSpecialRegion(region string) bool {
+ specialRegions := []string{
+ "台湾", "台湾省", "Taiwan", "TW",
+ "香港", "Hong Kong", "HK",
+ "澳门", "Macao", "Macau", "MO",
+ }
+
+ for _, keyword := range specialRegions {
+ if region == keyword {
+ return true
+ }
+ }
+
+ return false
+}
+
+// BatchIsOverseas 批量判断海外IP
+func (d *XDBDetector) BatchIsOverseas(ips []string) (map[string]bool, error) {
+ results := make(map[string]bool)
+
+ for _, ip := range ips {
+ isOverseas, err := d.IsOverseasIP(ip)
+ if err != nil {
+ // 失败时默认为海外(安全策略)
+ results[ip] = true
+ continue
+ }
+ results[ip] = isOverseas
+ }
+
+ return results, nil
+}
+
+// isPrivateIP 判断内网IP
+func (d *XDBDetector) isPrivateIP(ip string) bool {
+ parsedIP := net.ParseIP(ip)
+ if parsedIP == nil {
+ return false
+ }
+
+ privateBlocks := []string{
+ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
+ "127.0.0.0/8", "169.254.0.0/16",
+ "::1/128", "fc00::/7", "fe80::/10",
+ }
+
+ for _, block := range privateBlocks {
+ _, ipNet, err := net.ParseCIDR(block)
+ if err == nil && ipNet.Contains(parsedIP) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// 缓存管理
+func (d *XDBDetector) getFromCache(ip string) (bool, bool) {
+ if item, ok := d.cache.Load(ip); ok {
+ if cacheItem, ok := item.(*cacheResult); ok {
+ if time.Since(cacheItem.timestamp) < d.cacheTTL {
+ return cacheItem.isOverseas, true
+ }
+ d.cache.Delete(ip)
+ }
+ }
+ return false, false
+}
+
+func (d *XDBDetector) setCache(ip string, isOverseas bool) {
+ d.cache.Store(ip, &cacheResult{
+ isOverseas: isOverseas,
+ timestamp: time.Now(),
+ })
+}
+
+type cacheResult struct {
+ isOverseas bool
+ timestamp time.Time
+}
+
+// Close 关闭检测器
+func (d *XDBDetector) Close() {
+ if d.searcher != nil {
+ d.searcher.Close()
+ }
+}
+
+// findXDBFile 查找 xdb 文件
+func findXDBFile() string {
+ paths := []string{
+ "ip2region.xdb",
+ "data/ip2region.xdb",
+ "./ip2region.xdb",
+ "../ip2region.xdb",
+ "/usr/local/share/ip2region/ip2region.xdb",
+ }
+
+ for _, path := range paths {
+ if _, err := os.Stat(path); err == nil {
+ return path
+ }
+ }
+ return ""
+}
+
+// ResolveClientIP 解析客户端 IP,用于 m3u8 限流、token 签发与 ipstat 统计。
+// CDN/LB 链路下 Gin ClientIP 常返回离源站最近的节点 IP(如 CloudFront);
+// 此时优先取 X-Forwarded-For 最左侧公网 IP(真实访客)。
+func ResolveClientIP(c *gin.Context) string {
+ if c == nil {
+ return ""
+ }
+ remoteIP := normalizeIPString(c.RemoteIP())
+ clientIP := normalizeIPString(c.ClientIP())
+
+ if shouldTrustForwardedHeaders(remoteIP, clientIP) {
+ if ip := firstPublicIPFromHeader(c.GetHeader("CF-Connecting-IP")); ip != "" {
+ return ip
+ }
+ if ip := firstPublicIPFromHeader(c.GetHeader("X-Forwarded-For")); ip != "" {
+ return ip
+ }
+ if ip := normalizeIPString(c.GetHeader("X-Real-IP")); ip != "" && !isPrivateOrLoopbackIP(ip) {
+ return ip
+ }
+ }
+
+ if clientIP != "" && !isPrivateOrLoopbackIP(clientIP) {
+ return clientIP
+ }
+ if clientIP != "" {
+ return clientIP
+ }
+ return remoteIP
+}
+
+// shouldTrustForwardedHeaders 判断是否处于反代/CDN 链路,可信任 XFF/X-Real-IP。
+// 公网直连时不信任,避免客户端伪造 XFF。
+func shouldTrustForwardedHeaders(remoteIP, clientIP string) bool {
+ if isPrivateOrLoopbackIP(remoteIP) {
+ return true
+ }
+ // Gin 从 XFF 解析出的 ClientIP 与 TCP 直连 IP 不同,说明经过了 LB/CDN。
+ return clientIP != "" && remoteIP != "" && clientIP != remoteIP
+}
+
+func normalizeIPString(raw string) string {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return ""
+ }
+ ip := net.ParseIP(raw)
+ if ip == nil {
+ return raw
+ }
+ if v4 := ip.To4(); v4 != nil {
+ return v4.String()
+ }
+ return ip.String()
+}
+
+func firstPublicIPFromHeader(xff string) string {
+ xff = strings.TrimSpace(xff)
+ if xff == "" {
+ return ""
+ }
+ for _, part := range strings.Split(xff, ",") {
+ ip := normalizeIPString(strings.TrimSpace(part))
+ if ip != "" && !isPrivateOrLoopbackIP(ip) {
+ return ip
+ }
+ }
+ return ""
+}
+
+func isPrivateOrLoopbackIP(ipStr string) bool {
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return false
+ }
+ return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
+}
diff --git a/pub/common/laosiji/client.go b/pub/common/laosiji/client.go
new file mode 100644
index 0000000..d58bb5c
--- /dev/null
+++ b/pub/common/laosiji/client.go
@@ -0,0 +1,115 @@
+package laosiji
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+)
+
+var (
+ // LaosijiDomains 老司机上传凭证缓存。由 web server 初始化。
+ LaosijiDomains *Domains
+ // LaosijiClient 老司机业务 API 客户端。由 web server 初始化。
+ LaosijiClient *Client
+)
+
+// Client 老司机「业务 API」客户端(区别于上传站 upload_url)。
+type Client struct {
+ baseURL string
+ appid string
+ appkey string
+ hc *http.Client
+}
+
+// New 创建业务 API 客户端。baseURL 含 /lsjapi 前缀。
+func New(baseURL, appid, appkey string) *Client {
+ return &Client{
+ baseURL: strings.TrimRight(baseURL, "/"),
+ appid: appid,
+ appkey: appkey,
+ hc: &http.Client{Timeout: 40 * time.Second},
+ }
+}
+
+// SetHTTPClient 替换底层 http.Client(测试/自定义超时用)。
+func (c *Client) SetHTTPClient(hc *http.Client) { c.hc = hc }
+
+type apiResp struct {
+ Status string `json:"status"`
+ Data string `json:"data"`
+ Error string `json:"error"`
+}
+
+// ErrEmptyData status=y 但 data 为空。
+var ErrEmptyData = errors.New("laosiji: empty response data")
+
+// Call 发起一次业务 API 调用。
+func (c *Client) Call(ctx context.Context, path string, req, out interface{}) error {
+ reqJSON := []byte("{}")
+ if req != nil {
+ b, err := json.Marshal(req)
+ if err != nil {
+ return fmt.Errorf("laosiji: marshal req: %w", err)
+ }
+ reqJSON = b
+ }
+ encBody, err := EncryptBase64(reqJSON, c.appkey)
+ if err != nil {
+ return fmt.Errorf("laosiji: encrypt req: %w", err)
+ }
+
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, strings.NewReader(encBody))
+ if err != nil {
+ return err
+ }
+ httpReq.Header.Set("appid", c.appid)
+ httpReq.Header.Set("Content-Type", "application/json")
+
+ resp, err := c.hc.Do(httpReq)
+ if err != nil {
+ return fmt.Errorf("laosiji: http do: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("laosiji: read resp: %w", err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("laosiji: http %d: %s", resp.StatusCode, truncate(body, 200))
+ }
+
+ var outer apiResp
+ if err := json.Unmarshal(body, &outer); err != nil {
+ return fmt.Errorf("laosiji: unmarshal outer: %w (body=%s)", err, truncate(body, 200))
+ }
+ if outer.Status != "y" {
+ return fmt.Errorf("laosiji: api status=%q error=%q", outer.Status, outer.Error)
+ }
+ if out == nil {
+ return nil
+ }
+ if outer.Data == "" {
+ return ErrEmptyData
+ }
+ plain, err := DecryptBase64(outer.Data, c.appkey)
+ if err != nil {
+ return fmt.Errorf("laosiji: decrypt data: %w", err)
+ }
+ if err := json.Unmarshal(plain, out); err != nil {
+ return fmt.Errorf("laosiji: unmarshal data: %w (plain=%s)", err, truncate(plain, 200))
+ }
+ return nil
+}
+
+func truncate(b []byte, n int) string {
+ if len(b) > n {
+ return string(b[:n]) + "..."
+ }
+ return string(b)
+}
diff --git a/pub/common/laosiji/crypto.go b/pub/common/laosiji/crypto.go
new file mode 100644
index 0000000..e9550fd
--- /dev/null
+++ b/pub/common/laosiji/crypto.go
@@ -0,0 +1,89 @@
+package laosiji
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/base64"
+ "errors"
+)
+
+var (
+ ErrInvalidCipher = errors.New("laosiji crypto: invalid ciphertext length")
+ ErrInvalidPadding = errors.New("laosiji crypto: invalid pkcs7 padding")
+)
+
+func AesKey(appkey string) []byte {
+ key := make([]byte, 16)
+ copy(key, appkey)
+ return key
+}
+
+func EncryptBase64(plain []byte, appkey string) (string, error) {
+ block, err := aes.NewCipher(AesKey(appkey))
+ if err != nil {
+ return "", err
+ }
+ padded := pkcs7Pad(plain, block.BlockSize())
+ out := make([]byte, len(padded))
+ ecbEncrypt(block, out, padded)
+ return base64.StdEncoding.EncodeToString(out), nil
+}
+
+func DecryptBase64(b64 string, appkey string) ([]byte, error) {
+ ct, err := base64.StdEncoding.DecodeString(b64)
+ if err != nil {
+ return nil, err
+ }
+ block, err := aes.NewCipher(AesKey(appkey))
+ if err != nil {
+ return nil, err
+ }
+ bs := block.BlockSize()
+ if len(ct) == 0 || len(ct)%bs != 0 {
+ return nil, ErrInvalidCipher
+ }
+ out := make([]byte, len(ct))
+ ecbDecrypt(block, out, ct)
+ return pkcs7Unpad(out, bs)
+}
+
+func ecbEncrypt(block cipher.Block, dst, src []byte) {
+ bs := block.BlockSize()
+ for len(src) > 0 {
+ block.Encrypt(dst[:bs], src[:bs])
+ src = src[bs:]
+ dst = dst[bs:]
+ }
+}
+
+func ecbDecrypt(block cipher.Block, dst, src []byte) {
+ bs := block.BlockSize()
+ for len(src) > 0 {
+ block.Decrypt(dst[:bs], src[:bs])
+ src = src[bs:]
+ dst = dst[bs:]
+ }
+}
+
+func pkcs7Pad(data []byte, blockSize int) []byte {
+ pad := blockSize - len(data)%blockSize
+ return append(data, bytes.Repeat([]byte{byte(pad)}, pad)...)
+}
+
+func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
+ n := len(data)
+ if n == 0 || n%blockSize != 0 {
+ return nil, ErrInvalidPadding
+ }
+ pad := int(data[n-1])
+ if pad == 0 || pad > blockSize {
+ return nil, ErrInvalidPadding
+ }
+ for i := n - pad; i < n; i++ {
+ if int(data[i]) != pad {
+ return nil, ErrInvalidPadding
+ }
+ }
+ return data[:n-pad], nil
+}
diff --git a/pub/common/laosiji/domains.go b/pub/common/laosiji/domains.go
new file mode 100644
index 0000000..6c818db
--- /dev/null
+++ b/pub/common/laosiji/domains.go
@@ -0,0 +1,77 @@
+package laosiji
+
+import (
+ "context"
+ "sync"
+ "time"
+)
+
+const SystemDomainsPath = "/lsjapi/system/domains"
+
+type SystemDomains struct {
+ ImgFreeCdn string `json:"img_free_cdn"`
+ ImgVipCdn string `json:"img_vip_cdn"`
+ ImgCnCdn string `json:"img_cn_cdn"`
+ ImgSourceCdn string `json:"img_source_cdn"`
+ MovieFreeCdn string `json:"movie_free_cdn"`
+ MovieVipCdn string `json:"movie_vip_cdn"`
+ MovieCnCdn string `json:"movie_cn_cdn"`
+ MovieSourceCdn string `json:"movie_source_cdn"`
+ UploadURL string `json:"upload_url"`
+ MediaDir string `json:"media_dir"`
+ UploadKey string `json:"upload_key"`
+ UserID string `json:"user_id"`
+}
+
+type Domains struct {
+ client *Client
+
+ mu sync.RWMutex
+ cur *SystemDomains
+ fetchedAt time.Time
+}
+
+func NewDomains(client *Client) *Domains {
+ return &Domains{client: client}
+}
+
+func (d *Domains) Refresh(ctx context.Context) (*SystemDomains, error) {
+ var sd SystemDomains
+ if err := d.client.Call(ctx, SystemDomainsPath, map[string]string{}, &sd); err != nil {
+ return nil, err
+ }
+ d.mu.Lock()
+ d.cur = &sd
+ d.fetchedAt = time.Now()
+ d.mu.Unlock()
+ return &sd, nil
+}
+
+func (d *Domains) Get(ctx context.Context) (*SystemDomains, error) {
+ d.mu.RLock()
+ cur := d.cur
+ d.mu.RUnlock()
+ if cur != nil {
+ return cur, nil
+ }
+ return d.Refresh(ctx)
+}
+
+func (d *Domains) StartAutoRefresh(interval time.Duration) (stop func()) {
+ done := make(chan struct{})
+ go func() {
+ t := time.NewTicker(interval)
+ defer t.Stop()
+ for {
+ select {
+ case <-done:
+ return
+ case <-t.C:
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ _, _ = d.Refresh(ctx)
+ cancel()
+ }
+ }
+ }()
+ return func() { close(done) }
+}
diff --git a/pub/common/laosiji/transcoding.go b/pub/common/laosiji/transcoding.go
new file mode 100644
index 0000000..bfaa00e
--- /dev/null
+++ b/pub/common/laosiji/transcoding.go
@@ -0,0 +1,292 @@
+package laosiji
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+const (
+ videoTranscodingVersion = "2.0"
+ transcodeSuccessStatus = 5
+ transcodeNoNeedStatus = 4
+ transcodeFailedStatus = -1
+ transcodeQueueLimit = 20
+)
+
+var errTranscodeTaskNotFound = errors.New("laosiji transcode task not found")
+
+type uploadAPIResp struct {
+ Status string `json:"status"`
+ Data json.RawMessage `json:"data"`
+ Time string `json:"time"`
+ Error string `json:"error"`
+ ErrorCode int `json:"errorCode"`
+}
+
+type transcodeTask struct {
+ ID string `json:"id"`
+ FileID string `json:"file_id"`
+ FileURL string `json:"file_url"`
+ Status int `json:"status"`
+ Duration int `json:"duration"`
+ Height int `json:"height"`
+ Width int `json:"width"`
+ TranscodeError string `json:"transcode_error"`
+ TranscodeFile string `json:"transcode_file"`
+ PreviewVtt string `json:"preview_vtt"`
+ AudioFile string `json:"audio_file"`
+ SmartCover string `json:"smart_cover"`
+ PreviewDir string `json:"preview_dir"`
+ SrcMetaInfo *MetaInfo `json:"src_meta_info"`
+ HevcMetaInfo *MetaInfo `json:"hevc_meta_info"`
+}
+
+// MetaInfo 视频元信息(src_meta_info / hevc_meta_info 共用结构)。
+type MetaInfo struct {
+ Codec string `json:"codec"`
+ Profile string `json:"profile"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Fps float64 `json:"fps"`
+ VideoBitrate int `json:"video_bitrate"`
+ PixFmt string `json:"pix_fmt"`
+ AudioCodec string `json:"audio_codec"`
+ AudioBitrate int `json:"audio_bitrate"`
+ AudioSampleRate int `json:"audio_sample_rate"`
+ AudioChannels int `json:"audio_channels"`
+ Bitrate int `json:"bitrate"`
+ Size int64 `json:"size"`
+ Duration float64 `json:"duration"`
+}
+
+type transcodeQueueInfo struct {
+ Waiting int `json:"waiting"`
+ Done int `json:"done"`
+ Error int `json:"error"`
+}
+
+// TranscodeResult 转码任务状态快照。
+type TranscodeResult struct {
+ Status int
+ Done bool
+ Failed bool
+ HevcURL string
+ PreviewVtt string
+ AudioFile string
+ SmartCover string
+ HevcMeta *MetaInfo
+}
+
+// ResolveTranscodeFileID 优先使用媒资 fileId,缺失时回退为 fileURL 的 md5。
+func ResolveTranscodeFileID(fileID, fileURL string) string {
+ if id := strings.TrimSpace(fileID); id != "" {
+ return id
+ }
+ return md5Hex(fileURL)
+}
+
+func (c *Client) SubmitTranscoding(ctx context.Context, fileURL, fileID string, domains SystemDomains, videoId int64) (TranscodeResult, error) {
+ fileURL = strings.TrimSpace(fileURL)
+ if fileURL == "" {
+ return TranscodeResult{}, fmt.Errorf("empty transcode source url")
+ }
+ fileID = ResolveTranscodeFileID(fileID, fileURL)
+
+ task, err := c.queryVideoTranscoding(ctx, domains, fileID)
+ if err == nil {
+ return taskToResult(task), nil
+ }
+ if !errors.Is(err, errTranscodeTaskNotFound) {
+ return TranscodeResult{}, err
+ }
+ task, err = c.createVideoTranscoding(ctx, domains, fileID, fileURL, videoId)
+ if err != nil {
+ return TranscodeResult{}, err
+ }
+ return taskToResult(task), nil
+}
+
+func (c *Client) QueryTranscoding(ctx context.Context, fileURL, fileID string, domains SystemDomains) (result TranscodeResult, found bool, err error) {
+ fileURL = strings.TrimSpace(fileURL)
+ if fileURL == "" {
+ return TranscodeResult{}, false, fmt.Errorf("empty transcode source url")
+ }
+ fileID = ResolveTranscodeFileID(fileID, fileURL)
+
+ task, err := c.queryVideoTranscoding(ctx, domains, fileID)
+ if err != nil {
+ if errors.Is(err, errTranscodeTaskNotFound) {
+ return TranscodeResult{}, false, nil
+ }
+ return TranscodeResult{}, false, err
+ }
+ return taskToResult(task), true, nil
+}
+
+func (c *Client) createVideoTranscoding(ctx context.Context, domains SystemDomains, fileID, fileURL string, videoId int64) (transcodeTask, error) {
+ if q, err := c.getVideoTranscodingQueue(ctx, domains); err == nil && q.Waiting >= transcodeQueueLimit {
+ return transcodeTask{}, fmt.Errorf("h264toh265 queue full waiting:%d limit:%d", q.Waiting, transcodeQueueLimit)
+ }
+ endpoint := joinURLPath(domains.UploadURL, "upload/videoTranscoding")
+ extData, _ := json.Marshal(map[string]interface{}{
+ "project": "ttrgw",
+ "url": fileURL,
+ "type": "h265",
+ "videoId": videoId,
+ })
+ return c.uploadAPIGet(ctx, endpoint, map[string]string{
+ "v": videoTranscodingVersion,
+ "key": domains.UploadKey,
+ "file_id": fileID,
+ "file_url": fileURL,
+ "notice_url": "",
+ "ext_data": string(extData),
+ })
+}
+
+func (c *Client) queryVideoTranscoding(ctx context.Context, domains SystemDomains, fileID string) (transcodeTask, error) {
+ endpoint := joinURLPath(domains.UploadURL, "upload/queryVideoTranscoding")
+ return c.uploadAPIGet(ctx, endpoint, map[string]string{
+ "v": videoTranscodingVersion,
+ "key": domains.UploadKey,
+ "file_id": fileID,
+ })
+}
+
+func taskToResult(task transcodeTask) TranscodeResult {
+ r := TranscodeResult{Status: task.Status}
+ switch {
+ case task.Status == transcodeSuccessStatus || task.Status == transcodeNoNeedStatus:
+ if task.Status == transcodeSuccessStatus {
+ if f := strings.TrimSpace(task.TranscodeFile); f != "" {
+ r.Done = true
+ r.HevcURL = joinURLPath("/laosiji/m3m", f)
+ }
+ } else {
+ r.Done = true
+ }
+ if r.Done {
+ r.PreviewVtt = laosijiPreviewURL(task.PreviewDir, task.PreviewVtt)
+ r.AudioFile = laosijiURL(task.AudioFile)
+ r.SmartCover = laosijiPreviewURL(task.PreviewDir, task.SmartCover)
+ r.HevcMeta = task.HevcMetaInfo
+ }
+ case task.Status <= transcodeFailedStatus:
+ r.Failed = true
+ }
+ return r
+}
+
+func laosijiURL(name string) string {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ return ""
+ }
+ return joinURLPath("/laosiji", name)
+}
+
+func laosijiPreviewURL(previewDir, name string) string {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ return ""
+ }
+ return joinURLPath(joinURLPath("/laosiji", strings.TrimSpace(previewDir)), name)
+}
+
+func (c *Client) getVideoTranscodingQueue(ctx context.Context, domains SystemDomains) (transcodeQueueInfo, error) {
+ endpoint := joinURLPath(domains.UploadURL, "upload/getVideoTranscodingQueueInfo")
+ data, err := c.uploadAPICall(ctx, endpoint, map[string]string{
+ "v": videoTranscodingVersion,
+ "key": domains.UploadKey,
+ })
+ if err != nil {
+ return transcodeQueueInfo{}, err
+ }
+ var q transcodeQueueInfo
+ if len(data) > 0 {
+ if err = json.Unmarshal(data, &q); err != nil {
+ return transcodeQueueInfo{}, err
+ }
+ }
+ return q, nil
+}
+
+func (c *Client) uploadAPIGet(ctx context.Context, endpoint string, params map[string]string) (transcodeTask, error) {
+ data, err := c.uploadAPICall(ctx, endpoint, params)
+ if err != nil {
+ return transcodeTask{}, err
+ }
+ var task transcodeTask
+ if len(data) > 0 {
+ if err = json.Unmarshal(data, &task); err != nil {
+ return transcodeTask{}, err
+ }
+ }
+ return task, nil
+}
+
+func (c *Client) uploadAPICall(ctx context.Context, endpoint string, params map[string]string) (json.RawMessage, error) {
+ parsed, err := url.Parse(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ query := parsed.Query()
+ for k, v := range params {
+ query.Set(k, v)
+ }
+ parsed.RawQuery = query.Encode()
+
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := c.hc.Do(httpReq)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("upload api http status:%d body:%s", resp.StatusCode, truncate(body, 300))
+ }
+ var apiResp uploadAPIResp
+ if err = json.Unmarshal(body, &apiResp); err != nil {
+ return nil, err
+ }
+ if apiResp.Status != "y" {
+ if apiResp.ErrorCode == 2000 {
+ return nil, errTranscodeTaskNotFound
+ }
+ return nil, fmt.Errorf("upload api errorCode:%d error:%s", apiResp.ErrorCode, apiResp.Error)
+ }
+ return apiResp.Data, nil
+}
+
+func md5Hex(value string) string {
+ sum := md5.Sum([]byte(value))
+ return hex.EncodeToString(sum[:])
+}
+
+func joinURLPath(host, uri string) string {
+ host = strings.TrimRight(strings.TrimSpace(host), "/")
+ uri = strings.TrimLeft(strings.TrimSpace(uri), "/")
+ if host == "" {
+ return uri
+ }
+ if uri == "" {
+ return host
+ }
+ return host + "/" + uri
+}
diff --git a/pub/common/linode/linode.go b/pub/common/linode/linode.go
new file mode 100644
index 0000000..b43230c
--- /dev/null
+++ b/pub/common/linode/linode.go
@@ -0,0 +1,212 @@
+package linode
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "mime"
+ "os"
+ "path"
+ "path/filepath"
+ "videoweb/pub/common/fconst"
+ "videoweb/pub/common/log"
+
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "github.com/karrick/godirwalk"
+ "github.com/xxjwxc/gowp/workpool"
+)
+
+const (
+ //region = "default"
+ //endpoint = "https://ap-south-1.linodeobjects.com"
+ //accessKey = "0RALF74H45ANVYPRQA1Y"
+ //secretKey = "S1yCKUqxFJHqlFPZXJ3BbD8T6P0Yz6loUDntUrsS"
+ //bucket = "fengshang"
+ M = 1024 * 1024
+ G = M * 1024
+ defaultConcurrency = 150
+ defaultTryMax = 5
+ defaultContentType = "application/octet-stream"
+)
+
+type Client struct {
+ session *session.Session
+ cfg Config
+}
+type UploadReq struct {
+ Key string
+ ContentType string
+ Data []byte
+}
+
+func (u *UploadReq) Valid() bool {
+ return u.Key != "" && len(u.Data) != 0
+}
+
+type Config struct {
+ Region string
+ Endpoint string
+ AccessKey string
+ SecretKey string
+ Bucket string
+ Concurrency int
+ TryMax int
+}
+
+func traverseDir(d string) ([]string, error) {
+ var fileNames []string
+ fn := func(name string, entry *godirwalk.Dirent) error {
+ if !entry.IsRegular() {
+ return nil
+ }
+ fileNames = append(fileNames, name)
+ return nil
+ }
+ pathName := d
+ err := godirwalk.Walk(pathName, &godirwalk.Options{
+ Unsorted: true,
+ Callback: fn,
+ })
+ if err != nil {
+ log.Info("list files failed", log.Any("path", pathName), log.E(err))
+ return nil, err
+ }
+ return fileNames, nil
+}
+
+func New(cfg Config) (*Client, error) {
+ if cfg.TryMax <= 0 {
+ cfg.TryMax = defaultTryMax
+ }
+ if cfg.Concurrency <= 0 {
+ cfg.Concurrency = defaultConcurrency
+ }
+ s, err := session.NewSession(&aws.Config{
+ Region: aws.String(cfg.Region),
+ Endpoint: aws.String(cfg.Endpoint),
+ Credentials: credentials.NewStaticCredentials(cfg.AccessKey, cfg.SecretKey, ""),
+ })
+ if err != nil {
+ log.Error("Linode NewSession err", log.E(err))
+ return nil, err
+ }
+ return &Client{session: s, cfg: cfg}, nil
+}
+
+//func WithDefault() (*Client, error) {
+// return New(&Config{Concurrency: defaultConcurrency, TryMax: defaultTryMax})
+//}
+
+func (c *Client) UploadFolder(folderName string) error {
+ absFiles, err := traverseDir(folderName)
+ if err != nil {
+ return err
+ }
+ if len(absFiles) == 0 {
+ return nil
+ }
+ wp := workpool.New(c.cfg.Concurrency)
+ for _, absFilename := range absFiles {
+ var absName = absFilename
+ wp.Do(func() error {
+ var err error
+ for i := 0; i < c.cfg.TryMax; i++ {
+ start := time.Now()
+ err = c.uploadToLinNode(absName)
+ diff := time.Now().Sub(start)
+ if diff < time.Second {
+ time.Sleep(time.Second - diff)
+ }
+ if err == nil {
+ break
+ }
+ }
+ return err
+ })
+ }
+ return wp.Wait()
+}
+
+func (c *Client) UploadRaw(key, contentType string, body io.Reader) (string, error) {
+ if contentType == "" {
+ contentType = defaultContentType
+ }
+ input := &s3manager.UploadInput{
+ Bucket: aws.String(c.cfg.Bucket),
+ Key: aws.String(key),
+ ACL: aws.String("public-read"),
+ Body: body,
+ ContentType: aws.String(contentType),
+ }
+ result, err := s3manager.NewUploader(c.session, func(uploader *s3manager.Uploader) {
+ uploader.PartSize = 1 * G
+ }).Upload(input)
+ if err != nil {
+ log.Info("UpdateRaw Upload Err", log.Any("key", key), log.E(err))
+ return "", err
+ }
+ log.Info("UpdateRaw Upload OK", log.Any("key", key), log.Any("Location", result.Location))
+ return "", nil
+}
+func (c *Client) Upload(req UploadReq) error {
+ if !req.Valid() {
+ return errors.New("invalid upload request")
+ }
+ if req.ContentType == "" {
+ req.ContentType = defaultContentType
+ ext := path.Ext(req.Key)
+ if ext != "" {
+ req.ContentType = mime.TypeByExtension(ext)
+ }
+ }
+ var err error
+ for i := 0; i < defaultTryMax; i++ {
+ buf := bytes.NewReader(req.Data)
+ input := &s3.PutObjectInput{
+ Bucket: aws.String(c.cfg.Bucket),
+ Key: aws.String(req.Key),
+ ACL: aws.String("public-read"),
+ Body: buf,
+ ContentType: aws.String(req.ContentType),
+ }
+
+ _, err := s3.New(c.session).PutObject(input)
+ if err == nil {
+ log.Info("Linode Upload OK", log.Any("key", req.Key))
+ return nil
+ }
+ time.Sleep(time.Millisecond) //出让CPU
+ log.Error("Linode Upload Try err", log.Any("key", req.Key), log.Any("tryCount", i+1), log.E(err))
+ }
+ log.Error("Linode Upload err", log.Any("key", req.Key), log.E(err))
+ return err
+}
+
+// 多线程调用
+func (m *Client) uploadToLinNodeWithKey(fileName, key string) error {
+ f, err := os.OpenFile(fileName, os.O_RDONLY, 0644)
+ if err != nil {
+ log.Info("uploadToLinNodeWithKey OpenFile", log.Any("fileName", fileName), log.E(err))
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ ext := filepath.Ext(fileName)
+ contentType := ""
+ if ext != "" {
+ contentType = mime.TypeByExtension(ext)
+ }
+ _, err = m.UploadRaw(key, contentType, f)
+ return err
+}
+
+func (m *Client) uploadToLinNode(absFilename string) error {
+ key := strings.TrimPrefix(absFilename, fconst.FsDir)
+ return m.uploadToLinNodeWithKey(absFilename, key)
+}
diff --git a/pub/common/linode/linode_test.go b/pub/common/linode/linode_test.go
new file mode 100644
index 0000000..6d94a97
--- /dev/null
+++ b/pub/common/linode/linode_test.go
@@ -0,0 +1,28 @@
+package linode
+
+import (
+ "errors"
+ "fmt"
+ "github.com/xxjwxc/gowp/workpool"
+ "testing"
+)
+
+func TestAA(t *testing.T) {
+ wp := workpool.New(100)
+ for i := 0; i < 200000; i++ {
+ var t = i
+ wp.Do(func() error {
+ fmt.Println(t)
+ if t == 22 {
+ return errors.New("111")
+ }
+ return nil
+ })
+ }
+ err := wp.Wait()
+ if err != nil {
+ fmt.Println("fail", err)
+ } else {
+ fmt.Println("OK")
+ }
+}
diff --git a/pub/common/lock/cache.go b/pub/common/lock/cache.go
new file mode 100644
index 0000000..5d8a314
--- /dev/null
+++ b/pub/common/lock/cache.go
@@ -0,0 +1,86 @@
+package lock
+
+import (
+ "sync"
+)
+
+type IIdent interface {
+ GetID() int64
+}
+
+type Cache[T IIdent] struct {
+ dict map[int64]T
+ lock sync.RWMutex
+}
+
+func NewCache[T IIdent]() *Cache[T] {
+ return &Cache[T]{
+ dict: make(map[int64]T),
+ }
+}
+
+func (c *Cache[T]) Get(Id int64) (T, bool) {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ t, ok := c.dict[Id]
+ return t, ok
+}
+
+func (c *Cache[T]) Keys() []int64 {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ keys := make([]int64, 0, len(c.dict))
+ for k := range c.dict {
+ keys = append(keys, k)
+ }
+ return keys
+}
+
+func (c *Cache[T]) Set(items ...T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, item := range items {
+ c.dict[item.GetID()] = item
+ }
+}
+
+func (c *Cache[T]) Del(items ...T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, item := range items {
+ delete(c.dict, item.GetID())
+ }
+}
+
+func (c *Cache[T]) Update(dict map[int64]T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ c.dict = dict
+}
+
+func (c *Cache[T]) SetItems(items []T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, item := range items {
+ c.dict[item.GetID()] = item
+ }
+}
+
+func (c *Cache[T]) Has(Id int64) bool {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ _, ok := c.dict[Id]
+ return ok
+}
+
+func (c *Cache[T]) GetItems(Ids []int64) []T {
+ ret := make([]T, 0)
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ for _, id := range Ids {
+ if item, ok := c.dict[id]; ok {
+ ret = append(ret, item)
+ }
+ }
+ return ret
+}
diff --git a/pub/common/lock/dict.go b/pub/common/lock/dict.go
new file mode 100644
index 0000000..10b7979
--- /dev/null
+++ b/pub/common/lock/dict.go
@@ -0,0 +1,143 @@
+package lock
+
+import (
+ "sync"
+)
+
+type Dict[K comparable, V any] struct {
+ dict map[K]V
+ lock sync.RWMutex
+}
+
+func NewDict[K comparable, V any]() *Dict[K, V] {
+ return &Dict[K, V]{
+ dict: make(map[K]V),
+ }
+}
+
+// Count get dict key count
+func (d *Dict[K, V]) Count() int {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ return len(d.dict)
+}
+
+// Keys get dict keys
+func (d *Dict[K, V]) Keys() []K {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ keys := make([]K, 0, len(d.dict))
+ for k := range d.dict {
+ keys = append(keys, k)
+ }
+ return keys
+}
+
+// Set set k v to dict, overwrite old value if exists
+func (d *Dict[K, V]) Set(key K, value V) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.dict[key] = value
+}
+
+// Has if exists, return true
+func (d *Dict[K, V]) Has(key K) bool {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ _, exists := d.dict[key]
+ return exists
+}
+
+// Get if exists, return value and true
+func (d *Dict[K, V]) Get(key K) (V, bool) {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ value, exists := d.dict[key]
+ return value, exists
+}
+
+// Del if exists and del success, return true
+func (d *Dict[K, V]) Del(key K) bool {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ _, exists := d.dict[key]
+ delete(d.dict, key)
+ return exists
+}
+
+// Remove if exists and remove success, return old value and true
+func (d *Dict[K, V]) Remove(key K) (V, bool) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ value, exists := d.dict[key]
+ delete(d.dict, key)
+ return value, exists
+}
+
+// SetIfNotExists if not exists and set success, return true
+func (d *Dict[K, V]) SetIfNotExists(key K, value V) bool {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ _, exists := d.dict[key]
+ if !exists {
+ d.dict[key] = value
+ return true
+ }
+ return false
+}
+
+// GetOrSetIfNotExists if key exists return old:true, or, set new value, and return new:false
+func (d *Dict[K, V]) GetOrSetIfNotExists(key K, new V) (V, bool) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ old, exists := d.dict[key]
+ if !exists {
+ d.dict[key] = new
+ return new, false
+ }
+ return old, true
+}
+
+// Travel travels all dict keys and values
+func (d *Dict[K, V]) Travel(travel func(key K, value V) bool) {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ for key, value := range d.dict {
+ cont := travel(key, value)
+ if !cont {
+ break
+ }
+ }
+}
+
+// Clear dict directly
+func (d *Dict[K, V]) Clear() {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.dict = map[K]V{}
+}
+
+// Update replace dict directly
+func (d *Dict[K, V]) Update(dict map[K]V) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.dict = dict
+}
+
+// Replace replace dict directly and return old dict
+func (d *Dict[K, V]) Replace(dict map[K]V) map[K]V {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ old := d.dict
+ d.dict = dict
+ return old
+}
+
+// Append dict directly
+func (d *Dict[K, V]) Append(dict map[K]V) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ for k, v := range dict {
+ d.dict[k] = v
+ }
+}
diff --git a/pub/common/lock/queue.go b/pub/common/lock/queue.go
new file mode 100644
index 0000000..531d557
--- /dev/null
+++ b/pub/common/lock/queue.go
@@ -0,0 +1,75 @@
+package lock
+
+import "sync"
+
+// Queue 定义一个泛型队列结构
+type Queue[T any] struct {
+ items []T
+ lock sync.RWMutex // 添加读写锁
+}
+
+// NewQueue 创建一个新的队列
+func NewQueue[T any]() *Queue[T] {
+ return &Queue[T]{
+ items: make([]T, 0),
+ }
+}
+
+// Enqueue 入队操作 - 将元素添加到队列末尾
+func (q *Queue[T]) Enqueue(item T) {
+ q.lock.Lock() // 写操作需要写锁
+ defer q.lock.Unlock()
+
+ q.items = append(q.items, item)
+}
+
+// Dequeue 出队操作 - 从队列头部移除并返回元素
+func (q *Queue[T]) Dequeue() (T, bool) {
+ q.lock.Lock() // 写操作需要写锁
+ defer q.lock.Unlock()
+
+ var zero T
+ if len(q.items) == 0 {
+ return zero, false
+ }
+
+ item := q.items[0]
+ q.items = q.items[1:]
+ return item, true
+}
+
+// Peek 查看队列头部元素但不移除
+func (q *Queue[T]) Peek() (T, bool) {
+ q.lock.RLock() // 读操作使用读锁
+ defer q.lock.RUnlock()
+
+ var zero T
+ if len(q.items) == 0 {
+ return zero, false
+ }
+ return q.items[0], true
+}
+
+// IsEmpty 检查队列是否为空
+func (q *Queue[T]) IsEmpty() bool {
+ q.lock.RLock() // 读操作使用读锁
+ defer q.lock.RUnlock()
+
+ return len(q.items) == 0
+}
+
+// Size 返回队列中的元素个数
+func (q *Queue[T]) Size() int {
+ q.lock.RLock() // 读操作使用读锁
+ defer q.lock.RUnlock()
+
+ return len(q.items)
+}
+
+// Clear 清空队列
+func (q *Queue[T]) Clear() {
+ q.lock.Lock() // 写操作需要写锁
+ defer q.lock.Unlock()
+
+ q.items = make([]T, 0)
+}
diff --git a/pub/common/log/log.go b/pub/common/log/log.go
new file mode 100644
index 0000000..29ca8d3
--- /dev/null
+++ b/pub/common/log/log.go
@@ -0,0 +1,128 @@
+package log
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "time"
+ "videoweb/pub/common/log/zapcorex"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+var (
+ zapLogger *zap.Logger
+ zapLoggerS *zap.SugaredLogger
+ ZapLog *zap.Logger
+ ZapLogS *zap.SugaredLogger
+ Debug = zapLogger.Debug
+ Info = zapLogger.Info
+ Warn = zapLogger.Warn
+ Error = zapLogger.Error
+ Panic = zapLogger.Panic
+ Fatal = zapLogger.Fatal
+ Debugf = zapLoggerS.Debugf
+ Infof = zapLoggerS.Infof
+ Warnf = zapLoggerS.Warnf
+ Errorf = zapLoggerS.Errorf
+ Fatalf = zapLoggerS.Fatalf
+ Debugw = zapLoggerS.Debugw
+ Infow = zapLoggerS.Infow
+ Warnw = zapLoggerS.Warnw
+ Errorw = zapLoggerS.Errorw
+ Fatalw = zapLoggerS.Fatalw
+)
+
+func newEncoderConfig() zapcore.EncoderConfig {
+ return zapcore.EncoderConfig{
+ // Keys can be anything except the empty string.
+ TimeKey: "T",
+ LevelKey: "L",
+ NameKey: "N",
+ CallerKey: "C",
+ MessageKey: "M",
+ StacktraceKey: "S",
+ FunctionKey: "F",
+ LineEnding: zapcore.DefaultLineEnding,
+ EncodeLevel: zapcore.CapitalColorLevelEncoder,
+ EncodeTime: timeEncoder,
+ EncodeDuration: zapcore.StringDurationEncoder,
+ EncodeCaller: zapcore.ShortCallerEncoder,
+ ConsoleSeparator: " ",
+ }
+}
+
+func init() {
+ core := zapcore.NewCore(
+ zapcorex.NewConsoleEncoder(newEncoderConfig()),
+ zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
+ zap.DebugLevel,
+ )
+ zapLogger = zap.New(core, zap.AddCaller())
+ zapLoggerS = zapLogger.Sugar()
+ ZapLog = zapLogger
+ ZapLogS = zapLoggerS
+ Debug = zapLogger.Debug
+ Info = zapLogger.Info
+ Warn = zapLogger.Warn
+ Error = zapLogger.Error
+ Panic = zapLogger.Panic
+ Fatal = zapLogger.Fatal
+ Debugf = zapLoggerS.Debugf
+ Infof = zapLoggerS.Infof
+ Warnf = zapLoggerS.Warnf
+ Errorf = zapLoggerS.Errorf
+ Fatalf = zapLoggerS.Fatalf
+ Debugw = zapLoggerS.Debugw
+ Infow = zapLoggerS.Infow
+ Warnw = zapLoggerS.Warnw
+ Errorw = zapLoggerS.Errorw
+ Fatalw = zapLoggerS.Fatalw
+}
+
+func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+ enc.AppendString(t.In(time.Local).Format("2006-01-02 15:04:05"))
+}
+
+type Field = zap.Field
+
+func Any(key string, value interface{}) Field {
+ return zap.Any(key, value)
+}
+
+func E(err error) Field {
+ return Any("err", err)
+}
+
+type contextKey int64
+
+var (
+ loggerContextKey contextKey
+)
+
+func ContextWithLogger(ctx context.Context, l *zap.SugaredLogger) context.Context {
+ return context.WithValue(ctx, loggerContextKey, l)
+}
+
+func LoggerFromContext(ctx context.Context) *zap.SugaredLogger {
+ v := ctx.Value(loggerContextKey)
+ if v == nil {
+ return ZapLogS
+ }
+ return v.(*zap.SugaredLogger)
+}
+
+type Logger struct{}
+
+// Info logs routine messages about cron's operation.
+func (s *Logger) Info(msg string, keysAndValues ...interface{}) {
+ _ = log.Default().Output(2, msg+" "+fmt.Sprint(keysAndValues...))
+}
+
+// Error logs an error condition.
+func (s *Logger) Error(err error, msg string, keysAndValues ...interface{}) {
+ line := fmt.Sprintf("%s %v %s", msg, err, fmt.Sprint(keysAndValues...))
+ _ = log.Default().Output(2, line)
+}
diff --git a/pub/common/log/zapcorex/bufferpool/bufferpool.go b/pub/common/log/zapcorex/bufferpool/bufferpool.go
new file mode 100644
index 0000000..dad583a
--- /dev/null
+++ b/pub/common/log/zapcorex/bufferpool/bufferpool.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package bufferpool houses zap's shared internal buffer pool. Third-party
+// packages can recreate the same functionality with buffers.NewPool.
+package bufferpool
+
+import "go.uber.org/zap/buffer"
+
+var (
+ _pool = buffer.NewPool()
+ // Get retrieves a buffer from the pool, creating one if necessary.
+ Get = _pool.Get
+)
diff --git a/pub/common/log/zapcorex/color.go b/pub/common/log/zapcorex/color.go
new file mode 100644
index 0000000..6b8faa7
--- /dev/null
+++ b/pub/common/log/zapcorex/color.go
@@ -0,0 +1,44 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package color adds coloring functionality for TTY output.
+package zapcorex
+
+import "fmt"
+
+// Foreground colors.
+const (
+ Black Color = iota + 30
+ Red
+ Green
+ Yellow
+ Blue
+ Magenta
+ Cyan
+ White
+)
+
+// Color represents a text color.
+type Color uint8
+
+// Add adds the coloring to the given string.
+func (c Color) Add(s string) string {
+ return fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(c), s)
+}
diff --git a/pub/common/log/zapcorex/console_encoder.go b/pub/common/log/zapcorex/console_encoder.go
new file mode 100644
index 0000000..51ea796
--- /dev/null
+++ b/pub/common/log/zapcorex/console_encoder.go
@@ -0,0 +1,150 @@
+package zapcorex
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "videoweb/pub/common/log/zapcorex/bufferpool"
+
+ "go.uber.org/zap/buffer"
+ "go.uber.org/zap/zapcore"
+)
+
+var _sliceEncoderPool = sync.Pool{
+ New: func() interface{} {
+ return &sliceArrayEncoder{elems: make([]interface{}, 0, 2)}
+ },
+}
+
+func getSliceEncoder() *sliceArrayEncoder {
+ return _sliceEncoderPool.Get().(*sliceArrayEncoder)
+}
+
+func putSliceEncoder(e *sliceArrayEncoder) {
+ e.elems = e.elems[:0]
+ _sliceEncoderPool.Put(e)
+}
+
+type consoleEncoder struct {
+ *jsonEncoder
+}
+
+// NewConsoleEncoder creates an encoder whose output is designed for human -
+// rather than machine - consumption. It serializes the core log entry data
+// (message, level, timestamp, etc.) in a plain-text format and leaves the
+// structured context as JSON.
+//
+// Note that although the console encoder doesn't use the keys specified in the
+// encoder configuration, it will omit any element whose key is set to the empty
+// string.
+func NewConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
+ if cfg.ConsoleSeparator == "" {
+ // Use a default delimiter of '\t' for backwards compatibility
+ cfg.ConsoleSeparator = "\t"
+ }
+ return consoleEncoder{newJSONEncoder(cfg, true)}
+}
+
+func (c consoleEncoder) Clone() zapcore.Encoder {
+ return consoleEncoder{c.jsonEncoder.Clone().(*jsonEncoder)}
+}
+
+func (c consoleEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
+ line := bufferpool.Get()
+
+ // We don't want the entry's metadata to be quoted and escaped (if it's
+ // encoded as strings), which means that we can't use the JSON encoder. The
+ // simplest option is to use the memory encoder and fmt.Fprint.
+ //
+ // If this ever becomes a performance bottleneck, we can implement
+ // ArrayEncoder for our plain-text format.
+ arr := getSliceEncoder()
+ if c.TimeKey != "" && c.EncodeTime != nil {
+ c.EncodeTime(ent.Time, arr)
+ }
+ if c.LevelKey != "" && c.EncodeLevel != nil {
+ c.EncodeLevel(ent.Level, arr)
+ }
+ if ent.LoggerName != "" && c.NameKey != "" {
+ nameEncoder := c.EncodeName
+
+ if nameEncoder == nil {
+ // Fall back to FullNameEncoder for backward compatibility.
+ nameEncoder = zapcore.FullNameEncoder
+ }
+
+ nameEncoder(ent.LoggerName, arr)
+ }
+
+ for i := range arr.elems {
+ if i > 0 {
+ line.AppendString(c.ConsoleSeparator)
+ }
+ fmt.Fprint(line, arr.elems[i])
+ }
+
+ putSliceEncoder(arr)
+
+ // Add the message itself.
+ if c.MessageKey != "" {
+ c.addSeparatorIfNecessary(line)
+ line.AppendString(ent.Message)
+ }
+
+ // Add any structured context.
+ c.writeContext(line, fields)
+
+ if ent.Caller.Defined {
+ if c.FunctionKey != "" {
+ c.addSeparatorIfNecessary(line)
+ if idx := strings.LastIndex(ent.Caller.Function, "/"); idx > 0 {
+ line.AppendString(White.Add(ent.Caller.Function[idx+1:]))
+ } else {
+ line.AppendString(White.Add(ent.Caller.Function))
+ }
+ }
+
+ line.AppendString(c.ConsoleSeparator)
+ if c.CallerKey != "" && c.EncodeCaller != nil {
+ line.AppendString(White.Add(ent.Caller.TrimmedPath()))
+ }
+ }
+
+ // If there's no stacktrace key, honor that; this allows users to force
+ // single-line output.
+ if ent.Stack != "" && c.StacktraceKey != "" {
+ line.AppendByte('\n')
+ line.AppendString(ent.Stack)
+ }
+
+ line.AppendString(c.LineEnding)
+ return line, nil
+}
+
+func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []zapcore.Field) {
+ context := c.jsonEncoder.Clone().(*jsonEncoder)
+ defer func() {
+ // putJSONEncoder assumes the buffer is still used, but we write out the buffer so
+ // we can free it.
+ context.buf.Free()
+ putJSONEncoder(context)
+ }()
+
+ addFields(context, extra)
+ context.closeOpenNamespaces()
+ if context.buf.Len() == 0 {
+ return
+ }
+
+ c.addSeparatorIfNecessary(line)
+ line.AppendByte('{')
+ line.Write(context.buf.Bytes())
+ line.AppendByte('}')
+}
+
+func (c consoleEncoder) addSeparatorIfNecessary(line *buffer.Buffer) {
+ if line.Len() > 0 {
+ line.AppendString(c.ConsoleSeparator)
+ }
+}
diff --git a/pub/common/log/zapcorex/field.go b/pub/common/log/zapcorex/field.go
new file mode 100644
index 0000000..d5f91e8
--- /dev/null
+++ b/pub/common/log/zapcorex/field.go
@@ -0,0 +1,9 @@
+package zapcorex
+
+import "go.uber.org/zap/zapcore"
+
+func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
+ for i := range fields {
+ fields[i].AddTo(enc)
+ }
+}
diff --git a/pub/common/log/zapcorex/json_encoder.go b/pub/common/log/zapcorex/json_encoder.go
new file mode 100644
index 0000000..95f8975
--- /dev/null
+++ b/pub/common/log/zapcorex/json_encoder.go
@@ -0,0 +1,518 @@
+package zapcorex
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "math"
+ "sync"
+ "time"
+ "unicode/utf8"
+
+ "videoweb/pub/common/log/zapcorex/bufferpool"
+
+ "go.uber.org/zap/buffer"
+ "go.uber.org/zap/zapcore"
+)
+
+// For JSON-escaping; see jsonEncoder.safeAddString below.
+const _hex = "0123456789abcdef"
+
+var _jsonPool = sync.Pool{New: func() interface{} {
+ return &jsonEncoder{}
+}}
+
+func getJSONEncoder() *jsonEncoder {
+ return _jsonPool.Get().(*jsonEncoder)
+}
+
+func putJSONEncoder(enc *jsonEncoder) {
+ if enc.reflectBuf != nil {
+ enc.reflectBuf.Free()
+ }
+ enc.EncoderConfig = nil
+ enc.buf = nil
+ enc.spaced = false
+ enc.openNamespaces = 0
+ enc.reflectBuf = nil
+ enc.reflectEnc = nil
+ _jsonPool.Put(enc)
+}
+
+type jsonEncoder struct {
+ *zapcore.EncoderConfig
+ buf *buffer.Buffer
+ spaced bool // include spaces after colons and commas
+ openNamespaces int
+
+ // for encoding generic values by reflection
+ reflectBuf *buffer.Buffer
+ reflectEnc *json.Encoder
+}
+
+// NewJSONEncoder creates a fast, low-allocation JSON encoder. The encoder
+// appropriately escapes all field keys and values.
+//
+// Note that the encoder doesn't deduplicate keys, so it's possible to produce
+// a message like
+//
+// {"foo":"bar","foo":"baz"}
+//
+// This is permitted by the JSON specification, but not encouraged. Many
+// libraries will ignore duplicate key-value pairs (typically keeping the last
+// pair) when unmarshaling, but users should attempt to avoid adding duplicate
+// keys.
+func NewJSONEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
+ return newJSONEncoder(cfg, false)
+}
+
+func newJSONEncoder(cfg zapcore.EncoderConfig, spaced bool) *jsonEncoder {
+ return &jsonEncoder{
+ EncoderConfig: &cfg,
+ buf: bufferpool.Get(),
+ spaced: spaced,
+ }
+}
+
+func (enc *jsonEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error {
+ enc.addKey(key)
+ return enc.AppendArray(arr)
+}
+
+func (enc *jsonEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error {
+ enc.addKey(key)
+ return enc.AppendObject(obj)
+}
+
+func (enc *jsonEncoder) AddBinary(key string, val []byte) {
+ enc.AddString(key, base64.StdEncoding.EncodeToString(val))
+}
+
+func (enc *jsonEncoder) AddByteString(key string, val []byte) {
+ enc.addKey(key)
+ enc.AppendByteString(val)
+}
+
+func (enc *jsonEncoder) AddBool(key string, val bool) {
+ enc.addKey(key)
+ enc.AppendBool(val)
+}
+
+func (enc *jsonEncoder) AddComplex128(key string, val complex128) {
+ enc.addKey(key)
+ enc.AppendComplex128(val)
+}
+
+func (enc *jsonEncoder) AddDuration(key string, val time.Duration) {
+ enc.addKey(key)
+ enc.AppendDuration(val)
+}
+
+func (enc *jsonEncoder) AddFloat64(key string, val float64) {
+ enc.addKey(key)
+ enc.AppendFloat64(val)
+}
+
+func (enc *jsonEncoder) AddInt64(key string, val int64) {
+ enc.addKey(key)
+ enc.AppendInt64(val)
+}
+
+func (enc *jsonEncoder) resetReflectBuf() {
+ if enc.reflectBuf == nil {
+ enc.reflectBuf = bufferpool.Get()
+ enc.reflectEnc = json.NewEncoder(enc.reflectBuf)
+
+ // For consistency with our custom JSON encoder.
+ enc.reflectEnc.SetEscapeHTML(false)
+ } else {
+ enc.reflectBuf.Reset()
+ }
+}
+
+var nullLiteralBytes = []byte("null")
+
+// Only invoke the standard JSON encoder if there is actually something to
+// encode; otherwise write JSON null literal directly.
+func (enc *jsonEncoder) encodeReflected(obj interface{}) ([]byte, error) {
+ if obj == nil {
+ return nullLiteralBytes, nil
+ }
+ enc.resetReflectBuf()
+ if err := enc.reflectEnc.Encode(obj); err != nil {
+ return nil, err
+ }
+ enc.reflectBuf.TrimNewline()
+ return enc.reflectBuf.Bytes(), nil
+}
+
+func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error {
+ valueBytes, err := enc.encodeReflected(obj)
+ if err != nil {
+ return err
+ }
+ enc.addKey(key)
+ _, err = enc.buf.Write(valueBytes)
+ return err
+}
+
+func (enc *jsonEncoder) OpenNamespace(key string) {
+ enc.addKey(key)
+ enc.buf.AppendByte('{')
+ enc.openNamespaces++
+}
+
+func (enc *jsonEncoder) AddString(key, val string) {
+ enc.addKey(key)
+ enc.AppendString(val)
+}
+
+func (enc *jsonEncoder) AddTime(key string, val time.Time) {
+ enc.addKey(key)
+ enc.AppendTime(val)
+}
+
+func (enc *jsonEncoder) AddUint64(key string, val uint64) {
+ enc.addKey(key)
+ enc.AppendUint64(val)
+}
+
+func (enc *jsonEncoder) AppendArray(arr zapcore.ArrayMarshaler) error {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('[')
+ err := arr.MarshalLogArray(enc)
+ enc.buf.AppendByte(']')
+ return err
+}
+
+func (enc *jsonEncoder) AppendObject(obj zapcore.ObjectMarshaler) error {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('{')
+ err := obj.MarshalLogObject(enc)
+ enc.buf.AppendByte('}')
+ return err
+}
+
+func (enc *jsonEncoder) AppendBool(val bool) {
+ enc.addElementSeparator()
+ enc.buf.AppendBool(val)
+}
+
+func (enc *jsonEncoder) AppendByteString(val []byte) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.safeAddByteString(val)
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendComplex128(val complex128) {
+ enc.addElementSeparator()
+ // Cast to a platform-independent, fixed-size type.
+ r, i := float64(real(val)), float64(imag(val))
+ enc.buf.AppendByte('"')
+ // Because we're always in a quoted string, we can use strconv without
+ // special-casing NaN and +/-Inf.
+ enc.buf.AppendFloat(r, 64)
+ enc.buf.AppendByte('+')
+ enc.buf.AppendFloat(i, 64)
+ enc.buf.AppendByte('i')
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendDuration(val time.Duration) {
+ cur := enc.buf.Len()
+ if e := enc.EncodeDuration; e != nil {
+ e(val, enc)
+ }
+ if cur == enc.buf.Len() {
+ // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
+ // JSON valid.
+ enc.AppendInt64(int64(val))
+ }
+}
+
+func (enc *jsonEncoder) AppendInt64(val int64) {
+ enc.addElementSeparator()
+ enc.buf.AppendInt(val)
+}
+
+func (enc *jsonEncoder) AppendReflected(val interface{}) error {
+ valueBytes, err := enc.encodeReflected(val)
+ if err != nil {
+ return err
+ }
+ enc.addElementSeparator()
+ _, err = enc.buf.Write(valueBytes)
+ return err
+}
+
+func (enc *jsonEncoder) AppendString(val string) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.safeAddString(val)
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendTimeLayout(time time.Time, layout string) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.buf.AppendTime(time, layout)
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendTime(val time.Time) {
+ cur := enc.buf.Len()
+ if e := enc.EncodeTime; e != nil {
+ e(val, enc)
+ }
+ if cur == enc.buf.Len() {
+ // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
+ // output JSON valid.
+ enc.AppendInt64(val.UnixNano())
+ }
+}
+
+func (enc *jsonEncoder) AppendUint64(val uint64) {
+ enc.addElementSeparator()
+ enc.buf.AppendUint(val)
+}
+
+func (enc *jsonEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) }
+func (enc *jsonEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) }
+func (enc *jsonEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) }
+func (enc *jsonEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) }
+func (enc *jsonEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) }
+func (enc *jsonEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) }
+
+func (enc *jsonEncoder) Clone() zapcore.Encoder {
+ clone := enc.clone()
+ clone.buf.Write(enc.buf.Bytes())
+ return clone
+}
+
+func (enc *jsonEncoder) clone() *jsonEncoder {
+ clone := getJSONEncoder()
+ clone.EncoderConfig = enc.EncoderConfig
+ clone.spaced = enc.spaced
+ clone.openNamespaces = enc.openNamespaces
+ clone.buf = bufferpool.Get()
+ return clone
+}
+
+func (enc *jsonEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
+ final := enc.clone()
+ final.buf.AppendByte('{')
+
+ if final.LevelKey != "" {
+ final.addKey(final.LevelKey)
+ cur := final.buf.Len()
+ final.EncodeLevel(ent.Level, final)
+ if cur == final.buf.Len() {
+ // User-supplied EncodeLevel was a no-op. Fall back to strings to keep
+ // output JSON valid.
+ final.AppendString(ent.Level.String())
+ }
+ }
+ if final.TimeKey != "" {
+ final.AddTime(final.TimeKey, ent.Time)
+ }
+ if ent.LoggerName != "" && final.NameKey != "" {
+ final.addKey(final.NameKey)
+ cur := final.buf.Len()
+ nameEncoder := final.EncodeName
+
+ // if no name encoder provided, fall back to FullNameEncoder for backwards
+ // compatibility
+ if nameEncoder == nil {
+ nameEncoder = zapcore.FullNameEncoder
+ }
+
+ nameEncoder(ent.LoggerName, final)
+ if cur == final.buf.Len() {
+ // User-supplied EncodeName was a no-op. Fall back to strings to
+ // keep output JSON valid.
+ final.AppendString(ent.LoggerName)
+ }
+ }
+ if ent.Caller.Defined {
+ if final.CallerKey != "" {
+ final.addKey(final.CallerKey)
+ cur := final.buf.Len()
+ final.EncodeCaller(ent.Caller, final)
+ if cur == final.buf.Len() {
+ // User-supplied EncodeCaller was a no-op. Fall back to strings to
+ // keep output JSON valid.
+ final.AppendString(ent.Caller.String())
+ }
+ }
+ if final.FunctionKey != "" {
+ final.addKey(final.FunctionKey)
+ final.AppendString(ent.Caller.Function)
+ }
+ }
+ if final.MessageKey != "" {
+ final.addKey(enc.MessageKey)
+ final.AppendString(ent.Message)
+ }
+ if enc.buf.Len() > 0 {
+ final.addElementSeparator()
+ final.buf.Write(enc.buf.Bytes())
+ }
+ addFields(final, fields)
+ final.closeOpenNamespaces()
+ if ent.Stack != "" && final.StacktraceKey != "" {
+ final.AddString(final.StacktraceKey, ent.Stack)
+ }
+ final.buf.AppendByte('}')
+ if final.LineEnding != "" {
+ final.buf.AppendString(final.LineEnding)
+ } else {
+ final.buf.AppendString(zapcore.DefaultLineEnding)
+ }
+
+ ret := final.buf
+ putJSONEncoder(final)
+ return ret, nil
+}
+
+func (enc *jsonEncoder) truncate() {
+ enc.buf.Reset()
+}
+
+func (enc *jsonEncoder) closeOpenNamespaces() {
+ for i := 0; i < enc.openNamespaces; i++ {
+ enc.buf.AppendByte('}')
+ }
+}
+
+func (enc *jsonEncoder) addKey(key string) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.safeAddString(key)
+ enc.buf.AppendByte('"')
+ enc.buf.AppendByte(':')
+ if enc.spaced {
+ enc.buf.AppendByte(' ')
+ }
+}
+
+func (enc *jsonEncoder) addElementSeparator() {
+ last := enc.buf.Len() - 1
+ if last < 0 {
+ return
+ }
+ switch enc.buf.Bytes()[last] {
+ case '{', '[', ':', ',', ' ':
+ return
+ default:
+ enc.buf.AppendByte(',')
+ if enc.spaced {
+ enc.buf.AppendByte(' ')
+ }
+ }
+}
+
+func (enc *jsonEncoder) appendFloat(val float64, bitSize int) {
+ enc.addElementSeparator()
+ switch {
+ case math.IsNaN(val):
+ enc.buf.AppendString(`"NaN"`)
+ case math.IsInf(val, 1):
+ enc.buf.AppendString(`"+Inf"`)
+ case math.IsInf(val, -1):
+ enc.buf.AppendString(`"-Inf"`)
+ default:
+ enc.buf.AppendFloat(val, bitSize)
+ }
+}
+
+// safeAddString JSON-escapes a string and appends it to the internal buffer.
+// Unlike the standard library's encoder, it doesn't attempt to protect the
+// user from browser vulnerabilities or JSONP-related problems.
+func (enc *jsonEncoder) safeAddString(s string) {
+ for i := 0; i < len(s); {
+ if enc.tryAddRuneSelf(s[i]) {
+ i++
+ continue
+ }
+ r, size := utf8.DecodeRuneInString(s[i:])
+ if enc.tryAddRuneError(r, size) {
+ i++
+ continue
+ }
+ enc.buf.AppendString(s[i : i+size])
+ i += size
+ }
+}
+
+// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte.
+func (enc *jsonEncoder) safeAddByteString(s []byte) {
+ for i := 0; i < len(s); {
+ if enc.tryAddRuneSelf(s[i]) {
+ i++
+ continue
+ }
+ r, size := utf8.DecodeRune(s[i:])
+ if enc.tryAddRuneError(r, size) {
+ i++
+ continue
+ }
+ enc.buf.Write(s[i : i+size])
+ i += size
+ }
+}
+
+// tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte.
+func (enc *jsonEncoder) tryAddRuneSelf(b byte) bool {
+ if b >= utf8.RuneSelf {
+ return false
+ }
+ if 0x20 <= b && b != '\\' && b != '"' {
+ enc.buf.AppendByte(b)
+ return true
+ }
+ switch b {
+ case '\\', '"':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte(b)
+ case '\n':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte('n')
+ case '\r':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte('r')
+ case '\t':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte('t')
+ default:
+ // Encode bytes < 0x20, except for the escape sequences above.
+ enc.buf.AppendString(`\u00`)
+ enc.buf.AppendByte(_hex[b>>4])
+ enc.buf.AppendByte(_hex[b&0xF])
+ }
+ return true
+}
+
+func (enc *jsonEncoder) tryAddRuneError(r rune, size int) bool {
+ if r == utf8.RuneError && size == 1 {
+ enc.buf.AppendString(`\ufffd`)
+ return true
+ }
+ return false
+}
diff --git a/pub/common/log/zapcorex/memory_encoder.go b/pub/common/log/zapcorex/memory_encoder.go
new file mode 100644
index 0000000..646a142
--- /dev/null
+++ b/pub/common/log/zapcorex/memory_encoder.go
@@ -0,0 +1,183 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zapcorex
+
+import (
+ "time"
+
+ "go.uber.org/zap/zapcore"
+)
+
+// MapObjectEncoder is an ObjectEncoder backed by a simple
+// map[string]interface{}. It's not fast enough for production use, but it's
+// helpful in tests.
+type MapObjectEncoder struct {
+ // Fields contains the entire encoded log context.
+ Fields map[string]interface{}
+ // cur is a pointer to the namespace we're currently writing to.
+ cur map[string]interface{}
+}
+
+// NewMapObjectEncoder creates a new map-backed ObjectEncoder.
+func NewMapObjectEncoder() *MapObjectEncoder {
+ m := make(map[string]interface{})
+ return &MapObjectEncoder{
+ Fields: m,
+ cur: m,
+ }
+}
+
+// AddArray implements ObjectEncoder.
+func (m *MapObjectEncoder) AddArray(key string, v zapcore.ArrayMarshaler) error {
+ arr := &sliceArrayEncoder{elems: make([]interface{}, 0)}
+ err := v.MarshalLogArray(arr)
+ m.cur[key] = arr.elems
+ return err
+}
+
+// AddObject implements ObjectEncoder.
+func (m *MapObjectEncoder) AddObject(k string, v zapcore.ObjectMarshaler) error {
+ newMap := NewMapObjectEncoder()
+ m.cur[k] = newMap.Fields
+ return v.MarshalLogObject(newMap)
+}
+
+// AddBinary implements ObjectEncoder.
+func (m *MapObjectEncoder) AddBinary(k string, v []byte) { m.cur[k] = v }
+
+// AddByteString implements ObjectEncoder.
+func (m *MapObjectEncoder) AddByteString(k string, v []byte) { m.cur[k] = string(v) }
+
+// AddBool implements ObjectEncoder.
+func (m *MapObjectEncoder) AddBool(k string, v bool) { m.cur[k] = v }
+
+// AddDuration implements ObjectEncoder.
+func (m MapObjectEncoder) AddDuration(k string, v time.Duration) { m.cur[k] = v }
+
+// AddComplex128 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddComplex128(k string, v complex128) { m.cur[k] = v }
+
+// AddComplex64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddComplex64(k string, v complex64) { m.cur[k] = v }
+
+// AddFloat64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddFloat64(k string, v float64) { m.cur[k] = v }
+
+// AddFloat32 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddFloat32(k string, v float32) { m.cur[k] = v }
+
+// AddInt implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt(k string, v int) { m.cur[k] = v }
+
+// AddInt64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt64(k string, v int64) { m.cur[k] = v }
+
+// AddInt32 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt32(k string, v int32) { m.cur[k] = v }
+
+// AddInt16 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt16(k string, v int16) { m.cur[k] = v }
+
+// AddInt8 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt8(k string, v int8) { m.cur[k] = v }
+
+// AddString implements ObjectEncoder.
+func (m *MapObjectEncoder) AddString(k string, v string) { m.cur[k] = v }
+
+// AddTime implements ObjectEncoder.
+func (m MapObjectEncoder) AddTime(k string, v time.Time) { m.cur[k] = v }
+
+// AddUint implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint(k string, v uint) { m.cur[k] = v }
+
+// AddUint64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint64(k string, v uint64) { m.cur[k] = v }
+
+// AddUint32 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint32(k string, v uint32) { m.cur[k] = v }
+
+// AddUint16 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint16(k string, v uint16) { m.cur[k] = v }
+
+// AddUint8 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint8(k string, v uint8) { m.cur[k] = v }
+
+// AddUintptr implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUintptr(k string, v uintptr) { m.cur[k] = v }
+
+// AddReflected implements ObjectEncoder.
+func (m *MapObjectEncoder) AddReflected(k string, v interface{}) error {
+ m.cur[k] = v
+ return nil
+}
+
+// OpenNamespace implements ObjectEncoder.
+func (m *MapObjectEncoder) OpenNamespace(k string) {
+ ns := make(map[string]interface{})
+ m.cur[k] = ns
+ m.cur = ns
+}
+
+// sliceArrayEncoder is an ArrayEncoder backed by a simple []interface{}. Like
+// the MapObjectEncoder, it's not designed for production use.
+type sliceArrayEncoder struct {
+ elems []interface{}
+}
+
+func (s *sliceArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error {
+ enc := &sliceArrayEncoder{}
+ err := v.MarshalLogArray(enc)
+ s.elems = append(s.elems, enc.elems)
+ return err
+}
+
+func (s *sliceArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error {
+ m := NewMapObjectEncoder()
+ err := v.MarshalLogObject(m)
+ s.elems = append(s.elems, m.Fields)
+ return err
+}
+
+func (s *sliceArrayEncoder) AppendReflected(v interface{}) error {
+ s.elems = append(s.elems, v)
+ return nil
+}
+
+func (s *sliceArrayEncoder) AppendBool(v bool) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, string(v)) }
+func (s *sliceArrayEncoder) AppendComplex128(v complex128) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendComplex64(v complex64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendDuration(v time.Duration) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendFloat64(v float64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendFloat32(v float32) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt(v int) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt64(v int64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt32(v int32) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt16(v int16) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt8(v int8) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendString(v string) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendTime(v time.Time) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint(v uint) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint64(v uint64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint32(v uint32) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint16(v uint16) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint8(v uint8) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUintptr(v uintptr) { s.elems = append(s.elems, v) }
diff --git a/pub/common/m3u8/enkey.go b/pub/common/m3u8/enkey.go
new file mode 100644
index 0000000..407e155
--- /dev/null
+++ b/pub/common/m3u8/enkey.go
@@ -0,0 +1,63 @@
+package m3u8
+
+import (
+ "bytes"
+ "context"
+ "io/ioutil"
+ "sync"
+)
+
+var (
+ cacheEnkey []byte
+ cacheLsjEnkey []byte
+ initOnce sync.Once
+ cacheNewEnkey []byte
+ newEnkeyErr error
+ newEnkeyOnce sync.Once
+)
+
+func initEnkey() {
+ initOnce.Do(func() {
+ if len(cacheEnkey) == 0 {
+ data, err := ioutil.ReadFile("conf/m3u8.key")
+ if err != nil {
+ panic(err)
+ }
+ cacheEnkey = data
+ }
+ if len(cacheLsjEnkey) == 0 {
+ data, err := ioutil.ReadFile("conf/lsjm3u8.key")
+ if err != nil {
+ panic(err)
+ }
+ cacheLsjEnkey = data
+ }
+ })
+}
+
+func Enkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
+ if cacheEnkey == nil {
+ initEnkey()
+ }
+ buffer = bytes.NewBuffer(cacheEnkey)
+ return
+}
+
+// NewEnkey 返回短剧原片使用的解密密钥。
+func NewEnkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
+ newEnkeyOnce.Do(func() {
+ cacheNewEnkey, newEnkeyErr = ioutil.ReadFile("conf/newm3u8.key")
+ })
+ if newEnkeyErr != nil {
+ return nil, newEnkeyErr
+ }
+ return bytes.NewBuffer(cacheNewEnkey), nil
+}
+
+func LsjEnkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
+ if cacheLsjEnkey == nil {
+ initEnkey()
+ }
+ buffer = bytes.NewBuffer(cacheLsjEnkey)
+ return
+}
diff --git a/pub/common/m3u8/media_play_list.go b/pub/common/m3u8/media_play_list.go
new file mode 100644
index 0000000..60c6c7b
--- /dev/null
+++ b/pub/common/m3u8/media_play_list.go
@@ -0,0 +1,192 @@
+package m3u8
+
+import (
+ "net/url"
+ "path"
+ "strings"
+ "time"
+ "videoweb/pub/common/utils"
+
+ "github.com/grafov/m3u8"
+)
+
+func makeMediaPlayList(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
+ media, err = m3u8.NewMediaPlaylist(0, from.Count())
+ if err != nil {
+ return
+ }
+ base := utils.JoinUrlPath(cdnDomain, dirPath)
+ now := time.Now()
+
+ for _, segment := range from.Segments {
+ if segment == nil {
+ continue
+ }
+ sg := new(m3u8.MediaSegment)
+ if strings.Contains(dirPath, "laosiji") {
+ segment.URI = removeDomainPrefix(segment.URI)
+ }
+ sg.URI = utils.JoinUrlPath(base, segment.URI)
+ if len(authKey) > 0 {
+ urlPath := utils.JoinUrlPath(dirPath, segment.URI)
+ urlAuth := generateUrlAuth(now, urlPath, authKey)
+ sg.URI = sg.URI + urlAuth
+ }
+ sg.Title = segment.Title
+ sg.Duration = segment.Duration
+ if err = media.AppendSegment(sg); err != nil {
+ return
+ }
+ }
+ _ = media.SetDefaultKey("AES-128", keyUri, "", "", "")
+ media.Close()
+ return
+}
+
+func makeMediaPlayListLaosiji(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
+ media, err = m3u8.NewMediaPlaylist(0, from.Count())
+ if err != nil {
+ return
+ }
+
+ now := time.Now()
+ isFMP4 := from.Map != nil
+ media.SetVersion(from.Version())
+ if isFMP4 && media.Version() < 5 {
+ media.SetVersion(5)
+ }
+ media.SeqNo = from.SeqNo
+ media.MediaType = from.MediaType
+ media.DiscontinuitySeq = from.DiscontinuitySeq
+ media.StartTime = from.StartTime
+ media.StartTimePrecise = from.StartTimePrecise
+ media.Iframe = from.Iframe
+
+ if from.Map != nil {
+ mapURI, mapErr := rewriteLaosijiMediaURI(from.Map.URI, cdnDomain, dirPath, "")
+ if mapErr != nil {
+ return nil, mapErr
+ }
+ mapURI = appendLaosijiURLAuth(mapURI, now, authKey)
+ media.SetDefaultMap(mapURI, from.Map.Limit, from.Map.Offset)
+ }
+
+ for _, segment := range from.Segments {
+ if segment == nil {
+ continue
+ }
+
+ sg := new(m3u8.MediaSegment)
+ sg.URI, err = rewriteLaosijiMediaURI(segment.URI, cdnDomain, dirPath, fromMapURI(from))
+ if err != nil {
+ return nil, err
+ }
+ sg.URI = appendLaosijiURLAuth(sg.URI, now, authKey)
+ if from.Map == nil && segment.Map != nil {
+ segmentMapURI, mapErr := rewriteLaosijiMediaURI(segment.Map.URI, cdnDomain, dirPath, "")
+ if mapErr != nil {
+ return nil, mapErr
+ }
+ sg.Map = &m3u8.Map{
+ URI: appendLaosijiURLAuth(segmentMapURI, now, authKey),
+ Limit: segment.Map.Limit,
+ Offset: segment.Map.Offset,
+ }
+ isFMP4 = true
+ }
+ sg.Title = segment.Title
+ sg.Duration = segment.Duration
+ sg.Limit = segment.Limit
+ sg.Offset = segment.Offset
+ sg.Discontinuity = segment.Discontinuity
+ if err = media.AppendSegment(sg); err != nil {
+ return
+ }
+ }
+ if from.TargetDuration > media.TargetDuration {
+ media.TargetDuration = from.TargetDuration
+ }
+ if isFMP4 && media.Version() < 5 {
+ media.SetVersion(5)
+ }
+ // 老司机旧 TS 仍使用 AES-128 key;fMP4/HEVC 依赖 EXT-X-MAP,不能再强制插入 EXT-X-KEY。
+ if !isFMP4 {
+ _ = media.SetDefaultKey("AES-128", keyUri, "0x64cf692cebc516e4a86ac6081aae50ce", "", "")
+ }
+ media.Close()
+ return
+}
+
+func rewriteLaosijiMediaURI(originalURI, cdnDomain, playlistDir, mapURI string) (string, error) {
+ originalURI = strings.TrimSpace(originalURI)
+ if originalURI == "" {
+ return "", nil
+ }
+
+ parsed, err := url.Parse(originalURI)
+ if err != nil {
+ return "", err
+ }
+ if !parsed.IsAbs() {
+ if mapURL, mapErr := url.Parse(strings.TrimSpace(mapURI)); mapErr == nil && mapURL.IsAbs() {
+ base := *mapURL
+ base.RawQuery = ""
+ base.Fragment = ""
+ base.Path = path.Dir(base.Path) + "/"
+ parsed = base.ResolveReference(parsed)
+ } else {
+ cleanDir := strings.TrimPrefix(strings.TrimSpace(playlistDir), "/")
+ cleanDir = strings.TrimPrefix(cleanDir, "laosiji/m3m/")
+ cleanDir = strings.TrimPrefix(cleanDir, "laosiji/")
+ cleanDir = strings.TrimPrefix(cleanDir, "m3m/")
+ parsed.Path = path.Join("/", cleanDir, parsed.Path)
+ }
+ }
+
+ return replaceDomainAndPath(parsed.String(), utils.JoinUrlPath(cdnDomain, "/laosiji"))
+}
+
+func appendLaosijiURLAuth(mediaURL string, now time.Time, authKey string) string {
+ if mediaURL == "" || authKey == "" {
+ return mediaURL
+ }
+ signPath := mediaURL
+ if parsed, err := url.Parse(mediaURL); err == nil {
+ signPath = parsed.Path
+ }
+ return mediaURL + generateUrlAuth(now, signPath, authKey)
+}
+
+func fromMapURI(playlist *m3u8.MediaPlaylist) string {
+ if playlist == nil || playlist.Map == nil {
+ return ""
+ }
+ return playlist.Map.URI
+}
+
+func replaceDomainAndPath(originalURL, newBase string) (string, error) {
+ parsedURL, err := url.Parse(originalURL)
+ if err != nil {
+ return "", err
+ }
+
+ newBaseURL, err := url.Parse(newBase)
+ if err != nil {
+ return "", err
+ }
+
+ parsedURL.Scheme = newBaseURL.Scheme
+ parsedURL.Host = newBaseURL.Host
+ parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, ""))
+ parsedURL.RawQuery = ""
+
+ return parsedURL.String(), nil
+}
+
+func removeDomainPrefix(originalURL string) string {
+ parsedURL, err := url.Parse(originalURL)
+ if err != nil {
+ return originalURL
+ }
+ return strings.TrimPrefix(parsedURL.Path, "")
+}
diff --git a/pub/common/m3u8/play_list.go b/pub/common/m3u8/play_list.go
new file mode 100644
index 0000000..c72bd5d
--- /dev/null
+++ b/pub/common/m3u8/play_list.go
@@ -0,0 +1,199 @@
+package m3u8
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/http"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/redis"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/grafov/m3u8"
+)
+
+const (
+ keyUri = "/enkey"
+ webkeyUri = "/api/web/enkey"
+ h5KeyUri = "/enkey"
+ AuthKey = "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
+ keyUriLsj = "/lsjenkey"
+ keyUriWebLsj = "/api/web/lsjenkey"
+)
+
+// resolveTsAuthKey 优先使用传入的 tsAuthKey,为空则回退默认 AuthKey。
+func resolveTsAuthKey(tsAuthKey string) string {
+ if key := strings.TrimSpace(tsAuthKey); key != "" {
+ return key
+ }
+ return AuthKey
+}
+
+var headerMediaPlayList *m3u8.MediaPlaylist
+
+// TransFormWithSafely 被检查权限的播放列表。
+// tsAuthKey: 普通 TS CDN 鉴权;lsjM3u8Yuan: 老司机源站;cdnAuthKey: 老司机分片 CDN 鉴权。
+func TransFormWithSafely(req *Req, cdnDomain, origDomain string, web bool, tsAuthKey, lsjM3u8Yuan, cdnAuthKey string) (*m3u8.MediaPlaylist, *bytes.Buffer, error) {
+ if req.Name == "" {
+ return nil, nil, errors.MakeWithMsg(errors.M3u8FileName, "file name is empty")
+ }
+ if cdnDomain == "" || origDomain == "" {
+ return nil, nil, errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
+ }
+ if strings.Contains(req.Name, "hc237") {
+ req.Name = "/laosiji" + req.Name
+ }
+ if strings.Contains(req.Name, "laosiji") {
+ return TransformLaosiji(req.Name, cdnDomain, web, lsjM3u8Yuan, cdnAuthKey)
+ }
+ return transform(req.Name, cdnDomain, origDomain, web, tsAuthKey)
+}
+
+func TransformLaosiji(name, cdnDomain string, web bool, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ // fMP4/HEVC 分片 CDN 使用传入的视频 CDN(web 为 MediaWebCdn,app/h5 为 TsCdn),
+ // 与 mdsm GetCDNUrlList()[0] 一致;拉取源 m3u8 仍走 lsjM3u8Yuan。
+ m3u8reader, err := readFromRedis(laosijiUpstreamPath(name), lsjM3u8Yuan)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+
+ authKey := strings.TrimSpace(cdnAuthKey)
+ keyurl := keyUriLsj
+ if web {
+ keyurl = keyUriWebLsj
+ }
+ if len(customKeyUri) > 0 && customKeyUri[0] != "" {
+ keyurl = customKeyUri[0]
+ }
+
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ dirPath := utils.PathForInfo(name).Dir
+ media, err = makeMediaPlayListLaosiji(pl, cdnDomain, dirPath, keyurl, authKey)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ mpl := p.(*m3u8.MasterPlaylist)
+ buff = mpl.Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+func transform(name, cdnDomain, origDomain string, web bool, tsAuthKey string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ mediaVerson := proxy.AppendsMediaS3Version(name)
+ if len(mediaVerson) > 0 {
+ name = mediaVerson + name
+ }
+
+ m3u8reader, err := readFromRedis(name, origDomain)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+
+ authKey := resolveTsAuthKey(tsAuthKey)
+ keyurl := keyUri
+ if web {
+ keyurl = webkeyUri
+ }
+
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ dirPath := utils.PathForInfo(name).Dir
+ media, err = makeMediaPlayList(pl, cdnDomain, dirPath, keyurl, authKey)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ mpl := p.(*m3u8.MasterPlaylist)
+ buff = mpl.Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+func readFromCDN(name, domain string) (reader io.Reader, err error) {
+ requestUrl := utils.JoinUrlPath(domain, name)
+ resp, err := http.GetLargeFile(requestUrl, nil)
+ if err != nil {
+ return
+ }
+ if resp.StatusCode != 200 {
+ msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
+ err = errors.MakeWithMsg(errors.M3u8Read, msg)
+ return
+ }
+ reader = bytes.NewReader(resp.Data)
+ return
+}
+
+func readFromRedis(name, domain string) (reader io.Reader, err error) {
+ m3u8Key := redisconst.MediaM3U8Key(name)
+ data, err := redis.GetBytes(m3u8Key)
+ if err == nil && len(data) > 0 {
+ reader = bytes.NewReader(data)
+ return
+ }
+ if strings.Contains(name, "hc237") {
+ name = "/m3m/" + name
+ }
+ requestUrl := utils.JoinUrlPath(domain, name)
+ resp, err := http.GetLargeFile(requestUrl, nil)
+ if err != nil {
+ return
+ }
+ if resp.StatusCode != 200 {
+ msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
+ err = errors.MakeWithMsg(errors.M3u8Read, msg)
+ return
+ }
+ if err := redis.Set(m3u8Key, resp.Data,
+ redisconst.MediaM3U8CacheExpire); err != nil {
+ log.Info("m3u8:readFromRedis redis set err", log.E(err))
+ }
+ reader = bytes.NewReader(resp.Data)
+ return
+}
+
+func GetHeaderMediaSegUrls() []string {
+ urls := make([]string, 0)
+ if headerMediaPlayList == nil {
+ return urls
+ }
+ for _, segment := range headerMediaPlayList.Segments {
+ if segment == nil {
+ continue
+ }
+ urls = append(urls, segment.URI)
+ }
+ return urls
+}
+
+// laosijiUpstreamPath maps internal laosiji m3u8 path to upstream m3m path.
+func laosijiUpstreamPath(name string) string {
+ name = strings.TrimPrefix(name, "/")
+ name = strings.TrimPrefix(name, "laosiji/m3m/")
+ name = strings.TrimPrefix(name, "laosiji/")
+ return "m3m/" + name
+}
diff --git a/pub/common/m3u8/transcode_source.go b/pub/common/m3u8/transcode_source.go
new file mode 100644
index 0000000..359d66f
--- /dev/null
+++ b/pub/common/m3u8/transcode_source.go
@@ -0,0 +1,116 @@
+package m3u8
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/grafov/m3u8"
+)
+
+const lsjKeyIV = "0x64cf692cebc516e4a86ac6081aae50ce"
+
+// TransformForTranscode 生成转码专用播放列表,供老司机转码服务器拉取。
+func TransformForTranscode(name, origDomain, cdnDomain, webM3u8Host, lsjM3u8Yuan, lsjTsYuan string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ if strings.Contains(name, "hc237") {
+ name = "/laosiji" + name
+ }
+ if strings.Contains(name, "laosiji") {
+ return transcodeLaosiji(name, webM3u8Host, lsjM3u8Yuan, lsjTsYuan)
+ }
+ return transcodeNormal(name, origDomain, cdnDomain, webM3u8Host)
+}
+
+func transcodeLaosiji(name, webM3u8Host, lsjM3u8Yuan, lsjTsYuan string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ m3u8reader, err := readFromRedis(laosijiUpstreamPath(name), lsjM3u8Yuan)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ keyURI := utils.JoinUrlPath(webM3u8Host, keyUriWebLsj)
+ // ts 走 CDN(TsYuan/laosiji),与正常播放的 CDN 路径一致。
+ media, err = makeTranscodeSegList(pl, utils.JoinUrlPath(lsjTsYuan, "/laosiji"), "", keyURI, lsjKeyIV, true)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ buff = p.(*m3u8.MasterPlaylist).Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+func transcodeNormal(name, origDomain, cdnDomain, webM3u8Host string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ if v := proxy.AppendsMediaS3Version(name); len(v) > 0 {
+ name = v + name
+ }
+ m3u8reader, err := readFromRedis(name, origDomain)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ dirPath := utils.PathForInfo(name).Dir
+ keyURI := utils.JoinUrlPath(webM3u8Host, webkeyUri)
+ // ts 走 CDN(CdnDomain),与正常播放一致。
+ media, err = makeTranscodeSegList(pl, cdnDomain, dirPath, keyURI, "", false)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ buff = p.(*m3u8.MasterPlaylist).Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+func makeTranscodeSegList(from *m3u8.MediaPlaylist, srcDomain, dirPath, keyURI, iv string, laosiji bool) (media *m3u8.MediaPlaylist, err error) {
+ media, err = m3u8.NewMediaPlaylist(0, from.Count())
+ if err != nil {
+ return
+ }
+ base := utils.JoinUrlPath(srcDomain, dirPath)
+ for _, segment := range from.Segments {
+ if segment == nil {
+ continue
+ }
+ sg := new(m3u8.MediaSegment)
+ if laosiji {
+ if u, e := replaceDomainAndPath(segment.URI, srcDomain); e == nil {
+ sg.URI = u
+ } else {
+ sg.URI = segment.URI
+ }
+ } else {
+ sg.URI = utils.JoinUrlPath(base, segment.URI)
+ }
+ sg.Title = segment.Title
+ sg.Duration = segment.Duration
+ if err = media.AppendSegment(sg); err != nil {
+ return
+ }
+ }
+ _ = media.SetDefaultKey("AES-128", keyURI, iv, "", "")
+ media.Close()
+ return
+}
diff --git a/pub/common/m3u8/transformer.go b/pub/common/m3u8/transformer.go
new file mode 100644
index 0000000..b40bc82
--- /dev/null
+++ b/pub/common/m3u8/transformer.go
@@ -0,0 +1,152 @@
+package m3u8
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/grafov/m3u8"
+)
+
+// TransformEx m3u8 文件转换。
+// apiType(1:app,2:h5app,3:web)
+// authKey: TS CDN 鉴权(空则回退默认 AuthKey)
+// lsjM3u8Yuan / cdnAuthKey: 老司机源站与分片鉴权
+// customKeyUri: 非空时直接使用,用于将鉴权参数嵌入 EXT-X-KEY URI
+func TransformEx(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (
+ media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ return transformEx(name, cdnDomain, origDomain, apiType, authKey, lsjM3u8Yuan, cdnAuthKey, true, customKeyUri...)
+}
+
+// TransformExOriginalPath 按库里的原片路径读取 m3u8,不自动追加 v1/v2/v3。
+func TransformExOriginalPath(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (
+ media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ return transformEx(name, cdnDomain, origDomain, apiType, authKey, lsjM3u8Yuan, cdnAuthKey, false, customKeyUri...)
+}
+
+func transformEx(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, appendMediaVersion bool, customKeyUri ...string) (
+ media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+
+ if name == "" {
+ err = errors.MakeWithMsg(errors.M3u8FileName, "m3u8 file name is empty")
+ return
+ }
+ if cdnDomain == "" || origDomain == "" {
+ err = errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
+ return
+ }
+ if strings.Contains(name, "hc237") {
+ name = "/laosiji" + name
+ }
+ if strings.Contains(name, "laosiji") {
+ return TransformLaosiji(name, cdnDomain, false, lsjM3u8Yuan, cdnAuthKey, customKeyUri...)
+ }
+
+ if appendMediaVersion {
+ mediaVerson := proxy.AppendsMediaS3Version(name)
+ if len(mediaVerson) > 0 {
+ name = mediaVerson + name
+ }
+ }
+ authKey = resolveTsAuthKey(authKey)
+
+ m3u8reader, err := readFromRedis(name, origDomain)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+ apiPath := keyUri
+ if len(customKeyUri) > 0 && customKeyUri[0] != "" {
+ apiPath = customKeyUri[0]
+ } else if apiType == 2 {
+ apiPath = h5KeyUri
+ } else if apiType == 3 {
+ apiPath = webkeyUri
+ }
+
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ dirPath := utils.PathForInfo(name).Dir
+ media, err = makeTSList(pl, cdnDomain, dirPath, apiPath, authKey)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ mpl := p.(*m3u8.MasterPlaylist)
+ buff = mpl.Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+func makeTSList(from *m3u8.MediaPlaylist, cdnDomain string,
+ dirPath string, apiPath string, authKey string) (
+ media *m3u8.MediaPlaylist, err error) {
+ media, err = m3u8.NewMediaPlaylist(from.WinSize(), from.Count())
+ if err != nil {
+ return
+ }
+ now := time.Now()
+ base := utils.JoinUrlPath(cdnDomain, dirPath)
+ for _, segment := range from.Segments {
+ if segment == nil {
+ continue
+ }
+ sg := new(m3u8.MediaSegment)
+ sg.URI = utils.JoinUrlPath(base, segment.URI)
+ if len(authKey) > 0 {
+ urlPath := utils.JoinUrlPath(dirPath, segment.URI)
+ urlAuth := generateUrlAuth(now, urlPath, authKey)
+ sg.URI = sg.URI + urlAuth
+ }
+ sg.Title = segment.Title
+ sg.Duration = segment.Duration
+ if err = media.AppendSegment(sg); err != nil {
+ return
+ }
+ }
+ _ = media.SetDefaultKey("AES-128", apiPath, "", "", "")
+ media.Close()
+ return
+}
+
+func generateUrlAuth(now time.Time, path string, authKey string) string {
+ timestamp := now.Unix()
+ signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey)
+ md5Str := getMD5Sign(signStr)
+ // TS CDN 鉴权参数后追加应用标识,便于流量统计。
+ urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d", timestamp, md5Str, pubdef.AppId)
+ return urlAuth
+}
+
+func generateUrlAuthQiniu(now time.Time, path string, authKey string) string {
+ timestamp := strconv.FormatInt(now.Add(2*time.Hour).Unix(), 16)
+ base64 := path
+ signStr := authKey + base64 + timestamp
+ md5Str := getMD5Sign(signStr)
+ urlAuth := fmt.Sprintf("?sign=%s&t=%s", md5Str, timestamp)
+ return urlAuth
+}
+
+func getMD5Sign(buf string) string {
+ md5Ctx := md5.New()
+ md5Ctx.Write([]byte(buf))
+ cipherStr := md5Ctx.Sum(nil)
+ nsign := hex.EncodeToString(cipherStr)
+ return nsign
+}
diff --git a/pub/common/m3u8/types.go b/pub/common/m3u8/types.go
new file mode 100644
index 0000000..97a1614
--- /dev/null
+++ b/pub/common/m3u8/types.go
@@ -0,0 +1,13 @@
+package m3u8
+
+type Req struct {
+ Token string `json:"token" form:"token"`
+ VId int64 `json:"vId" form:"vId"`
+ Name string `json:"name" form:"name"`
+ NeedHeader bool `json:"needHeader" form:"needHeader"`
+}
+
+type PostVideoReq struct {
+ Token string `json:"token" form:"token"`
+ Name string `json:"name" form:"name" binding:"required" comment:"must be set"`
+}
diff --git a/pub/common/m3u8/validate.go b/pub/common/m3u8/validate.go
new file mode 100644
index 0000000..a7b4927
--- /dev/null
+++ b/pub/common/m3u8/validate.go
@@ -0,0 +1,18 @@
+package m3u8
+
+//sErr "sz-app-server/pub/common/errors"
+
+// func CheckParams(req *Req) (err error) {
+// if req.Token == "" {
+// err = sErr.MakeWithMsg(sErr.NoToken, "no token")
+// return
+// }
+// if req.VId <= 0 {
+// err = sErr.MakeWithMsg(sErr.VideoId, "invalid video id")
+// return
+// }
+// if req.Name == "" {
+// err = sErr.MakeWithMsg(sErr.VideoName, "invalid name")
+// }
+// return
+// }
diff --git a/pub/common/m3u8/vtt.go b/pub/common/m3u8/vtt.go
new file mode 100644
index 0000000..c08beb3
--- /dev/null
+++ b/pub/common/m3u8/vtt.go
@@ -0,0 +1,96 @@
+package m3u8
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "strings"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/storage/proxy"
+)
+
+// TransformVtt 读取雪碧图 vtt 文件,并把其中的缩略图路径替换为图片 cdn 地址。
+// 兼容现有 /laosiji 路径资源:laosiji 的 vtt 需移除 /laosiji 前缀后,从 lsjImageYuan 源站读取。
+// imgCdn 图片 cdn 域名(缩略图访问地址)
+// origDomain 普通媒资的源站域名
+// lsjImageYuan 老司机图片源站域名(由 app/web 项目从配置 lsjMediaConf.imageYuan 传入)
+func TransformVtt(name, imgCdn, origDomain, lsjImageYuan string) (buff []byte, err error) {
+ if name == "" {
+ err = errors.MakeWithMsg(errors.M3u8FileName, "vtt file name is empty")
+ return
+ }
+ if imgCdn == "" || origDomain == "" {
+ err = errors.MakeWithMsg(errors.DomainErr, "imgCdn or origDomain is empty")
+ return
+ }
+
+ mediaVersion := proxy.AppendsMediaS3Version(name)
+ if len(mediaVersion) > 0 {
+ name = mediaVersion + name
+ }
+
+ originName := name
+ if strings.Contains(name, "laosiji") {
+ if lsjImageYuan == "" {
+ err = errors.MakeWithMsg(errors.DomainErr, "lsjImageYuan is empty")
+ return
+ }
+ originName = stripLaosijiPrefix(name)
+ origDomain = lsjImageYuan
+ }
+
+ srcReader, err := readFromRedis(originName, origDomain)
+ if err != nil {
+ return
+ }
+ raw, err := io.ReadAll(srcReader)
+ if err != nil {
+ return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt reader fail: "+err.Error())
+ }
+
+ dirPath := utils.PathForInfo(name).Dir
+ buff = rewriteVttWithCDN(raw, imgCdn, dirPath)
+ return
+}
+
+func stripLaosijiPrefix(name string) string {
+ p := strings.TrimPrefix(name, "/")
+ p = strings.TrimPrefix(p, "laosiji")
+ p = strings.TrimPrefix(p, "/")
+ return p
+}
+
+func rewriteVttWithCDN(vttData []byte, imgCdn, dirPath string) []byte {
+ imgCdn = strings.TrimRight(strings.TrimSpace(imgCdn), "/")
+
+ var out bytes.Buffer
+ scanner := bufio.NewScanner(bytes.NewReader(vttData))
+ scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ trim := strings.TrimSpace(line)
+
+ if idx := strings.Index(trim, "#xywh="); idx >= 0 {
+ p := strings.TrimSpace(trim[:idx])
+ xywh := trim[idx:]
+
+ var newPath string
+ switch {
+ case strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://"):
+ newPath = p
+ case strings.HasPrefix(p, "/"):
+ newPath = imgCdn + p
+ default:
+ newPath = utils.JoinUrlPath(utils.JoinUrlPath(imgCdn, strings.TrimRight(dirPath, "/")), p)
+ }
+ out.WriteString(newPath + xywh + "\n")
+ continue
+ }
+
+ out.WriteString(line + "\n")
+ }
+
+ return out.Bytes()
+}
diff --git a/pub/common/maths/maths.go b/pub/common/maths/maths.go
new file mode 100644
index 0000000..523f9bd
--- /dev/null
+++ b/pub/common/maths/maths.go
@@ -0,0 +1,68 @@
+package maths
+
+import (
+ "math/big"
+ "strings"
+
+ uuid "github.com/satori/go.uuid"
+)
+
+const CODE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+const CodeLenth = 62
+
+/**
+ * 10进制 为 62进制 字符串。大数字类型
+ *
+ * param: *big.Int number 待转换的数字,非常大的
+ * return: string
+ */
+func From10To62Big(number *big.Int) string {
+ tmpNumber := big.NewInt(0)
+ var result []byte
+ for number.Cmp(big.NewInt(0)) != 0 {
+ var tmp []byte
+ tmpNumber.Rem(number, big.NewInt(CodeLenth))
+ tmp = append(tmp, CODE62[tmpNumber.Int64()])
+ result = append(tmp, result...)
+ number.Div(number, big.NewInt(CodeLenth))
+ }
+ return string(result)
+}
+
+/**
+ * 10进制 为 62进制 字符串。大数字类型
+ *
+ * param: int64 num 待转换的数字
+ * return: string
+ */
+func From10To62(number int64) string {
+ var result []byte
+ for number != 0 {
+ var tmp []byte
+ number1 := number % CodeLenth
+ tmp = append(tmp, CODE62[number1])
+ result = append(tmp, result...)
+ number = number / CodeLenth
+ }
+ return string(result)
+}
+
+func GenUUID() string {
+ u1 := uuid.Must(uuid.NewV4(), nil).String()
+ uuidStr := strings.Replace(u1, "-", "", -1)
+ number, _ := new(big.Int).SetString(uuidStr, 16)
+ return From10To62Big(number)
+}
+
+func Abs(n int64) int64 {
+ if n < 0 {
+ return -n
+ }
+ return n
+}
+
+func Gen32String() string {
+ u1 := uuid.Must(uuid.NewV4(), nil).String()
+ uuidStr := strings.Replace(u1, "-", "", -1)
+ return uuidStr
+}
diff --git a/pub/common/netutil/netutil.go b/pub/common/netutil/netutil.go
new file mode 100644
index 0000000..5edf224
--- /dev/null
+++ b/pub/common/netutil/netutil.go
@@ -0,0 +1,55 @@
+package netutil
+
+import (
+ "net"
+ "net/url"
+ "path"
+ "strings"
+)
+
+func GetLocalIP() string {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return ""
+ }
+ for _, address := range addrs {
+ if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+ if ipnet.IP.To4() != nil {
+ return ipnet.IP.String()
+ }
+ }
+ }
+ return ""
+}
+
+func MergeURL(baseURL, relativePath string) string {
+ if relativePath == "" {
+ return ""
+ }
+ // 解析基础URL
+ base, err := url.Parse(baseURL)
+ if err != nil {
+ return ""
+ }
+
+ // 如果相对路径是绝对URL,直接返回
+ if strings.HasPrefix(relativePath, "http://") || strings.HasPrefix(relativePath, "https://") {
+ return relativePath
+ }
+
+ // 合并路径
+ mergedPath := path.Join(base.Path, relativePath)
+
+ // 处理路径中的 ./ 和 ../
+ mergedPath = path.Clean(mergedPath)
+
+ // 确保路径以/开头(如果原始base.Path以/结尾)
+ if strings.HasSuffix(base.Path, "/") && !strings.HasPrefix(mergedPath, "/") {
+ mergedPath = "/" + mergedPath
+ }
+
+ // 更新URL的Path部分
+ base.Path = mergedPath
+
+ return base.String()
+}
diff --git a/pub/common/netutil/netutil_test.go b/pub/common/netutil/netutil_test.go
new file mode 100644
index 0000000..fc87a5a
--- /dev/null
+++ b/pub/common/netutil/netutil_test.go
@@ -0,0 +1,10 @@
+package netutil
+
+import (
+ "fmt"
+ "testing"
+)
+
+func Test_GetLocalIP(t *testing.T) {
+ fmt.Println(GetLocalIP())
+}
diff --git a/pub/common/pageopt/match.go b/pub/common/pageopt/match.go
new file mode 100644
index 0000000..e6185af
--- /dev/null
+++ b/pub/common/pageopt/match.go
@@ -0,0 +1,580 @@
+package pageopt
+
+import (
+ "reflect"
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// A A from Array
+type A = []interface{}
+
+// M M from Map
+type M = bson.M
+
+// *******************************************//
+// Matcher
+type Matcher interface {
+ Filter() M
+}
+
+// MergeM
+func MergeM(mats []Matcher) M {
+ merge := M{}
+ for _, m := range mats {
+ if m == nil {
+ continue
+ }
+ ql := m.Filter()
+ for k, v := range ql {
+ merge[k] = v
+ }
+ }
+ return merge
+}
+
+// *******************************************//
+type MergeMatch struct {
+ merge M
+}
+
+func NewMergeMatch(mats ...Matcher) Matcher {
+ mergeM := MergeM(mats)
+ return &MergeMatch{mergeM}
+}
+
+func (m *MergeMatch) Filter() M {
+ return m.merge
+}
+
+// *******************************************//
+// AssignMatch
+type AssignMatch struct {
+ Key string
+ Val interface{}
+}
+
+func NewAssignMatch(key string, val interface{}) Matcher {
+ return &AssignMatch{
+ key,
+ val,
+ }
+}
+
+func (m *AssignMatch) Filter() M {
+ if IsEmpt(m.Val) {
+ return M{}
+ }
+ return M{
+ m.Key: m.Val,
+ }
+}
+
+// *******************************************//
+
+type Sort = bson.D
+
+func SortDesc(Key string) Sort {
+ return Sort{{Key, -1}}
+}
+
+func SortAsc(Key string) Sort {
+ return Sort{{Key, 1}}
+}
+
+// *******************************************//
+// RegexMatch
+type RegexMatch struct {
+ Key string
+ Val interface{}
+ Options string
+}
+
+func NewRegexMatch(key string, val interface{}, opt string) Matcher {
+ return &RegexMatch{
+ Key: key,
+ Val: val,
+ Options: opt,
+ }
+}
+
+func (r *RegexMatch) Filter() M {
+ if IsEmpt(r.Val) {
+ return M{}
+ }
+ return M{
+ r.Key: M{"$regex": r.Val, "$options": r.Options},
+ }
+}
+
+// *******************************************//
+func IsNil(i interface{}) bool {
+ vi := reflect.ValueOf(i)
+ return vi.IsNil()
+}
+
+func IsEmpt(i interface{}) bool {
+ v := reflect.ValueOf(i)
+ switch v.Kind() {
+ case reflect.Ptr:
+ return v.IsNil()
+ case reflect.Slice, reflect.Array:
+ return v.Len() == 0
+ }
+ return false
+}
+
+// *******************************************//
+
+type NEMatch struct {
+ Key string
+ NE interface{}
+}
+
+func (m *NEMatch) Filter() M {
+ if IsEmpt(m.NE) {
+ return M{}
+ }
+ return M{m.Key: M{"$ne": m.NE}}
+}
+
+func NewNEMatch(Key string, ne interface{}) Matcher {
+ return &NEMatch{
+ Key: Key,
+ NE: ne,
+ }
+}
+
+// *******************************************//
+// GEMatch
+type GEMatch struct {
+ Key string
+ GT interface{}
+}
+
+func NewGEMatch(Key string, gt interface{}) Matcher {
+ return &GEMatch{
+ Key: Key,
+ GT: gt,
+ }
+}
+
+func (m *GEMatch) Filter() M {
+ if IsEmpt(m.GT) {
+ return M{}
+ }
+ return M{m.Key: M{"$gt": m.GT}}
+}
+
+// *******************************************//
+// GTEMatch
+type GTEMatch struct {
+ Key string
+ GTE interface{}
+}
+
+func NewGTEMatch(Key string, gte interface{}) Matcher {
+ return >EMatch{
+ Key: Key,
+ GTE: gte,
+ }
+}
+
+func (m *GTEMatch) Filter() M {
+ if IsEmpt(m.GTE) {
+ return M{}
+ }
+ return M{m.Key: M{"$gte": m.GTE}}
+}
+
+// *******************************************//
+
+// GTMatch
+type GTMatch struct {
+ Key string
+ GT interface{}
+}
+
+func NewGTMatch(Key string, gt interface{}) Matcher {
+ return >Match{
+ Key: Key,
+ GT: gt,
+ }
+}
+
+func (m *GTMatch) Filter() M {
+ if IsEmpt(m.GT) {
+ return M{}
+ }
+ return M{m.Key: M{"$gt": m.GT}}
+}
+
+// *******************************************//
+// LTMatch
+type LTMatch struct {
+ Key string
+ LT interface{}
+}
+
+func NewLTMatch(Key string, lt interface{}) Matcher {
+ return <Match{
+ Key: Key,
+ LT: lt,
+ }
+}
+
+func (m *LTMatch) Filter() M {
+ if IsEmpt(m.LT) {
+ return M{}
+ }
+ return M{m.Key: M{"$lt": m.LT}}
+}
+
+// *******************************************//
+
+// LTEMatch
+type LTEMatch struct {
+ Key string
+ LTE interface{}
+}
+
+func NewLTEMatch(Key string, lte interface{}) Matcher {
+ return <EMatch{
+ Key: Key,
+ LTE: lte,
+ }
+}
+
+func (m *LTEMatch) Filter() M {
+ if IsEmpt(m.LTE) {
+ return M{}
+ }
+ return M{m.Key: M{"$lte": m.LTE}}
+}
+
+// *******************************************//
+// GTEAndLTEMatch
+type GTEAndLTEMatch struct {
+ Key string
+ GTE interface{}
+ LTE interface{}
+}
+
+func NewGTEAndLTEMatch(Key string, gte, lte interface{}) Matcher {
+ return >EAndLTEMatch{
+ Key,
+ gte,
+ lte,
+ }
+}
+
+func (m *GTEAndLTEMatch) Filter() M {
+ if IsEmpt(m.GTE) || IsEmpt(m.LTE) {
+ return M{}
+ }
+
+ return M{
+ m.Key: M{
+ "$gte": m.GTE,
+ "$lte": m.LTE,
+ },
+ }
+}
+
+// *******************************************//
+// GTEAndLTMatch
+type GTEAndLTMatch struct {
+ Key string
+ GTE interface{}
+ LT interface{}
+}
+
+func NewGTEAndLTMatch(key string, gte, lt interface{}) Matcher {
+ return >EAndLTMatch{
+ key,
+ gte,
+ lt,
+ }
+}
+
+func (m *GTEAndLTMatch) Filter() M {
+ if IsEmpt(m.GTE) || IsEmpt(m.LT) {
+ return M{}
+ }
+
+ return M{
+ m.Key: M{
+ "$gte": m.GTE,
+ "$lt": m.LT,
+ },
+ }
+}
+
+// *******************************************//
+
+// ExistsMatch
+type ExistsMatch struct {
+ Key string
+ IsExisted *bool
+}
+
+func NewExistsMatch(key string, isExisted *bool) Matcher {
+ return &ExistsMatch{
+ Key: key,
+ IsExisted: isExisted,
+ }
+}
+
+func (e *ExistsMatch) Filter() M {
+ if IsEmpt(e.IsExisted) {
+ return M{}
+ }
+ return M{
+ e.Key: M{"$exists": e.IsExisted},
+ }
+}
+
+// *******************************************//
+// InMatch
+type InMatch struct {
+ Key string
+ Slice interface{}
+}
+
+func NewInMatch(key string, slice interface{}) Matcher {
+ return &InMatch{
+ Key: key,
+ Slice: slice,
+ }
+}
+
+func (i *InMatch) Filter() M {
+ if IsNil(i.Slice) {
+ return M{}
+ }
+ return M{
+ i.Key: M{"$in": i.Slice},
+ }
+}
+
+// *******************************************//
+// NinMatch
+type NinMatch struct {
+ Key string
+ Slice interface{}
+}
+
+func NewNinMatch(key string, slice interface{}) Matcher {
+ return &NinMatch{
+ Key: key,
+ Slice: slice,
+ }
+}
+
+// *******************************************//
+func (n *NinMatch) Filter() M {
+ if IsNil(n.Slice) {
+ return M{}
+ }
+ return M{
+ n.Key: M{"$nin": n.Slice},
+ }
+}
+
+// *******************************************//
+// ManualOrMatch
+type ManualOrMatch struct {
+ Key string
+ Mats []Matcher
+}
+
+func NewManualOrMatch(key string, mats []Matcher) Matcher {
+ return &ManualOrMatch{
+ Key: key,
+ Mats: mats,
+ }
+}
+
+func (o *ManualOrMatch) Filter() M {
+ if len(o.Mats) == 0 {
+ return M{}
+ }
+ fs := []interface{}{}
+ for _, v := range o.Mats {
+ f := v.Filter()
+ if len(f) != 0 {
+ fs = append(fs, v.Filter())
+ }
+ }
+ if len(fs) == 0 {
+ return M{}
+ }
+ return M{"$or": fs}
+}
+
+// *******************************************//
+// BitsAllSetMatch
+type BitsAllSetMatch struct {
+ Key string
+ Bits []int
+}
+
+func NewBitsAllSetMatch(key string, bits []int) Matcher {
+ return &BitsAllSetMatch{
+ Key: key,
+ Bits: bits,
+ }
+}
+
+func (b *BitsAllSetMatch) Filter() M {
+ if len(b.Bits) == 0 {
+ return M{}
+ }
+ return M{
+ b.Key: M{"$bitsAllSet": b.Bits},
+ }
+}
+
+// BitsAllClearMatch
+type BitsAllClearMatch struct {
+ Key string
+ Bits []int
+}
+
+// *******************************************//
+func NewBitsAllClearMatch(key string, bits []int) Matcher {
+ return &BitsAllClearMatch{
+ Key: key,
+ Bits: bits,
+ }
+}
+
+func (b *BitsAllClearMatch) Filter() M {
+ if len(b.Bits) == 0 {
+ return M{}
+ }
+ return M{
+ b.Key: M{"$bitsAllClear": b.Bits},
+ }
+}
+
+// *******************************************//
+
+// UIDMatch
+type UIDMatch struct {
+ UID *int64
+}
+
+func (u *UIDMatch) New() Matcher {
+ return NewAssignMatch("userId", u.UID)
+}
+
+// UIDInMatch
+type UIDInMatch struct {
+ UIDS []int64
+}
+
+func (u *UIDInMatch) New() Matcher {
+ return NewInMatch("userId", u.UIDS)
+}
+
+// *******************************************//
+// UIDMatch
+type UserUIDMatch struct {
+ UID *int64
+}
+
+func (u *UserUIDMatch) New() Matcher {
+ return NewAssignMatch("id", u.UID)
+}
+
+// UIDInMatch
+type UserUIDInMatch struct {
+ UIDS []int64
+}
+
+func (u *UserUIDInMatch) New() Matcher {
+ return NewInMatch("id", u.UIDS)
+}
+
+// *******************************************//
+
+// IDMatch
+type IDMatch struct {
+ ID *primitive.ObjectID
+}
+
+func (i *IDMatch) New() Matcher {
+ return NewAssignMatch("_id", i.ID)
+}
+
+// IDMatch
+type IDint64Match struct {
+ Id *int64
+}
+
+func (i *IDint64Match) New() Matcher {
+ return NewAssignMatch("id", i.Id)
+}
+
+// *******************************************//
+// IDInMatch
+type IDInMatch struct {
+ IDs []primitive.ObjectID
+}
+
+func (c *IDInMatch) New() Matcher {
+ return NewInMatch("_id", c.IDs)
+}
+
+// *******************************************//
+// CreatedAtGTEMatch
+type CreatedAtGTEMatch struct {
+ GTE *time.Time
+}
+
+func (c *CreatedAtGTEMatch) New() Matcher {
+ return NewGTEMatch("createdAt", c.GTE)
+}
+
+// *******************************************//
+// CreatedAtLTMatch
+type CreatedAtLTMatch struct {
+ LT *time.Time
+}
+
+func (c *CreatedAtLTMatch) New() Matcher {
+ return NewLTMatch("createdAt", c.LT)
+}
+
+// *******************************************//
+// CreatedAtGTEAndLTMatch
+type CreatedAtGTEAndLTMatch struct {
+ GTE *time.Time
+ LT *time.Time
+}
+
+func (c *CreatedAtGTEAndLTMatch) New() Matcher {
+ return NewGTEAndLTMatch("createdAt", c.GTE, c.LT)
+}
+
+// // *******************************************//
+// type Int32Match struct {
+// key string //值
+// value *int32
+// }
+//
+// func (i *Int32Match) New() Matcher {
+// return NewAssignMatch("_id", i.ID)
+// }
+//
+// type Int32InMatch struct {
+// key string //值
+// values []int32
+// }
+//
+// func (c *Int32InMatch) New() Matcher {
+// return NewInMatch("_id", c.IDs)
+// }
diff --git a/pub/common/playletcache/cache.go b/pub/common/playletcache/cache.go
new file mode 100644
index 0000000..fb3818f
--- /dev/null
+++ b/pub/common/playletcache/cache.go
@@ -0,0 +1,39 @@
+package playletcache
+
+import (
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/redis"
+)
+
+// ClearHomeCache 切换缓存版本,旧数据由 TTL 自动过期
+func ClearHomeCache() {
+ if redis.Client() == nil {
+ return
+ }
+ if err := redis.Client().Incr(redisconst.AppPlayletHomeCacheVersionKey()).Err(); err != nil {
+ log.Warn("更新短剧首页缓存版本失败", log.E(err))
+ }
+}
+
+// ClearRankCache 更新短剧榜单缓存版本
+func ClearRankCache() {
+ if redis.Client() == nil {
+ return
+ }
+ if err := redis.Client().Incr(redisconst.AppPlayletRankCacheVersionKey()).Err(); err != nil {
+ log.Warn("更新短剧榜单缓存版本失败", log.E(err))
+ }
+}
+
+// RankCacheVersion 查询短剧榜单缓存版本
+func RankCacheVersion() int64 {
+ if redis.Client() == nil {
+ return 0
+ }
+ version, err := redis.Client().Get(redisconst.AppPlayletRankCacheVersionKey()).Int64()
+ if err != nil {
+ return 0
+ }
+ return version
+}
diff --git a/pub/common/pointers/pointers.go b/pub/common/pointers/pointers.go
new file mode 100644
index 0000000..908c79d
--- /dev/null
+++ b/pub/common/pointers/pointers.go
@@ -0,0 +1,83 @@
+package pointers
+
+import "time"
+
+func Bool(v bool) *bool { return &v }
+func Int(v int) *int { return &v }
+func Int32(v int32) *int32 { return &v }
+func Int64(v int64) *int64 { return &v }
+func Uint32(v uint32) *uint32 { return &v }
+func Uint64(v uint64) *uint64 { return &v }
+func Float32(v float32) *float32 { return &v }
+func Float64(v float64) *float64 { return &v }
+func String(v string) *string { return &v }
+func Time(v time.Time) *time.Time { return &v }
+func StringSlice(v []string) *[]string { return &v }
+func Int64Slice(v []int64) *[]int64 { return &v }
+
+func GetBool(v *bool) bool {
+ if v != nil {
+ return *v
+ }
+ return false
+}
+func GetInt(v *int) int {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetInt32(v *int32) int32 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetInt64(v *int64) int64 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetInt64Slice(v *[]int64) []int64 {
+ if v != nil {
+ return *v
+ }
+ return []int64{}
+}
+func GetUint32(v *uint32) uint32 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetUint64(v *uint64) uint64 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetFloat32(v *float32) float32 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetFloat64(v *float64) float64 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetString(v *string) string {
+ if v != nil {
+ return *v
+ }
+ return ""
+}
+func GetStringSlice(v *[]string) []string {
+ if v != nil {
+ return *v
+ }
+ return []string{}
+}
diff --git a/pub/common/random/number.go b/pub/common/random/number.go
new file mode 100644
index 0000000..af0d5c3
--- /dev/null
+++ b/pub/common/random/number.go
@@ -0,0 +1,53 @@
+package random
+
+import (
+ "fmt"
+ "math/rand"
+ "time"
+)
+
+const letterBytesAll = "0123456789abcdefghijklmnopqrstvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func init() {
+ rand.Seed(time.Now().Unix())
+}
+
+func Int(n int) int {
+ return rand.Intn(n)
+}
+
+func SliceRand(ids []int64) (result []int64) {
+ result = make([]int64, 0)
+ if len(ids) <= 0 {
+ return
+ }
+ copy := make([]int64, 0)
+ for _, id := range ids {
+ copy = append(copy, id)
+ }
+ num := Int(len(copy)) + 1
+ for i := 0; i < num; i++ {
+ index := Int(len(copy))
+ result = append(result, copy[index])
+ copy = append(copy[:index], copy[index+1:]...)
+ }
+ return
+}
+
+func RandDigits(n uint) string {
+ s := ""
+ for n != 0 {
+ s += fmt.Sprintf("%d", rand.Intn(10))
+ n--
+ }
+ return s
+}
+
+// RandStrRandrAll 生成指定位数的随机数
+func RandStrRandrAll(n int) string {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytesAll[rand.Int63()%int64(len(letterBytesAll))]
+ }
+ return string(b)
+}
diff --git a/pub/common/recomapi/define.go b/pub/common/recomapi/define.go
new file mode 100644
index 0000000..11969a4
--- /dev/null
+++ b/pub/common/recomapi/define.go
@@ -0,0 +1,42 @@
+package recomapi
+
+import "time"
+
+const (
+ FBT_Star string = "star"
+ FBT_Like string = "like"
+ FBT_Read string = "read"
+)
+
+type RSItemInfo struct {
+ Comment string `json:"Comment"`
+ ItemId string `json:"ItemId"`
+ Labels []string `json:"Labels"`
+ Timestamp time.Time `json:"Timestamp"`
+}
+
+type RSItemInfoList []*RSItemInfo
+
+type RSUserInfo struct {
+ Comment string `json:"Comment"`
+ UserId string `json:"UserId"`
+ Labels []string `json:"Labels"`
+ Subscribe []string `json:"Subscribe"`
+ Timestamp time.Time `json:"Timestamp"`
+}
+
+type RSUserInfoList []*RSUserInfo
+
+type APIResult struct {
+ RowAffected int64 `json:"RowAffected"`
+}
+
+type FeedbackInfo struct {
+ Comment string `json:"Comment"`
+ FeedbackType string `json:"FeedbackType"`
+ ItemId string `json:"ItemId"`
+ Timestamp time.Time `json:"Timestamp"`
+ UserId string `json:"UserId"`
+}
+
+type FeedbackInfoList []*FeedbackInfo
diff --git a/pub/common/recomapi/feedback.go b/pub/common/recomapi/feedback.go
new file mode 100644
index 0000000..c933a08
--- /dev/null
+++ b/pub/common/recomapi/feedback.go
@@ -0,0 +1,45 @@
+package recomapi
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+)
+
+// AddOneFeedback 添加一个推荐反馈
+func AddOneFeedback(url string, key string,
+ userId int64, mediaId int64, fbType string, comment string) (
+ int64, *srvdef.SrvErr) {
+ feedBack := &FeedbackInfo{
+ UserId: strconv.FormatInt(userId, 10),
+ ItemId: strconv.FormatInt(mediaId, 10),
+ FeedbackType: fbType,
+ Comment: comment,
+ Timestamp: time.Now(),
+ }
+ return AddManyFeedbacks(url, key, FeedbackInfoList{feedBack})
+}
+
+func AddManyFeedbacks(url string, key string, feedbacks FeedbackInfoList) (
+ int64, *srvdef.SrvErr) {
+ reqUrl := url + "/api/feedback"
+ resp := &APIResult{}
+ reqBody, _ := json.Marshal(feedbacks)
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.POSTJsonWithJResp(resp, reqUrl, header, reqBody)
+ if err != nil {
+ log.Error("recomapi:AddManyFeedbacks POSTJsonWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:AddManyFeedbacks POSTJsonWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ return resp.RowAffected, nil
+}
diff --git a/pub/common/recomapi/itemapi.go b/pub/common/recomapi/itemapi.go
new file mode 100644
index 0000000..51578a7
--- /dev/null
+++ b/pub/common/recomapi/itemapi.go
@@ -0,0 +1,106 @@
+package recomapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+)
+
+func InsertOneItem(url string, key string, itemInfo *RSItemInfo) *srvdef.SrvErr {
+ reqUrl := url + "/api/item"
+ resp := &APIResult{}
+ reqBody, _ := json.Marshal(itemInfo)
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.POSTJsonWithJResp(resp, reqUrl, header, reqBody)
+ if err != nil {
+ log.Error("recomapi:InsertOneItem POSTJsonWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:InsertOneItem POSTJsonWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ return nil
+}
+
+func InsertManyItems(url string, key string, itemInfos RSItemInfoList) *srvdef.SrvErr {
+ reqUrl := url + "/api/items"
+ reqBody, _ := json.Marshal(itemInfos)
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.POSTJsonWithJResp(nil, reqUrl, header, reqBody)
+ if err != nil {
+ log.Error("recomapi:InsertManyItems POSTJsonWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:InsertManyItems POSTJsonWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ return nil
+}
+
+func GetOneItem(url string, key string, itemId string) (*RSItemInfo, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/item/%s", url, itemId)
+ resp := &RSItemInfo{}
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.GetWithJResp(resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:GetOneItem GetWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:GetOneItem GetWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ log.Info("Test", log.Any("resp", resp))
+ return resp, nil
+}
+
+func GetManyItems(url string, key string, cursor string, size int64) (RSItemInfoList, string, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/items?n=%d&cursor=%s", url, size, cursor)
+ type Resp struct {
+ Cursor string `json:"Cursor"`
+ Items RSItemInfoList `json:"Items"`
+ }
+ var resp Resp
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.GetWithJResp(&resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:GetOneItem GetWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return nil, "", srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:GetOneItem GetWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return nil, "", srvdef.New(srvdef.Err, "", "")
+ }
+ return resp.Items, resp.Cursor, nil
+}
+
+func DeleteOneItem(url string, key string, itemId string) (int64, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/item/%s", url, itemId)
+ resp := &APIResult{}
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.DeleteWithJResp(resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:DeleteOneItem DeleteWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:DeleteOneItem DeleteWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ return resp.RowAffected, nil
+}
diff --git a/pub/common/recomapi/recomapi.go b/pub/common/recomapi/recomapi.go
new file mode 100644
index 0000000..b4356a6
--- /dev/null
+++ b/pub/common/recomapi/recomapi.go
@@ -0,0 +1,36 @@
+package recomapi
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+)
+
+func GetRecomItems(url string, key string, userId int64, writeBack string, size int64) (
+ []int64, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/recommend/%d?n=%d&write-back=%s", url, userId, size, writeBack)
+ var resp []string
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.GetWithJResp(&resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:GetRecomItems GetWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:GetRecomItems GetWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ itemIds := make([]int64, 0)
+ for _, v := range resp {
+ id, err := strconv.ParseInt(v, 10, 64)
+ if err == nil {
+ itemIds = append(itemIds, id)
+ }
+ }
+ return itemIds, nil
+}
diff --git a/pub/common/recomapi/recomapi_test.go b/pub/common/recomapi/recomapi_test.go
new file mode 100644
index 0000000..7bdccd1
--- /dev/null
+++ b/pub/common/recomapi/recomapi_test.go
@@ -0,0 +1,77 @@
+package recomapi
+
+import (
+ "fmt"
+ "testing"
+ "time"
+ "videoweb/pub/common/log"
+)
+
+var (
+ testUrl = "http://139.177.187.219:8087"
+ testKey = ""
+)
+
+func Test_InsertOneItem(t *testing.T) {
+ itemInfo := &RSItemInfo{
+ Comment: "Comment1",
+ ItemId: "10004",
+ Labels: []string{"lable1", "lable2", "lable3"},
+ Timestamp: time.Now(),
+ }
+ serr := InsertOneItem(testUrl, testKey, itemInfo)
+ fmt.Printf("\nTest_pingReq %s %v\n", itemInfo, serr)
+}
+
+func Test_InsertManyItems(t *testing.T) {
+ list := make(RSItemInfoList, 0)
+ itemInfo1 := &RSItemInfo{
+ Comment: "Comment1",
+ ItemId: "10005",
+ Labels: []string{"lable1", "lable2", "lable3"},
+ Timestamp: time.Now(),
+ }
+ list = append(list, itemInfo1)
+ itemInfo2 := &RSItemInfo{
+ Comment: "Comment1",
+ ItemId: "10006",
+ Labels: []string{"lable1", "lable2", "lable3"},
+ Timestamp: time.Now(),
+ }
+ list = append(list, itemInfo2)
+ serr := InsertManyItems(testUrl, testKey, list)
+ fmt.Printf("\nTest_InsertManyItems %v\n", serr)
+}
+
+func Test_GetOneItem(t *testing.T) {
+ itemInfo, serr := GetOneItem(testUrl, testKey, "10004")
+ fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfo, serr)
+}
+
+func Test_GetManyItem(t *testing.T) {
+ itemInfos, cursor, serr := GetManyItems(testUrl, testKey, "10003", 2)
+ log.Info("Test_GetManyItem", log.Any("itemInfos", itemInfos),
+ log.Any("cursor", cursor), log.E(serr))
+ //fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
+}
+
+func Test_DeleteOneItem(t *testing.T) {
+ count, serr := DeleteOneItem(testUrl, testKey, "10004")
+ log.Info("Test_DeleteOneItem", log.Any("count", count), log.E(serr))
+ //fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
+}
+
+//////////////////////////////////////////////////////////////
+//feedback
+
+func Test_AddOneFeedback(t *testing.T) {
+ count, serr := AddOneFeedback(testUrl, testKey, 18531, 16346, FBT_Star, "feedback1")
+ fmt.Printf("\nTest_pingReq %d %v\n", count, serr)
+}
+
+// /reccommend
+func Test_GetManyRecomItems(t *testing.T) {
+ itemIds, serr := GetRecomItems(testUrl, testKey, 18531, "test", 10)
+ log.Info("Test_GetManyRecomItems", log.Any("itemIds", itemIds), log.E(serr))
+ //fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
+}
diff --git a/pub/common/recomapi/usersapi.go b/pub/common/recomapi/usersapi.go
new file mode 100644
index 0000000..e8699bd
--- /dev/null
+++ b/pub/common/recomapi/usersapi.go
@@ -0,0 +1 @@
+package recomapi
diff --git a/pub/common/regapi.go b/pub/common/regapi.go
new file mode 100644
index 0000000..aba3d64
--- /dev/null
+++ b/pub/common/regapi.go
@@ -0,0 +1,26 @@
+package common
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// type FunHandle func(*gin.Context) (proto.Message, string, string, pkt_pub.EType)
+
+type ApiOpType int
+
+const (
+ ApiGet ApiOpType = 1
+ ApiPost ApiOpType = 2
+ ApiPut ApiOpType = 3
+)
+
+// API接口设置
+type ApiSet struct {
+ Path string // 路径
+ OpType ApiOpType // 操作类型
+ Fun gin.HandlerFunc // 函数句柄
+ IsWhite bool // 是否白名单
+ Desc string // 描述
+}
+
+var ApiSetMaps = map[string]ApiSet{}
diff --git a/pub/common/response/base_page.go b/pub/common/response/base_page.go
new file mode 100644
index 0000000..a237fab
--- /dev/null
+++ b/pub/common/response/base_page.go
@@ -0,0 +1,9 @@
+package response
+
+// BasePageResp 分页数据返回结构体
+type BasePageResp struct {
+ TotalCount int64 `json:"totalCount"` // 总数量
+ PageSize int `json:"pageSize"` // 每页数量
+ PageNum int `json:"pageNum"` // 当前页码
+ List interface{} `json:"list"` // 当前页数据
+}
diff --git a/pub/common/slugx/slugx.go b/pub/common/slugx/slugx.go
new file mode 100644
index 0000000..53c1dcf
--- /dev/null
+++ b/pub/common/slugx/slugx.go
@@ -0,0 +1,70 @@
+package slugx
+
+import (
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// Exists 判断 slug 是否已被占用
+type Exists func(slug string) bool
+
+// Ensure 新增时保证 slug:有值则校验唯一且不能全数字,无值则按 source 自动生成
+func Ensure(slug *string, source string, exists Exists) *srvdef.SrvErr {
+ if slug == nil {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能为空", "Slug不能为空")
+ }
+ *slug = utils.SanitizeSlug(*slug)
+ if *slug != "" {
+ if utils.IsSlugAllDigits(*slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ if exists(*slug) {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ return nil
+ }
+ *slug = utils.MakeUniqueSlug(source, exists)
+ return nil
+}
+
+// PrepareOnUpdate 更新时处理 slug:手动传入则校验,历史为空则自动生成
+func PrepareOnUpdate(current string, reqSlug *string, source string, exists Exists) (string, bool, *srvdef.SrvErr) {
+ slug := current
+ if reqSlug != nil {
+ slug = utils.SanitizeSlug(*reqSlug)
+ }
+ if slug != "" {
+ if reqSlug != nil {
+ if utils.IsSlugAllDigits(slug) {
+ return "", false, srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ if exists(slug) {
+ return "", false, srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ return slug, true, nil
+ }
+ return "", false, nil
+ }
+ return utils.MakeUniqueSlug(source, exists), true, nil
+}
+
+// Filter 构造按 slug 查重条件,excludeId>0 时排除自身
+func Filter(slugField, idField, slug string, excludeId int64) bson.M {
+ filter := bson.M{slugField: slug}
+ if excludeId > 0 {
+ filter[idField] = bson.M{"$ne": excludeId}
+ }
+ return filter
+}
+
+// EmptyFilter 查询 slug 为空或不存在的历史数据
+func EmptyFilter() bson.M {
+ return bson.M{
+ "$or": []bson.M{
+ {"slug": ""},
+ {"slug": bson.M{"$exists": false}},
+ },
+ }
+}
diff --git a/pub/common/sms/fengniao/fengniao.go b/pub/common/sms/fengniao/fengniao.go
new file mode 100644
index 0000000..5d11c13
--- /dev/null
+++ b/pub/common/sms/fengniao/fengniao.go
@@ -0,0 +1,76 @@
+package fengniao
+
+import (
+ "crypto/sha1"
+ "errors"
+ "fmt"
+ "net/http"
+ "videoweb/pub/common/httputil"
+ "videoweb/pub/common/log"
+)
+
+type requestBody struct {
+ MercId string `json:"mercId"`
+ Secret string `json:"secret"`
+ TemplateId string `json:"templateId"`
+ Code string `json:"smsCode"`
+ Mobile string `json:"mobile"`
+ SignType string `json:"signType"` // 固定sha1
+ Sign string `json:"sign"` // 签名
+}
+
+type msg struct {
+ Status string `json:"status"`
+ TaskId string `json:"taskId"`
+}
+
+type responseBody struct {
+ Code int `json:"code"`
+ Msg msg `json:"msg"`
+ Err string `json:"err"`
+}
+
+type FengNiao struct {
+ Url string
+ MercId string
+ Secret string
+ Tmpl string
+}
+
+// Send 发送, 目前蜂鸟貌似只支持数字
+func (f *FengNiao) Send(mobile string, code string) error {
+ content := fmt.Sprintf(f.Tmpl, code)
+ data := fmt.Sprintf("mercId=%s&templateId=%s&smsCode=%s&mobile=%s&secret=%s", f.MercId, f.Tmpl, code, mobile, f.Secret)
+
+ sha := sha1.New()
+ _, err := sha.Write([]byte(data))
+ if err != nil {
+ log.Error("fengniao send error", log.Any("mobile", mobile), log.Any("content", content), log.E(err))
+ return err
+ }
+
+ body := &requestBody{
+ MercId: f.MercId,
+ Secret: f.Secret,
+ TemplateId: f.Tmpl,
+ Code: code,
+ Mobile: mobile,
+ SignType: "sha1",
+ Sign: fmt.Sprintf("%x", sha.Sum(nil)),
+ }
+ rp := responseBody{}
+ statusCode, err := httputil.Client().POSTJsonWithJResp(&rp, f.Url, nil, body)
+ if err != nil {
+ log.Error("fengniao POSTJsonWithResp err", log.E(err))
+ return err
+ }
+ if statusCode != http.StatusOK {
+ log.Error("fengniao status code not 200")
+ return errors.New("fengniao statusCode not 200")
+ }
+ if rp.Code != 200 {
+ log.Error("fengniao code not 200", log.Any("errdesc", rp.Err))
+ err = errors.New(rp.Err)
+ }
+ return err
+}
diff --git a/pub/common/sms/sms.go b/pub/common/sms/sms.go
new file mode 100644
index 0000000..e0c5514
--- /dev/null
+++ b/pub/common/sms/sms.go
@@ -0,0 +1,45 @@
+package sms
+
+import (
+ "strings"
+ "videoweb/pub/common/sms/fengniao"
+)
+
+type Provider = int
+
+const (
+ tmplID1 = "6029f19a99fd8b404062e4d2" // 大陆
+ tmplID2 = "5f8412c39396d16f787e561b" // 非大陆
+)
+
+const (
+ FengNiao Provider = 1 //蜂鸟
+ YunPian Provider = 2 //云片
+)
+
+func sendByFengNiao(mobile string, code string) error {
+ var tmplID string
+ if strings.HasPrefix(mobile, "+86") {
+ tmplID = tmplID1
+ return (&fengniao.FengNiao{
+ Url: "https://t9x6k.cc/api/sms/sendSms",
+ MercId: "10177",
+ Secret: "2dfb7ec44ed59ee3c1fe8dc533384e16",
+ Tmpl: tmplID,
+ }).Send(mobile, code)
+ } else {
+ tmplID = tmplID2
+ return (&fengniao.FengNiao{
+ Url: "https://t9x6k.cc/api/sms/sendSms",
+ MercId: "10228",
+ Secret: "3ea58a27a162dad4ed3e18c076648742",
+ Tmpl: tmplID,
+ }).Send(mobile, code)
+ }
+ // return
+}
+
+// Send 发送短信
+func Send(mobile string, code string) error {
+ return sendByFengNiao(mobile, code)
+}
diff --git a/pub/common/sms/sms_test.go b/pub/common/sms/sms_test.go
new file mode 100644
index 0000000..d0a80ba
--- /dev/null
+++ b/pub/common/sms/sms_test.go
@@ -0,0 +1,8 @@
+package sms
+
+import "testing"
+
+func TestSend(t *testing.T) {
+ err := Send("15008279990", "654321")
+ t.Log(err)
+}
diff --git a/pub/common/srvdef/codedef.go b/pub/common/srvdef/codedef.go
new file mode 100644
index 0000000..e60f643
--- /dev/null
+++ b/pub/common/srvdef/codedef.go
@@ -0,0 +1,331 @@
+package srvdef
+
+import "fmt"
+
+type CodeType int
+
+const NotFind = "mongo: no documents in result"
+
+// 错误级别
+const (
+ ParamLevel = 1000 // 参数级
+ StoreLevel = 3000 // 存储级
+ DBLevel = 5000 // 数据库
+ ServerLevel = 4000 // 服务级
+ BusinessLevel = 6000 // 业务逻辑级
+ NewErrorLevelV2 = 7000 // 新版错误
+ NewErrorLevelV3 = 8000 // 新版错误
+)
+
+// 通用code
+const (
+ Success CodeType = 200 //Success 通用成功
+ Failure CodeType = 400 //Failure 通用失败
+ NotFound CodeType = 404
+)
+
+func (c CodeType) Msg() string {
+ msg, ok := codeAndMsg[c]
+ if ok {
+ return msg
+ }
+ return ""
+}
+
+func (c CodeType) Tip() string {
+ tip, ok := codeAndTip[c]
+ if ok {
+ return tip
+ }
+ return ""
+}
+
+func (c CodeType) Error() string {
+ return fmt.Sprintf("code=%d,msg=%v,tip=%v", c, c.Msg(), c.Tip())
+}
+
+type ErrInfoer interface {
+ GetMsg() string
+ GetTip() string
+ GetCode() CodeType
+}
+
+// GetMsg alias of Msg()
+func (c CodeType) GetMsg() string { return c.Msg() }
+
+// GetTip alias of Tip()
+func (c CodeType) GetTip() string { return c.Tip() }
+func (c CodeType) GetCode() CodeType { return c }
+
+const (
+ InvalidToken CodeType = ParamLevel + iota // 框架错误
+ ErrParamError // 参数错误
+ NoToken // 无token
+ RoleErr // 角色错误
+ ReLogin // 重复登陆
+ UploadToFileSrvFailed // 文件上传失败
+ ErrCaptcha // 验证码为空
+ ErrCaptchaInvalid // 验证码非法
+ ErrCaptchaGet // 验证码获取失败
+ ErrCaptchaResend // 验证码重复发送
+ ErrCaptchaSendFail // 验证码发送失败
+ InvalidateToken // 非法token
+ NoUA // 没有UA信息
+ UploadFile // 文件上传失败
+ AlreadyRegisered // 已经组册
+ TokenRemoved // 你已经挤下线,请重新登陆
+ PostCommentNotUse //发送失败,服务器错误,请稍后再试
+ DBTransaction // 事务错误
+ ErrCrawlerUrl // 爬虫URL错误
+ ErrCrawler // 爬取数据错误
+)
+
+const (
+ DBQuery CodeType = StoreLevel + iota
+ DBInsert
+ DBUpdate
+ DBDelete
+ RedisSet // 设置redis错误
+ RedisGet // 获取redis值错误
+ RedisDel // 删除redis值错误
+ EtcdGet // 获取etcd错误
+ MongoDBQueryErr // 数据库查询失败
+)
+
+const (
+ ServerError CodeType = ServerLevel + iota // 用户相关错误
+)
+
+const (
+ _ CodeType = DBLevel + iota
+ ErrDbQueryError //ErrDbQueryError 数据库查询错误 5001
+ ErrDbInsertError //ErrDbInsertError 数据库插入错误 5002
+ ErrDbUpdateError //ErrDbUpdateError 数据库修改错误 5003
+ ErrDbDeleteError //ErrDbDeleteError 数据库删除错误 5004
+ ErrDbInputExist //ErrDbInputExist 数据库输入值已经存在 5005
+ ErrRdsOperError //ErrRdsOperError redis操作错误 5006
+ ErrElsOperError //ErrElsOperError redis操作错误 5007
+ ErrTransactionsError //ErrTransactionsError 数据库事务操作错误
+ ErrNoData //无数据
+)
+const (
+ ErrUserPermission CodeType = BusinessLevel + iota // 用户权限
+ ErrUserNotExsit // 用户不存在
+ ErrUserTypeNotUP // 用户不是UP主
+ AccountIsNotExists // 账户不存在
+ AccountErrAccessForbid // 账户禁止登陆
+ AccountErrPasswordError // 账户密码错误
+ GenerateTokenFailed // 生成二维码错误
+ ErrCardNotExists // 没有会员卡信息
+ ErrMadouCoinNotExists // 没有钻石信息
+ ErrMediaNotExsit // 视频不存在
+ ErrTopicInfoNotExsit // 视频专题不存在
+ ErrData // 数据错误
+ Err // 一般错误
+ ErrRchgOrderNotExist // 充值订单不存在
+ ErrSubmitRchgOrder // 发起充值订单错误
+ InvalidRequestReplayAttack // 非法请求
+ ErrBuyVIPCardFail // 购买会员卡失败
+ ErrOperationTooFrequently // 用户操作太频繁
+ ErrMediaSyncServer // 视频同步服务器错误
+ ErrUserType // 用户无改权限
+ DownloadM3u8File // 文件服务器访问失败
+ MediaSyncing // 视频同步中
+ MediaConnectServerFaild // 媒资库链接失败
+ AtressNotExist // 演员不存在
+ ErrAnnouncementNotExsit // 公告不存在
+ ErrContactDetailsNotExsit // 联系方式不存在
+ ErrPostCommentNotExsit // 评论不存在
+ HistoryNotExist // 记录不存在
+ ErrHotsearchNotExsit // 热搜不存在
+ ActivityDoesNotExist // 活动不存在
+ UserWatchNumNotEnough // 免费观看次数不足
+ MediaIsVip // 用户不是会员
+ MediaIsNeedPay // 视频需要付费
+ RepeatBuy // 重复购买
+ MovieTicketsNotExits // 观影券不足
+ MediaIsNotNeedPay // 视频不需要支付
+ _ // 已移除帖子不需要支付错误码,保留原数值
+ SystemMsgNotExist // 消息不存在
+ ForbidlogNotExist // 封禁消息不存在
+ AttachmentDoesNotExist // 导出记录
+ UserDownloadNotEnough // 下载次数不足
+ UserDownloadNeedPay // 付费视频需要先购买
+ RedeemCodeNotExists // 兑换码不存在
+ RedeemCodeTimesUsedUp // 兑换码次数使用完了
+ RedeemCodeAlreadyUsed // 兑换码你已经使用过兑换码了
+ RedeemCodeRecordNotExists // 兑换记录不存在
+ MediaIsNotNeedPreSale // 视频不支持预售
+ MediaIsPreSale // 预售视频
+ QRcodeError // 二维码错误
+ ModifyUserNameError // 修改
+ ErrSkdRecordNotExsit // 任务不存在
+ ErrContainIllegalWord // 包含非法字符
+ DatingOrderNotExist // 约会购买记录不存在
+ DatingHasBuy // 重复购买
+ DatingCollectNotExist // 收藏记录不存在
+ DatingBossNotExist // 经纪人不存在
+ DatingReportNotExist // 验证报告不存在
+ DatingReportDouble // 重复验证
+ DatingAlreadyComplaint // 已经提交举报
+ SortNotExist // 视频排序不存在
+ MessageDialogNotExist // 私信对话框不存在
+ VideoUploadFaild // 文件上传失败
+ _ // 已移除帖子状态错误码,保留原数值
+ WtdrFrequencyLimits // 提现频率限制
+ WtdrErroTip1 // 提现不是100整数倍
+ WtdrErroTip2 // 赠送3元金额要满500才能提现
+ WtdrErroTip3 // 请填写银行名字或升级App版本
+ GameWtdrErroTip7 // 银行卡必须与您以前的提现卡一致,如需更改请联系客服
+ ProxyUserNotExist // 代理不存在
+ ErrPostReportNotExsit // 举报信息不存在
+ FileUploadDouble // 文件重复上传
+ UserRepeatSubmitRchgOrder // 未支付订单太多,有问题请联系客服
+ Limiting // 限流
+ ErrIpForbid // Ip禁止
+ DeviceIdInvalid // 设备id无效
+ UserScoreNotEnough // 用户积分不足
+ UserSocreLVLimit // 用户积分等级限制
+ UploaderNeedActive // Up主没有激活
+ UploaderNeedBind // 后台用户没有up主绑定
+ PostCollectionNotExists // 图文专辑不存在
+ MediaCollectionNotExist // 该合集或选集不存在
+ ContestNotExists // 该比赛不存在
+ CannotJoinContest // 该比赛无法参加
+ ContestIsVip // 该比赛只限VIP参加
+ AppInfoNotExist // 该App不存在
+ MediaNotVerified // 视频未上架
+ PCCategoryUnmatch // 专辑的分类和作品的分类不一致
+ ErrMediaRecommendNotExsit // 推荐不存在
+ DatingBuyHistoryNotExist // 购买记录不存在
+ DatingWatchHistoryNotExist // 观看记录不存在
+ CollectIsExist // 收藏已经存在
+ CollectNotExist // 收藏不存在
+ DiscussRankDoesNotExist // 话题视频排名
+ DiscussActivityDoesNotExist // 话题活动不存在
+ UpDayStatDoesNotExist // 创作者收益
+ DiscussObjectRankDoesNotExist // 话题视频
+ DiscussCategoryDoesNotExist // 话题分类不存在
+ ErrCareNotExist // 取消关注对象不存在
+ ErrUpMasterTaskNotCompleted // up主申请任务未完成
+ HGameNotExist // 游戏不存在
+ HGameIsVip // 游戏需要会员
+ HGameIsNeedPay // 游戏需要付费
+ ErrApplyUpIsVip // UP主申请需要vip
+ UserLevelCardNotExist // 用户等级卡不存在
+ UserCheckinSetupNotExist // 签到配置不存在
+ PriceMediaNotCancel // 钻石视频不能下架
+ PriceMediaHasBuy // 钻石视频已经被购买
+ GiftWeekIsExist // 礼包已领取
+ UserLevelPointTooLow // 用户等级积分太低
+ UserGiftHaveReceive // 任务奖励已经领取
+ UserGiftExistNoReceive // 没有领取资格
+ DatingUpMasterNotExist // up主不存在
+ UserIsUpMaster // 用户已经是up主
+ UserUpCerting // up主认证中
+ MissionSetupNotExist // 任务配置不存在
+ UserIsUpMasterBind // 用户已被up主绑定
+ ErrCareIsExist // 不可重复关注
+ ObjectDayStatDoesNotExist // 作品统计记录不存在
+ ObjectDayViewIncStatDoesNotExist // 作品增量记录不存在
+ UserNotEnoughFreeLotteryCount // 免费抽奖次数不足
+ PrizeOutOfStore // 奖品库存不足
+ MediaSubscribedNotNeedPay // 订阅的视频不需要支付
+ DatingComplaintNotExist // 举报帖子不存在
+ DatingUserNotExist // 楼凤不存在
+ UserDauInfoNotExist // 搜寻日期没有资料
+ ParseJsonError // 数据解析错误
+ AlreadyRefund // 已退款
+ AiCountTypeNotSupport // ai内容不支持
+ TodayPontNotExist // 积分记录不存在
+ RedeemPrizeHistoryNotExist // 奖品兑换记录不存在
+ PrizeNotExist // 奖品不存在
+ ErrUserPointsNotExist // 积分不足
+ AIFreeCountNotEnough // ai免费次使用数不足
+ AIFreeCountNOtSupportRefund // AI免费使用次数不支持退款
+ CrowdfundStatusFail // 众筹女神的状态不在可修改的范围内
+ CrowdfundNotOpenPrizeFail // 众筹女神的订单开奖失败
+ CrowdfundPrizeNumberError // 众筹女神的开奖号码不对
+ RedeemFail // 积分兑换失败
+ CrowdfundNotBuy // 当前女神状态不能购买
+ CrowdfundNumberHaveBuy // 这个号码已经被其他人购买了
+ UserCheckinDisabled // 用户签到功能已关闭
+ PrizeTypeUnsupported // 奖品类型不支持自动发放
+ CouponExpired // 卡券已过期
+ CouponUsed // 卡券已被使用
+ CouponNotFound // 没有卡券信息
+ JumpTabNotExist // 首页跳转tab不存在
+ CouponCanNotUse // 卡券无法使用
+ SysConfigError //系统配置错误
+ _ // 已移除漫画分类错误码,保留原数值
+ _ // 已移除漫画标签错误码,保留原数值
+ _ // 已移除漫画主题错误码,保留原数值
+ _ // 已移除漫画章节不存在错误码,保留原数值
+ _ // 已移除漫画不存在错误码,保留原数值
+ _ // 已移除漫画CV错误码,保留原数值
+ _ // 已移除漫画视频错误码,保留原数值
+ _ // 已移除漫画排行榜错误码,保留原数值
+ VomicsTopicNotExist // 有声漫不存在
+ _ // 已移除漫画黑名单错误码,保留原数值
+ CommentTextIsEmpty // 评论内容为空
+ _ // 已移除漫画会员错误码,保留原数值
+ _ // 已移除漫画付费错误码,保留原数值
+ _ // 已移除漫画章节已存在错误码,保留原数值
+ ObjectNotExist // 数据不存在
+ GameWtdrErroTip3 // 请填写银行名字或升级App版本
+ UserDoTaskErr // 用户任务出错
+ GetPrizeFail // 获取奖品失败
+ _ // 已移除小说付费错误码,保留原数值
+ _ // 已移除小说会员错误码,保留原数值
+ LotteryActivityNotEnable // 抽奖活动没有启用
+ CurrTimeNotActivity // 当前时间没有活动
+ AIOrderStripsCountLimit // 今天AI脱衣提交次数达到上限
+ NotWtdrChannel // 没有匹配到提现通过
+ ExcelExportErr // 文件导出错误
+ _ // 已移除漫画活动错误码,保留原数值
+ NoGiftToReceive // 没有奖品可领取
+ UserRepeatDrawDiscountCard // 用户重复翻牌
+ NoUseDiscountCard // 不可使用折扣卡
+ AIOrderNotDel // 未完成的订单不能删除
+ AIWaitOrderTooMuch // 排队中的AI订单太多,请稍后再试
+ ActorNotExist // 演员不存在
+ MediaFSServerHandleIng // 文件服务器处理中
+ VideoRepeat // 视频重复
+ ErrReqForbidden // 请求被拒
+ APPIDError // APPID异常
+ RepeatActress // 重复的女优
+ CreateNotExist // 作者不存在
+)
+const (
+ ErrRecharge CodeType = iota + 18000 // 充值相关错误
+ ErrGetRechargeType // 充值方式获取错误
+ UserPhoneNotBind // 手机号码未注册
+ PostCommentPermissionDenied // 禁止评论
+ UserLoginPasswordErr // 密码错误
+ ErrMediaRecordNotExsit // 视频记录不存在
+ ErrThressServerHttpErr // 第三方服务错误
+ UserPhoneBeenBound // 手机号码已被绑定
+ WeekCommentNumNotEnough // 评论条数不足
+ ErrAccessForbid //ErrAccessForbid 没有访问权限 1000
+ MediaAlreadyInEpisode // 该视频已经有选集
+ MediaNotInEpisode // 选集中不包含该视频
+ MediaCollectNotNull // 合集视频不为空
+)
+
+const (
+ //
+ CircleErrCode CodeType = iota + 20000 // 圈子错误码 20000
+ PublistNotVIPPermissionDenied // 不是会员禁止发布
+ PublistContentErrDenied // 包含敏感词汇
+ _ // 已移除帖子重复购买错误码,保留原数值
+ UserNotEnoughMoney // 用户的钻石数量不足
+ PostStatusError // 订单状态错误
+ PostNotAllowComplaint // 不允许举报
+ UserNotEnoughScore // 用户的钻石数量不足
+ ErrUserAccExist // 账号已存在
+ UserAccountNotBind // 账号未注册
+ _ // 已移除帖子上架数量限制错误码,保留原数值
+ ErrUserBeBanned //ErrUserBeBanned 用户已经禁言 1004
+ ErrCustomerBanned //ErrCustomerBanned 客服已休息 1005
+ ErrVersionUpdate //ErrVersionUpdate 版本强制更新 1006
+)
diff --git a/pub/common/srvdef/msgdef.go b/pub/common/srvdef/msgdef.go
new file mode 100644
index 0000000..98bcf53
--- /dev/null
+++ b/pub/common/srvdef/msgdef.go
@@ -0,0 +1,58 @@
+package srvdef
+
+// error tips
+const (
+ Ok = "成功"
+ //基础错误提示
+ ET_Store = "存储错误"
+ ET_DBOp = "DB操作错误"
+ ET_DBUpdate = "DB更新失败"
+ ET_RdsOp = "Rds操作错误"
+ ET_ESOp = "ES操作错误"
+ ET_Param = "参数错误"
+ ET_NoDocment = "无数据不存在"
+ ET_NoCache = "缓存数据不存在"
+ ET_MoneyNotEnough = "余额不足"
+ //----------------------------------------------
+ ET_UserNoExist = "用户不存在"
+ ET_IsntBlogger = "用户不是博主"
+ ET_BloggerApply = "博主申请条件不满足"
+ ET_BloggerAlreadyIs = "用户已经是博主"
+ ET_VipLevel2Low = "用户的VIP等级不够"
+ //----------------------care------------------------
+ ET_StatusEqual = "已经是请求状态"
+ ET_AlreadyCare = "已关注"
+ ET_AlreadyJoinFG = "您已加入了该粉丝团"
+)
+
+var codeAndMsg = map[CodeType]string{
+ Success: "success",
+ Failure: "Failed",
+ ErrDbQueryError: "database query error",
+ ErrDbInsertError: "database insert error",
+ ErrDbUpdateError: "database update error",
+ ErrDbDeleteError: "database delete error",
+ ErrDbInputExist: "current input has already exists",
+ ErrRdsOperError: "redis operate error",
+ ErrElsOperError: "elastic operate error",
+ BusinessLevel: "business error",
+ ErrCardNotExists: "card does not exist",
+ ErrMadouCoinNotExists: "madou coin does not exist",
+ ErrGetRechargeType: "get recharge type error",
+ ErrRchgOrderNotExist: "recharge order does not exist",
+ ErrBuyVIPCardFail: "buy vip card failed",
+ ErrOperationTooFrequently: "user operation is too frequent",
+ UserNotEnoughMoney: "user's coin is not enough",
+ RedeemCodeNotExists: "code does not exist",
+ RedeemCodeTimesUsedUp: "code times used up",
+ RedeemCodeAlreadyUsed: "you have used this code",
+ RedeemCodeRecordNotExists: "code use record does not exist",
+ ErrNoData: "no data",
+ UserPhoneBeenBound: "phone number has been bound",
+ WtdrFrequencyLimits: "wtdr frequency limits,please try again later",
+ MediaIsNeedPay: "You have to buy the video.",
+ UserWatchNumNotEnough: "More than three times",
+ UserScoreNotEnough: "user score is not enough",
+ UserSocreLVLimit: "user score level limit",
+ AlreadyRefund: "already refund",
+}
diff --git a/pub/common/srvdef/srvdef.go b/pub/common/srvdef/srvdef.go
new file mode 100644
index 0000000..70382d4
--- /dev/null
+++ b/pub/common/srvdef/srvdef.go
@@ -0,0 +1,67 @@
+package srvdef
+
+import (
+ "fmt"
+)
+
+// SrvErr 代替/common/error中的定义,逐渐在代码中弃掉(common/error模块)
+type SrvErr struct {
+ Code CodeType `json:"code"`
+ Msg string `json:"msg"`
+ Tips string `json:"tips"`
+}
+
+// New create an error
+func New(code CodeType, msgAndTips ...string) *SrvErr {
+ msg := code.Msg()
+ tips := code.Tip()
+ if len(msgAndTips) == 1 {
+ msg = msgAndTips[0]
+ } else if len(msgAndTips) == 2 {
+ msg, tips = msgAndTips[0], msgAndTips[1]
+ }
+ return &SrvErr{
+ Code: code,
+ Msg: msg,
+ Tips: tips,
+ }
+}
+
+// SrvErr 返回字符串
+func (s *SrvErr) Error() string {
+ if s == nil {
+ return ""
+ }
+ return fmt.Sprintf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips)
+}
+
+// SrvErr 返回error
+func (s *SrvErr) ToError() error {
+ if s == nil {
+ return nil
+ }
+ return fmt.Errorf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips)
+}
+
+func (c *SrvErr) GetMsg() string {
+ // 有自定义的msg就用自定义的msg
+ if c.Msg != "" {
+ return c.Msg
+ }
+ return c.Code.Msg()
+}
+func (c *SrvErr) GetTip() string {
+ // 有自定义的tip就用自定义的tip
+ if c.Tips != "" {
+ return c.Tips
+ }
+ return c.Code.Tip()
+}
+func (c *SrvErr) GetCode() CodeType { return c.Code }
+
+var (
+ ErrDbInsert = New(ErrDbInsertError, "", "")
+ ErrDbQuery = New(ErrDbQueryError, "", "")
+ ErrDbUpdate = New(ErrDbUpdateError, "", "")
+ ErrDbDelete = New(ErrDbDeleteError, "", "")
+)
diff --git a/pub/common/srvdef/tipdef.go b/pub/common/srvdef/tipdef.go
new file mode 100644
index 0000000..49daeee
--- /dev/null
+++ b/pub/common/srvdef/tipdef.go
@@ -0,0 +1,160 @@
+package srvdef
+
+var codeAndTip = map[CodeType]string{
+ Success: "成功",
+ Failure: "失敗",
+ ErrCardNotExists: "沒有會員卡信息",
+ ErrMadouCoinNotExists: "沒有金幣信息",
+ ErrGetRechargeType: "充值方式獲取錯誤",
+ ErrRchgOrderNotExist: "充值訂單不存在",
+ ErrBuyVIPCardFail: "購買會員卡失敗",
+ ErrOperationTooFrequently: "用戶操作太頻繁",
+ UserNotEnoughMoney: "用户余额不足",
+ UserPhoneNotBind: "手机号码未注册",
+ PostCommentPermissionDenied: "非会员禁止发贴和评论",
+ UserLoginPasswordErr: "密码错误",
+ ErrUserTypeNotUP: "用户不是UP主",
+ AccountIsNotExists: "账户不存在",
+ AccountErrAccessForbid: "账户禁止登陆",
+ AccountErrPasswordError: "账户密码错误",
+ GenerateTokenFailed: "生成二维码错误",
+ ErrMediaNotExsit: "视频不存在",
+ ErrTopicInfoNotExsit: "视频专题不存在",
+ ErrData: "数据错误",
+ Err: "一般错误",
+ ErrSubmitRchgOrder: "发起充值订单错误",
+ InvalidRequestReplayAttack: "非法请求",
+ ErrMediaSyncServer: "视频同步服务器错误",
+ ErrUserType: "用户无改权限",
+ DownloadM3u8File: "文件服务器访问失败",
+ MediaSyncing: "视频同步中",
+ MediaConnectServerFaild: "媒资库链接失败",
+ AtressNotExist: "演员不存在",
+ ErrAnnouncementNotExsit: "公告不存在",
+ ErrContactDetailsNotExsit: "联系方式不存在",
+ ErrPostCommentNotExsit: "评论不存在",
+ HistoryNotExist: "记录不存在",
+ ErrHotsearchNotExsit: "热搜不存在",
+ ActivityDoesNotExist: "活动不存在",
+ UserWatchNumNotEnough: "免费观看次数不足",
+ MediaIsVip: "用户不是会员",
+ MediaIsNeedPay: "视频需要付费",
+ RepeatBuy: "重复购买",
+ MovieTicketsNotExits: "观影券不足",
+ MediaIsNotNeedPay: "视频不需要支付",
+ SystemMsgNotExist: "消息不存在",
+ ForbidlogNotExist: "封禁消息不存在",
+ AttachmentDoesNotExist: "导出记录",
+ UserDownloadNotEnough: "下载次数不足",
+ UserDownloadNeedPay: "付费视频需要先购买",
+ ErrNoData: "无数据",
+ ModifyUserNameError: "昵称!每月只能修改一次",
+ RedeemCodeTimesUsedUp: "該兌換碼已使用",
+ RedeemCodeNotExists: "兌換碼已使用不存在",
+ RedeemCodeAlreadyUsed: "該兌換碼已使用",
+ InvalidToken: "框架错误",
+ ErrParamError: "参数错误",
+ NoToken: "无token",
+ RoleErr: "角色错误",
+ ReLogin: "重复登陆",
+ UploadToFileSrvFailed: "文件上传失败",
+ ErrCaptcha: "验证码为空",
+ ErrCaptchaInvalid: "验证码非法",
+ ErrCaptchaGet: "验证码获取失败",
+ ErrCaptchaResend: "验证码重复发送",
+ ErrCaptchaSendFail: "验证码发送失败",
+ InvalidateToken: "非法token",
+ NoUA: "没有UA信息",
+ UploadFile: "文件上传失败",
+ RedisSet: "设置redis错误",
+ RedisGet: "获取redis值错误",
+ RedisDel: "删除redis值错误",
+ EtcdGet: "获取etcd错误",
+ MongoDBQueryErr: "数据库查询失败",
+ ErrDbQueryError: "数据库查询错误",
+ ErrDbInsertError: "数据库插入错误",
+ ErrDbUpdateError: "数据库修改错误",
+ ErrDbDeleteError: "数据库删除错误",
+ ErrDbInputExist: "数据库输入值已经存在",
+ ErrRdsOperError: "redis操作错误",
+ ErrElsOperError: "redis操作错误",
+ ErrTransactionsError: "数据库事务操作错误",
+ ErrUserPermission: "用户权限",
+ ErrUserNotExsit: "用户不存在",
+ RedeemCodeRecordNotExists: "兑换记录不存在",
+ MediaIsNotNeedPreSale: "视频不支持预售",
+ MediaIsPreSale: "预售视频",
+ QRcodeError: "二维码错误",
+ ErrRecharge: "充值相关错误",
+ ErrMediaRecordNotExsit: "视频记录不存在",
+ UserPhoneBeenBound: "手机号码已被绑定",
+ DatingAlreadyComplaint: "已经举报过",
+ WtdrFrequencyLimits: "提现操作频率限制,请稍后再试",
+ WtdrErroTip1: "提现数额必须是100的整数倍",
+ WtdrErroTip2: "赠送6元体验金,满500可兑换钻石",
+ WtdrErroTip3: "请填写银行名字或升级App版本",
+ GameWtdrErroTip7: "银行卡必须与您以前的提现卡一致,如需更改请联系客服",
+ FileUploadDouble: "文件重复上传",
+ UserRepeatSubmitRchgOrder: "未支付订单太多,有问题请联系客服",
+ ErrIpForbid: "IP被禁止",
+ UserScoreNotEnough: "用户积分不足",
+ UserSocreLVLimit: "用户积分等级限制",
+ UploaderNeedBind: "后台用户没有up主绑定",
+ UploaderNeedActive: "Up主没有激活",
+ PostCollectionNotExists: "图文专辑不存在",
+ MediaCollectionNotExist: "该合集或选集不存在",
+ ContestNotExists: "该比赛不存在",
+ CannotJoinContest: "该比赛无法参加",
+ ContestIsVip: "该比赛只限VIP参加",
+ MediaNotVerified: "视频未上架",
+ PCCategoryUnmatch: "专辑的分类和作品的分类不一致",
+ ErrCareNotExist: "取消关注对象不存在",
+ ErrUpMasterTaskNotCompleted: "up主申请任务未完成",
+ HGameIsVip: "游戏需要会员",
+ HGameIsNeedPay: "游戏需要付费",
+ HGameNotExist: "游戏不存在",
+ ErrApplyUpIsVip: "UP主申请需要vip",
+ UserLevelCardNotExist: "用户等级卡不存在",
+ UserCheckinSetupNotExist: "签到配置不存在",
+ GiftWeekIsExist: "礼包已领取",
+ UserLevelPointTooLow: "用户等级积分太低",
+ UserGiftHaveReceive: "任务奖励已领取",
+ UserGiftExistNoReceive: "没有领取资格",
+ DatingUpMasterNotExist: "up主不存在",
+ UserIsUpMaster: "用户已经是up主",
+ UserUpCerting: "up主认证中",
+ MissionSetupNotExist: "任务配置不存在",
+ UserIsUpMasterBind: "用户已被up主绑定",
+ ErrCareIsExist: "不可重复关注",
+ ObjectDayStatDoesNotExist: "作品统计记录不存在",
+ ObjectDayViewIncStatDoesNotExist: "作品增量记录不存在",
+ UserNotEnoughFreeLotteryCount: "免费抽奖次数不足",
+ PrizeOutOfStore: "奖品库存不足",
+ MediaSubscribedNotNeedPay: "订阅的视频无需支付",
+ DatingComplaintNotExist: "举报帖子不存在",
+ DatingUserNotExist: "楼凤不存在",
+ WeekCommentNumNotEnough: "评论条数不足",
+ UserDauInfoNotExist: "搜寻日期没有资料",
+ PostCommentNotUse: "发送失败,服务器错误,请稍后再试",
+ AlreadyRefund: "已退款",
+ ErrUserPointsNotExist: "积分不足",
+ RedeemFail: "积分兑换失败",
+ CrowdfundNumberHaveBuy: "购买的号码已经出售",
+ CouponExpired: "卡券已过期",
+ CouponUsed: "卡券已使用",
+ CouponNotFound: "没有卡券信息",
+ CouponCanNotUse: "卡券无法使用",
+ LotteryActivityNotEnable: "抽奖活动没有启用",
+ CurrTimeNotActivity: "当前时间没有活动",
+ AIOrderStripsCountLimit: "今天AI脱衣提交次数达到上限",
+ NotWtdrChannel: "没有匹配到提现通过",
+ UserRepeatDrawDiscountCard: "重复翻牌",
+ NoUseDiscountCard: "不可使用折扣卡",
+ ErrUserAccExist: "账号已存在",
+ AIOrderNotDel: "未完成的订单不能删除",
+ AIWaitOrderTooMuch: "排队中的AI订单太多,请稍后再试",
+ ActorNotExist: "演员不存在",
+ VideoRepeat: "视频重复",
+ ErrReqForbidden: "请求被拒",
+ RepeatActress: "重复的女优",
+}
diff --git a/pub/common/srvversion/version.go b/pub/common/srvversion/version.go
new file mode 100644
index 0000000..3cafe3a
--- /dev/null
+++ b/pub/common/srvversion/version.go
@@ -0,0 +1,46 @@
+package srvversion
+
+import (
+ "fmt"
+ "os"
+ //"os"
+)
+
+var (
+ //每次编译时需要手动修改本字段,否则每次编译得到的服务版本号都会是默认值
+ SERVICE_VERSION = "V1.001.T"
+
+ //以下几个字段编译期间自动替换,无需手动操作
+ GIT_BRANCH = "GIT_BRANCH_FLAG"
+ GIT_VERSION = "GIT_VERSION_FLAG"
+ BUILD_DATE = "BUILD_DATE_FLAG"
+ BUILD_IP = "BUILD_IP_FLAG"
+)
+
+func Info() {
+ fmt.Printf("version:%s\n", SERVICE_VERSION)
+ fmt.Printf("git branch:%s\n", GIT_BRANCH)
+ fmt.Printf("git version:%s\n", GIT_VERSION)
+ fmt.Printf("build date:%s\n", BUILD_DATE)
+ fmt.Printf("build ip:%s\n", BUILD_IP)
+
+ os.Exit(0) //退出进程
+}
+
+func GetVersion() *Version {
+ return &Version{
+ ServiceVer: SERVICE_VERSION,
+ GitVer: GIT_VERSION,
+ BuildDate: BUILD_DATE,
+ BuildIp: BUILD_IP,
+ Branch: GIT_BRANCH,
+ }
+}
+
+type Version struct {
+ ServiceVer string `json:"serviceVer"`
+ GitVer string `json:"gitVer"`
+ BuildDate string `json:"buildDate"`
+ BuildIp string `json:"buildIp"`
+ Branch string `json:"branch"`
+}
diff --git a/pub/common/swaggerutil/swagger.go b/pub/common/swaggerutil/swagger.go
new file mode 100644
index 0000000..741f3bd
--- /dev/null
+++ b/pub/common/swaggerutil/swagger.go
@@ -0,0 +1,27 @@
+package swaggerutil
+
+import (
+ "fmt"
+ "videoweb/pub/common/netutil"
+
+ "github.com/gin-gonic/gin"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+)
+
+func isDevHost(runMode string, svcListenIp string) bool {
+ localIp := netutil.GetLocalIP()
+ return (runMode == "dev" || runMode == "debug") && localIp != svcListenIp
+}
+
+func SetupGinSwagger(runMode, svcListenIp, svcListenPort string) func(r *gin.Engine) {
+ return func(r *gin.Engine) {
+ if isDevHost(runMode, svcListenIp) {
+ url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", netutil.GetLocalIP(), svcListenPort))
+ r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
+ } else {
+ url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", svcListenIp, svcListenPort))
+ r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
+ }
+ }
+}
diff --git a/pub/common/synclock/lock.go b/pub/common/synclock/lock.go
new file mode 100644
index 0000000..9245e70
--- /dev/null
+++ b/pub/common/synclock/lock.go
@@ -0,0 +1,102 @@
+package synclock
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/redis"
+)
+
+const (
+ UserExtSyncLockFmt = "userExtSyncLock:%d" // 用户账户扩展信息
+ UserExtSyncLockExpire = 5 * time.Minute // 用户账户扩展信息 过期时间
+
+ UserAccountLockFmt = "UserAccountLockFmt:%d" // 用户账户
+ UserAccountLockExpire = 5 * time.Minute // 用户账户 过期时间
+ TraderAccountLockFmt = "TraderAccountLockFmt:%s" //商人账户
+ SpinLockExpire = 3 * time.Second
+)
+
+// UserExtSyncLock 通过redis按照用户ID加锁
+func UserExtcLock(userId int64) bool {
+ v, e := redis.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire)
+ if !v || e != nil {
+ return false
+ }
+
+ return true
+}
+
+// UserExtSpinLock 通过redis按照用户ID加锁
+func UserExtSpinLock(userId int64, lockTimeOut time.Duration) (bool, error) {
+ endTime := time.Now().Add(lockTimeOut)
+ for time.Now().Before(endTime) {
+ value, err := redis.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire)
+ if err != nil {
+ return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
+ }
+ if value {
+ return true, nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+
+ return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
+}
+
+// UserExtUnlock 按照用户ID释放锁
+func UserExtUnlock(userId int64) {
+ redis.Del(fmt.Sprintf(UserExtSyncLockFmt, userId))
+}
+
+// UserAccountLock 通过redis按照用户ID加锁
+func UserAccountLock(userId int64) bool {
+ v, e := redis.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire)
+ if !v || e != nil {
+ return false
+ }
+
+ return true
+}
+
+// UserAccountSpinLock 通过redis按照用户ID加锁
+func UserAccountSpinLock(userId int64, lockTimeOut time.Duration) (bool, error) {
+ endTime := time.Now().Add(lockTimeOut)
+ for time.Now().Before(endTime) {
+ value, err := redis.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire)
+ if err != nil {
+ return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
+ }
+ if value {
+ return true, nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+ return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
+}
+
+// 通过redis按照商人ID加锁
+func TraderAccountSpinLock(userId string, lockTimeOut time.Duration) (bool, error) {
+ endTime := time.Now().Add(lockTimeOut)
+ for time.Now().Before(endTime) {
+ value, err := redis.Setnx_NewOK(fmt.Sprintf(TraderAccountLockFmt, userId), true, SpinLockExpire)
+ if err != nil {
+ return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
+ }
+ if value {
+ return true, nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+ return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
+}
+
+// 按照商人ID释放锁
+func TraderAccountUnlock(userId string) {
+ redis.Del(fmt.Sprintf(TraderAccountLockFmt, userId))
+}
+
+// UserAccountUnlock 按照用户ID释放锁
+func UserAccountUnlock(userId int64) {
+ redis.Del(fmt.Sprintf(UserAccountLockFmt, userId))
+}
diff --git a/pub/common/tgbot/helper.go b/pub/common/tgbot/helper.go
new file mode 100644
index 0000000..58ce1de
--- /dev/null
+++ b/pub/common/tgbot/helper.go
@@ -0,0 +1,94 @@
+package tgbot
+
+import (
+ "videoweb/pub/common/srvdef"
+)
+
+//获取群聊id
+//https://api.telegram.org/bot1803190372:AAGmrrN6xUoh7IKiv4qCVJYdCs8wLt1ffCw/getUpdates
+
+var (
+ //配置
+ //zhihu app机器人 2048234434:AAF3XoW4iE6vAOmttTT_bofgan_PleKuaEk
+ //zhihu web机器人 1973289619:AAHjCZ8KQOewVfr20foHKlKVwJoulrf_OFc
+ tgToken = "2048234434:AAF3XoW4iE6vAOmttTT_bofgan_PleKuaEk"
+ //程序支付监控群tgids
+ programTgGroupIds = []int64{-778146822}
+ //天行监控组tgids
+ monitorTgGroupIds = []int64{-1001373963166}
+ //管理人员tgid(yl:1652126945)
+ adminTgIds = []int64{1652126945}
+ prefix = "[汁乎App]"
+ tgBot *TgBot = nil
+)
+
+func InitTgBot(token string, prefixStr string) *srvdef.SrvErr {
+ if tgBot != nil {
+ return nil
+ }
+ if prefixStr != "" {
+ prefix = prefixStr
+ }
+ tgBot = NewTelegramBot(token)
+ if serr := tgBot.Init(); nil != serr {
+ return serr
+ }
+
+ //SendToAll("支付监控测试", "Test")
+ return nil
+}
+
+func ReleaseTgBot() {
+ if tgBot == nil {
+ return
+ }
+ tgBot.Release()
+}
+
+// SendToAdmins 发到管理人员
+func SendToAdmins(title string, info string) {
+ sendTo(adminTgIds, title, info)
+}
+
+// SendToProgram 发到程序
+func SendToProgram(title string, info string) {
+ sendTo(programTgGroupIds, title, info)
+}
+
+// SendToMonitor 发到监控群组
+func SendToMonitor(title string, info string) {
+ sendTo(monitorTgGroupIds, title, info)
+}
+
+// SendToProgram 发到所有
+func SendToAll(title string, info string) {
+ groupIds := programTgGroupIds
+ groupIds = append(groupIds, monitorTgGroupIds...)
+ //groupIds = append(groupIds, adminTgIds...)
+ sendTo(groupIds, title, info)
+}
+
+func sendTo(ids []int64, title string, info string) {
+ if tgBot == nil {
+ return
+ }
+ //异步发送
+ go func() {
+ msg := prefix + "\n" + title + "\n" + info
+ tgBot.Send(ids, msg)
+ }()
+}
+
+func RegisterCommands(commands []*BotCmd) {
+ if tgBot == nil {
+ return
+ }
+ tgBot.RegisterCommands(commands)
+}
+
+func Receive() {
+ if tgBot == nil {
+ return
+ }
+ tgBot.Receive()
+}
diff --git a/pub/common/tgbot/tgbot.go b/pub/common/tgbot/tgbot.go
new file mode 100644
index 0000000..de5989c
--- /dev/null
+++ b/pub/common/tgbot/tgbot.go
@@ -0,0 +1,169 @@
+package tgbot
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+type RSP_TYPE int
+
+const (
+ TextType RSP_TYPE = 0
+ FileType RSP_TYPE = 1
+)
+
+type TgCmdRsp struct {
+ Buf []byte
+ BufType RSP_TYPE
+}
+
+// TgBot Telegram机器人.
+type TgBot struct {
+ // 机器人标示;
+ token string
+ // // 管理员分组;
+ // admins []int64
+ // // 管理群分组;
+ // groups []int64
+ // 机器人引擎;
+ engine *tgbotapi.BotAPI
+ // 命令存储集;
+ command map[string]func(string, int64) TgCmdRsp
+ help string
+ exitRcv chan interface{}
+}
+
+// BotCmd 命令.
+type BotCmd struct {
+ // 指令;
+ Key string
+ // 摘要;
+ Summary string
+ // 处理;
+ Handle func(string, int64) TgCmdRsp
+}
+
+type BotCmdList []*BotCmd
+
+// NewBotCmd 新的命令.
+func NewBotCmd(key, summary string, handle func(string, int64) TgCmdRsp) *BotCmd {
+ return &BotCmd{
+ Key: key,
+ Summary: summary,
+ Handle: handle,
+ }
+}
+
+// NewTelegramBot 新的Telegram机器人.
+func NewTelegramBot(token string) *TgBot {
+ return &TgBot{
+ token: token,
+ exitRcv: make(chan interface{}),
+ }
+}
+
+// Init 机器人初始化.
+func (tb *TgBot) Init() *srvdef.SrvErr {
+ var err error
+ tb.engine, err = tgbotapi.NewBotAPI(tb.token)
+ if err != nil {
+ log.Warn("(tgbot:Init) tgbotapi.NewBotAPI failed", log.E(err))
+ return srvdef.New(srvdef.BusinessLevel, "", "")
+ }
+ tb.command = make(map[string]func(string, int64) TgCmdRsp)
+ tb.command["help"] = tb.menu
+ return nil
+}
+
+// Release 机器人退出释放
+func (tb *TgBot) Release() *srvdef.SrvErr {
+ tb.engine.StopReceivingUpdates()
+ close(tb.exitRcv)
+ return nil
+}
+
+func (tb *TgBot) menu(string, int64) TgCmdRsp {
+ return TgCmdRsp{Buf: []byte(tb.help), BufType: TextType}
+}
+
+// RegisterCommands 注册命令并且自动生成help.
+func (tb *TgBot) RegisterCommands(commands []*BotCmd) {
+ for _, cmd := range commands {
+ tb.command[cmd.Key] = cmd.Handle
+ tb.help += fmt.Sprintln("/" + cmd.Key + ": " + cmd.Summary)
+ }
+}
+
+// Receive 接收消息(阻塞).
+func (tb *TgBot) Receive() error {
+ log.Info("(tgbot:Receive) start")
+ // 设置更新频率;
+ cfg := tgbotapi.NewUpdate(0)
+ //cfg.Timeout = 1
+
+ // 得到更新管道;
+ updates, err := tb.engine.GetUpdatesChan(cfg)
+ if err != nil {
+ log.Warn("(tgbot:Receive) tb.engine.GetUpdatesChan failed", log.E(err))
+ return srvdef.New(srvdef.BusinessLevel, "", "")
+ }
+ for {
+ select {
+ case update := <-updates:
+ tb.ProcessMessage(&update)
+ case <-tb.exitRcv:
+ log.Info("(tgbot:Receive) exist")
+ break
+ default:
+ }
+ time.Sleep(time.Second * 1)
+ }
+ return nil
+}
+
+func (tb *TgBot) ProcessMessage(update *tgbotapi.Update) {
+ // 忽略非消息更新;
+ if update.Message == nil {
+ log.Info("(tgbot:ProcessMessage) message is nil")
+ return
+ }
+ info := fmt.Sprintf("[Name: %s%s][ID: %d][Text: %s]\n",
+ update.Message.From.FirstName, update.Message.From.LastName,
+ update.Message.Chat.ID, update.Message.Text)
+ log.Info(info)
+ if update.Message.IsCommand() {
+ message := tgbotapi.NewMessage(update.Message.Chat.ID, "")
+ handle, ok := tb.command[update.Message.Command()]
+ if !ok {
+ message.Text = "unknown command"
+ } else {
+ param := update.Message.CommandArguments()
+ rsp := handle(param, update.Message.Chat.ID)
+ switch rsp.BufType {
+ case FileType:
+
+ default:
+ message.Text = string(rsp.Buf)
+ }
+ //message.Text = handle(param)
+ }
+ tb.engine.Send(message)
+ }
+}
+
+func (tb *TgBot) StopReceive() {
+ tb.engine.StopReceivingUpdates()
+ close(tb.exitRcv)
+}
+
+// Send 发送消息.
+func (tb *TgBot) Send(ids []int64, text string) {
+ for _, id := range ids {
+ message := tgbotapi.NewMessage(id, text)
+ tb.engine.Send(message)
+ }
+}
diff --git a/pub/common/tgbot/tgbot_test.go b/pub/common/tgbot/tgbot_test.go
new file mode 100644
index 0000000..9f1e8a6
--- /dev/null
+++ b/pub/common/tgbot/tgbot_test.go
@@ -0,0 +1,35 @@
+package tgbot
+
+import (
+ "testing"
+)
+
+func create(t *testing.T) *TgBot {
+ token := "1014745091:AAFkw5j3Fw4ytvWgIll6iRH1q5WooS6JO74"
+ robot := NewTelegramBot(token)
+ if err := robot.Init(); err != nil {
+ t.Error(err)
+ t.Fail()
+ }
+ // 注册命令.
+ cmds := BotCmdList{NewBotCmd("test", "return testfunc result", testfunc)}
+ robot.RegisterCommands(cmds)
+ return robot
+}
+
+func testfunc(string, int64) TgCmdRsp {
+ return TgCmdRsp{Buf: []byte("help"), BufType: TextType}
+}
+
+func TestReceive(t *testing.T) {
+ robot := create(t)
+ if err := robot.Receive(); err != nil {
+ t.Error(err)
+ t.Fail()
+ }
+}
+
+func TestSend(t *testing.T) {
+ robot := create(t)
+ robot.Send([]int64{790603940}, "test send")
+}
diff --git a/pub/common/timehelper.go b/pub/common/timehelper.go
new file mode 100644
index 0000000..2d6b21b
--- /dev/null
+++ b/pub/common/timehelper.go
@@ -0,0 +1,55 @@
+package common
+
+import (
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func BindUrl(hostname string, path ...string) string {
+ if hostname == "" {
+ return strings.TrimLeft(filepath.Join(path...), "/")
+ }
+ if len(path) == 0 {
+ return hostname
+ }
+ su := strings.TrimLeft(filepath.Join(path...), "/")
+ pr := strings.TrimRight(hostname, "/")
+ return pr + "/" + su
+}
+
+// 将time.Time转换成int64
+func Time2Stamp(t time.Time) int64 {
+ return t.UnixNano() / 1e9
+}
+
+/**时间对象->字符串*/
+func Time2Str() string {
+ const shortForm = "2006-01-01 15:04:05"
+ t := time.Now()
+ temp := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.Local)
+ str := temp.Format(shortForm)
+ return str
+}
+
+/*时间戳->字符串*/
+func Stamp2Str(stamp int64) string {
+ timeLayout := "2006-01-02 15:04:05"
+ str := time.Unix(stamp/1000, 0).Format(timeLayout)
+ return str
+}
+
+/*时间戳->时间对象*/
+func Stamp2Time(stamp int64) time.Time {
+ stampStr := Stamp2Str(stamp)
+ timer := Str2Time(stampStr)
+ return timer
+}
+
+/**字符串->时间对象*/
+func Str2Time(formatTimeStr string) time.Time {
+ timeLayout := "2006-01-02 15:04:05"
+ loc, _ := time.LoadLocation("Local")
+ theTime, _ := time.ParseInLocation(timeLayout, formatTimeStr, loc) //使用模板在对应时区转化为time.time类型
+ return theTime
+}
diff --git a/pub/common/topicmessage/topicMessage.go b/pub/common/topicmessage/topicMessage.go
new file mode 100644
index 0000000..b4a4876
--- /dev/null
+++ b/pub/common/topicmessage/topicMessage.go
@@ -0,0 +1,79 @@
+package topicmessage
+
+import (
+ "time"
+)
+
+/* 通信协议,第一版*/
+
+// 用户用户注册消息
+type UserRegisterMsg struct {
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ Name string `json:"name" bson:"name"` // 名称
+ IsDirect bool `json:"isDirect" bson:"isDirect"` // true:是直推用户
+ DeviceId string `json:"deviceId" bson:"deviceId"` // 设备Id
+ IP string `json:"ip,omitempty" bson:"ip"` // IP
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+ InviteCode string `json:"inviteCode" bson:"inviteCode"` // 被邀请码
+ RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间
+ UserAgent string `json:"userAgent" bson:"userAgent"` // User-Agent识别
+}
+
+// 用户第一次访问
+type UserVisitMsg struct {
+ UserId int64 `bson:"userId" json:"userId"` // 用户Id
+ SumDate time.Time `bson:"sumDate" json:"sumDate"` // 记录时间
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ IP string `json:"ip,omitempty" bson:"ip"` // IP
+ Version string `json:"version,omitempty" bson:"version"` // APP版本号
+ VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+}
+
+type UserOrderMsg struct {
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字
+ CID string `json:"cid" bson:"cid"` // 渠道id
+ Type string `bson:"type" json:"type"` // 充值方式
+ RchgUse int64 `json:"rchgUse" bson:"rchgUse"` // 充值用途 1:金币 2:VIP
+ Repeat bool `bson:"repeat" json:"repeat"` // 复冲: true 重复充值 false 第一次
+ OrderId string `json:"orderId" bson:"orderId"` // 流水id
+ Money int64 `json:"money" bson:"money"` // 充值金额 订单金额
+ PayMoney int64 `json:"payMoney" bson:"payMoney"` // 实际到账金额 用户实际支付金额
+ Status int `json:"status" bson:"status"` // 2付款失败 3付款成功(目前只有成功才发送)
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+ Rate string `json:"rate" bson:"rate"` // 渠道费率
+ SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type UserChannelEvent struct {
+ UserId int64 `bson:"userId" json:"userId"` // 用户Id
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+ WatchCount int64 `json:"watchCount,omitempty" bson:"watchCount"` // 观看次数
+ WatchTime int64 `json:"watchTime,omitempty" bson:"watchTime"` // 观看时长
+ AdClick int64 `json:"adClick,omitempty" bson:"adClick"` // 广告点击
+ AppClick int64 `json:"appClick,omitempty" bson:"appClick"` // APP点击
+ RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数
+}
+
+type MediaPlayEvent struct {
+ MediaId int64 `json:"mediaId" bson:"mediaId"` // 视频Id
+ Title string `json:"title" bson:"title"` // 标题
+ FileId string `json:"fileId" bson:"fileId"` // 媒资库ID
+ ActionType int64 `json:"actionType" bson:"actionType"` // 类型:0:播放 1:点赞 2:购买
+ UserId int64 `json:"userId" bson:"userId"` // 用户Id
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ WatchAt time.Time `json:"watchAt" bson:"watchAt"` // 时间
+}
diff --git a/pub/common/typeConv.go b/pub/common/typeConv.go
new file mode 100644
index 0000000..7f51bb4
--- /dev/null
+++ b/pub/common/typeConv.go
@@ -0,0 +1,64 @@
+package common
+
+import (
+ "reflect"
+ "unicode"
+)
+
+// StructToMap 对象转map,去掉无效值
+func StructToMap(u interface{}) (m map[string]interface{}) {
+ t := reflect.TypeOf(u)
+ v := reflect.ValueOf(u)
+ p := make(map[string]interface{})
+ for i := 0; i < t.NumField(); i++ {
+ fv := v.Field(i)
+ ft := t.Field(i)
+ jsonTag, _ := ft.Tag.Lookup("json")
+ switch fv.Kind() {
+ case reflect.String:
+ if fv.String() != "" {
+ p[jsonTag] = fv.Interface()
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if fv.Int() != 0 {
+ p[jsonTag] = fv.Interface()
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ if fv.Uint() != 0 {
+ p[jsonTag] = fv.Interface()
+ }
+ case reflect.Float32, reflect.Float64:
+ if fv.Float() != 0 {
+ p[jsonTag] = fv.Interface()
+ }
+ default:
+ p[jsonTag] = fv.Interface()
+
+ }
+
+ }
+ return p
+}
+
+// Ucfirst () 字符串首字母大写
+func Ucfirst(str string) string {
+ for i, v := range str {
+ return string(unicode.ToUpper(v)) + str[i+1:]
+ }
+ return ""
+}
+
+// Lcfirst () 字符串首字母小写
+func Lcfirst(str string) string {
+ for i, v := range str {
+ return string(unicode.ToLower(v)) + str[i+1:]
+ }
+ return ""
+}
+
+// MobileBlurry ()
+func MobileBlurry(str string) string {
+
+ return str[0:3] + "****" + str[7:]
+
+}
diff --git a/pub/common/useragent/sdk_common.go b/pub/common/useragent/sdk_common.go
new file mode 100644
index 0000000..19b62cb
--- /dev/null
+++ b/pub/common/useragent/sdk_common.go
@@ -0,0 +1,83 @@
+package useragent
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "unicode/utf8"
+)
+
+// SDKCommonHeader 由 H5/Flutter 将 SDK 完整公共参数转为 UTF-8 JSON,再用标准 Base64 编码后传入。
+const SDKCommonHeader = "X-SDK-Common"
+
+// 限制解析开销;正常 SDK 公共参数远小于此大小。
+const maxSDKCommonHeaderSize = 16 * 1024
+
+// SDKCommonFields 接收五项 SDK 公共字段及客户端 app_version,完整 fields 中的其他字段自动忽略。
+// 业务 DevID、用户 ID、渠道等继续走原有业务参数,不能被 SDK 对象覆盖。
+type SDKCommonFields struct {
+ SID string `json:"sid" bson:"sid"`
+ DeviceID string `json:"device_id" bson:"device_id"`
+ Device string `json:"device" bson:"device"`
+ DeviceFingerprint string `json:"device_fingerprint" bson:"device_fingerprint"`
+ FPVersion string `json:"fp_version" bson:"fp_version"`
+ AppVersion string `json:"app_version" bson:"app_version"` // 客户端版本随下单快照保存,支付回调不使用当前版本补值。
+}
+
+// ParseSDKCommonHeader 缺少请求头返回 nil;格式错误交给调用方记录,不阻断业务请求。
+// 不修剪、截断或推算字段值,SDK 指纹生成失败时也保留其空值。
+func ParseSDKCommonHeader(header string) (*SDKCommonFields, error) {
+ header = strings.TrimSpace(header)
+ if header == "" {
+ return nil, nil
+ }
+ if len(header) > maxSDKCommonHeaderSize {
+ return nil, fmt.Errorf("SDK 公共参数请求头超过 %d 字节", maxSDKCommonHeaderSize)
+ }
+ data, err := base64.StdEncoding.DecodeString(header)
+ if err != nil {
+ return nil, fmt.Errorf("SDK 公共参数 Base64 解码失败: %w", err)
+ }
+ if !utf8.Valid(data) {
+ return nil, fmt.Errorf("SDK 公共参数不是有效的 UTF-8")
+ }
+ var fields *SDKCommonFields
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return nil, fmt.Errorf("SDK 公共参数 JSON 解析失败: %w", err)
+ }
+ if fields == nil {
+ return nil, fmt.Errorf("SDK 公共参数必须是 JSON 对象,不能为 null")
+ }
+ return fields, nil
+}
+
+// Clone 下单时保存独立快照,避免请求对象后续变化影响支付事件。
+func (fields *SDKCommonFields) Clone() *SDKCommonFields {
+ if fields == nil {
+ return nil
+ }
+ snapshot := *fields
+ return &snapshot
+}
+
+// MissingFields 仅用于缺失观测;合法空值仍原样上报,不作为业务失败。
+func (fields *SDKCommonFields) MissingFields() []string {
+ if fields == nil {
+ fields = &SDKCommonFields{}
+ }
+ var missing []string
+ for _, field := range []struct{ name, value string }{
+ {"sid", fields.SID},
+ {"device_id", fields.DeviceID},
+ {"device", fields.Device},
+ {"device_fingerprint", fields.DeviceFingerprint},
+ {"fp_version", fields.FPVersion},
+ {"app_version", fields.AppVersion},
+ } {
+ if strings.TrimSpace(field.value) == "" {
+ missing = append(missing, field.name)
+ }
+ }
+ return missing
+}
diff --git a/pub/common/useragent/ua.go b/pub/common/useragent/ua.go
new file mode 100644
index 0000000..8c9a5f7
--- /dev/null
+++ b/pub/common/useragent/ua.go
@@ -0,0 +1,109 @@
+package useragent
+
+import (
+ "net/url"
+ "strings"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+type UA struct {
+ DevID string
+ DevType string
+ SysType pubdef.SysType
+ Ver string
+ BuildID string
+ UserAgent string
+ SDKCommon *SDKCommonFields // SDK 公共参数独立保存,不能与原业务 UA 字段混用。
+ RequestPath string // 仅用于定位埋点参数缺失的业务接口。
+}
+
+func IsH5(sysType pubdef.SysType) bool {
+ return sysType.IsH5()
+}
+
+func (s UA) IsH5() bool {
+ return s.SysType.IsH5()
+}
+
+func UserAgent(c *gin.Context) {
+ var uastr string
+ ua := UA{RequestPath: c.FullPath()}
+ var sdkErr error
+ ua.SDKCommon, sdkErr = ParseSDKCommonHeader(c.GetHeader(SDKCommonHeader))
+ if sdkErr != nil {
+ log.Error("【SDK公共参数解析失败】UserAgent", log.Any("接口", ua.RequestPath), log.E(sdkErr))
+ }
+ userAgent := c.GetHeader("X-User-Agent") //兼容H5 UserAgent
+ if userAgent == "" {
+ userAgent = c.Request.UserAgent()
+ }
+ ua.UserAgent = userAgent
+ if userAgent != "" {
+ // "BuildID=com.abc.Butterfly;SysType=h5;DevID=B9A00938-FE68-4686-9461-A365C7E5110D;Ver=0.0.1;DevType=iPhone;"
+ decodeUa, err := url.QueryUnescape(userAgent)
+ if err != nil {
+ log.Warn("URLDecode user-agent error", log.Any("ua", userAgent))
+ uastr = userAgent
+ } else {
+ uastr = decodeUa
+ }
+ uas := strings.Split(uastr, ";")
+ for _, v := range uas {
+ vss := strings.Split(strings.TrimSpace(v), "=")
+ if len(vss) < 2 {
+ c.Set(consts.CtxUAH5, vss[0])
+ // log.Warn("user-agent miss", log.Any("ua", userAgent), log.Any("v", v))
+ continue
+ }
+ switch strings.TrimSpace(vss[0]) {
+ case "DevID":
+ ua.DevID = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxDevId, ua.DevID)
+ case "DevType":
+ ua.DevType = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxDevType, ua.DevType)
+ case "SysType":
+ input := strings.ToLower(strings.TrimSpace(vss[1]))
+ switch {
+ case strings.Contains(input, "h5_android"):
+ ua.SysType = pubdef.ST_H5Android
+ case strings.Contains(input, "h5_ios"):
+ ua.SysType = pubdef.ST_H5IOS
+ case strings.Contains(input, "h5_pc"):
+ ua.SysType = pubdef.ST_H5PC
+ case strings.Contains(input, "android"):
+ ua.SysType = pubdef.ST_Android
+ case strings.Contains(input, "ios"):
+ ua.SysType = pubdef.ST_IOS
+ case strings.Contains(input, "h5"):
+ ua.SysType = pubdef.ST_H5
+ case strings.Contains(input, "pc"):
+ ua.SysType = pubdef.ST_PC
+ case strings.Contains(input, "light"):
+ ua.SysType = pubdef.ST_Light
+ case strings.Contains(input, "tg"):
+ ua.SysType = pubdef.ST_TG
+ case strings.Contains(input, "web"):
+ ua.SysType = pubdef.ST_Web
+ }
+ // 存入 context 时用 string,保持 ctx.GetString 兼容
+ c.Set(consts.CtxSysType, string(ua.SysType))
+ case "Ver":
+ ua.Ver = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxVersion, ua.Ver)
+ case "BuildID":
+ ua.BuildID = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxBuildId, ua.BuildID)
+ default:
+ log.Warn("user-agent unknown", log.Any("ua", userAgent))
+ }
+ }
+ } else {
+ log.Warn("user-agent empty")
+ }
+ c.Set(consts.CtxUA, ua)
+}
diff --git a/pub/common/utils/array.go b/pub/common/utils/array.go
new file mode 100644
index 0000000..e4b06d4
--- /dev/null
+++ b/pub/common/utils/array.go
@@ -0,0 +1,292 @@
+package utils
+
+import (
+ "math/rand"
+ "reflect"
+)
+
+//数组求交集,差集,并集
+/*求并集**/
+func Union(slice1, slice2 []string) []string {
+ m := make(map[string]int)
+ for _, v := range slice1 {
+ m[v]++
+ }
+
+ for _, v := range slice2 {
+ times, _ := m[v]
+ if times == 0 {
+ slice1 = append(slice1, v)
+ }
+ }
+ return slice1
+}
+
+// 求交集
+func Intersect(slice1, slice2 []string) []string {
+ m := make(map[string]int)
+ nn := make([]string, 0)
+ for _, v := range slice1 {
+ m[v]++
+ }
+
+ for _, v := range slice2 {
+ times, _ := m[v]
+ if times == 1 {
+ nn = append(nn, v)
+ }
+ }
+ return nn
+}
+
+// 求交集
+func IntersectInt64(slice1, slice2 []int64) []int64 {
+ m := make(map[int64]int)
+ nn := make([]int64, 0)
+ for _, v := range slice1 {
+ m[v]++
+ }
+
+ for _, v := range slice2 {
+ times, _ := m[v]
+ if times == 1 {
+ nn = append(nn, v)
+ }
+ }
+ return nn
+}
+
+// 求差集 slice1-并集
+func Difference(slice1, slice2 []string) []string {
+ m := make(map[string]int)
+ nn := make([]string, 0)
+ inter := Intersect(slice1, slice2)
+ for _, v := range inter {
+ m[v]++
+ }
+
+ for _, value := range slice1 {
+ times, _ := m[value]
+ if times == 0 {
+ nn = append(nn, value)
+ }
+ }
+ return nn
+}
+
+// 求差集 slice1-并集
+func DifferenceInt64(slice1, slice2 []int64) []int64 {
+ m := make(map[int64]int)
+ nn := make([]int64, 0)
+ inter := IntersectInt64(slice1, slice2)
+ for _, v := range inter {
+ m[v]++
+ }
+
+ for _, value := range slice1 {
+ times, _ := m[value]
+ if times == 0 {
+ nn = append(nn, value)
+ }
+ }
+ return nn
+}
+
+// 去重
+func RemoveRepeat(slc []int64) []int64 {
+ result := []int64{} // 存放结果
+ for i := range slc {
+ flag := true
+ for j := range result {
+ if slc[i] == result[j] {
+ flag = false // 存在重复元素,标识为false
+ break
+ }
+ }
+ if flag { // 标识为false,不添加进结果
+ result = append(result, slc[i])
+ }
+ }
+ return result
+}
+
+func IncludeInt32(slc []int32, n int32) bool {
+ for _, v := range slc {
+ if v == n {
+ return true
+ }
+ }
+ return false
+}
+
+func IncludeInt64(slc []int64, n int64) bool {
+ for _, v := range slc {
+ if v == n {
+ return true
+ }
+ }
+ return false
+}
+
+func RemoveInt64(slc []int64, n int64) []int64 {
+ for i := 0; i < len(slc); {
+ if slc[i] == n {
+ slc = append(slc[:i], slc[i+1:]...)
+ } else {
+ i++
+ }
+ }
+ return slc
+}
+
+func RemoveString(slc []string, n string) []string {
+ for i := 0; i < len(slc); {
+ if slc[i] == n {
+ slc = append(slc[:i], slc[i+1:]...)
+ } else {
+ i++
+ }
+ }
+ return slc
+}
+
+// RemoveRepeatString 去重string
+func RemoveRepeatString(slc []string) []string {
+ result := []string{} // 存放结果
+ for i := range slc {
+ flag := true
+ for j := range result {
+ if slc[i] == result[j] {
+ flag = false // 存在重复元素,标识为false
+ break
+ }
+ }
+ if flag { // 标识为false,不添加进结果
+ result = append(result, slc[i])
+ }
+ }
+ return result
+}
+
+func IncludeString(slc []string, s string) bool {
+ for _, v := range slc {
+ if v == s {
+ return true
+ }
+ }
+ return false
+}
+
+func EqualInt64s(slice1, slice2 []int64) bool {
+ if len(slice1) != len(slice2) {
+ return false
+ }
+ for _, v1 := range slice1 {
+ find := false
+ for _, v2 := range slice2 {
+ if v1 == v2 {
+ find = true
+ break
+ }
+ }
+ if !find {
+ return false
+ }
+ }
+ return true
+}
+
+func EqualInt64WithOrder(slice1, slice2 []int64) bool {
+ if len(slice1) != len(slice2) {
+ return false
+ }
+ for k, v1 := range slice1 {
+ if slice2[k] != v1 {
+ return false
+ }
+ }
+ return true
+}
+
+func EqualString(slice1, slice2 []string) bool {
+ if len(slice1) != len(slice2) {
+ return false
+ }
+ for _, v1 := range slice1 {
+ find := false
+ for _, v2 := range slice2 {
+ if v1 == v2 {
+ find = true
+ break
+ }
+ }
+ if find == false {
+ return false
+ }
+ }
+ return true
+}
+
+// 切片乱序
+func ShuffleSlice(slice interface{}) {
+ rv := reflect.ValueOf(slice)
+ if rv.Type().Kind() != reflect.Slice {
+ return
+ }
+
+ length := rv.Len()
+ if length < 2 {
+ return
+ }
+ swap := reflect.Swapper(slice)
+ //rand.Seed(time.Now().Unix())
+ for i := length - 1; i >= 0; i-- {
+ j := rand.Intn(length)
+ swap(i, j)
+ }
+ return
+}
+
+func RemoveOneInt64(slc []int64, n int64) []int64 {
+ for i := 0; i < len(slc); {
+ if slc[i] == n {
+ slc = append(slc[:i], slc[i+1:]...)
+ break
+ } else {
+ i++
+ }
+ }
+ return slc
+}
+
+// 求最大值
+func SliceMaxInt64(slc []int64) int64 {
+ maxVal := slc[0]
+ for i := 1; i < len(slc); i++ {
+ //从第二个 元素开始循环比较,如果发现有更大的,则交换
+ if maxVal < slc[i] {
+ maxVal = slc[i]
+ }
+ }
+ return maxVal
+}
+
+// 求最大值
+func SliceMinInt64(slc []int64) int64 {
+ minVal := slc[0]
+ for i := 1; i < len(slc); i++ {
+ //从第二个 元素开始循环比较,如果发现有更小的,则交换
+ if minVal > slc[i] {
+ minVal = slc[i]
+ }
+ }
+ return minVal
+}
+
+func ReverseAny(s interface{}) {
+ n := reflect.ValueOf(s).Len()
+ swap := reflect.Swapper(s)
+ for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
+ swap(i, j)
+ }
+}
diff --git a/pub/common/utils/convUtil.go b/pub/common/utils/convUtil.go
new file mode 100644
index 0000000..dda3ea9
--- /dev/null
+++ b/pub/common/utils/convUtil.go
@@ -0,0 +1,184 @@
+package utils
+
+import (
+ "encoding/json"
+ "path/filepath"
+ "strings"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ID = primitive.ObjectID
+
+// ToJsonM 将struct转换为 Json Map
+func ToJsonM(obj interface{}) (map[string]interface{}, error) {
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+// ToBsonM 将struct转换为 Bson Map
+func ToBsonM(s interface{}) (bson.M, error) {
+ data, err := bson.Marshal(s)
+ if err != nil {
+ return nil, err
+ }
+ m := make(bson.M)
+ if err := bson.Unmarshal(data, &m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func MapToJsonString(m map[string]string) (string, error) {
+ if data, err := json.Marshal(m); err != nil {
+ return "", err
+ } else {
+ return string(data), nil
+ }
+}
+
+func StructToJsonString(s interface{}) (string, error) {
+ j, err := json.Marshal(s)
+ if err != nil {
+ return "", err
+ }
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(j, &m); err != nil {
+ return "", err
+ }
+ if data, err := json.Marshal(m); err != nil {
+ return "", err
+ } else {
+ return string(data), nil
+ }
+}
+
+// BMapToStruct s is struct ptr
+func BMapToStruct(s interface{}, m map[string]interface{}) error {
+ bsonBytes, err := bson.Marshal(m)
+ if err != nil {
+ return err
+ }
+ return bson.Unmarshal(bsonBytes, s)
+}
+
+// JMapToStruct s is struct ptr
+func JMapToStruct(s interface{}, m map[string]interface{}) error {
+ bsonBytes, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(bsonBytes, s)
+}
+
+// JSONStruct2Map 将struct转换为Map
+func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+// Map2JSONStruct 将map转换为Json struct
+func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
+ j, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(j, v); err != nil {
+ return err
+ }
+ return err
+}
+
+func IDArray(ids []string) ([]ID, error) {
+ var idArray []ID
+ for _, id := range ids {
+ _id, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ return nil, err
+ }
+ idArray = append(idArray, _id)
+ }
+
+ return idArray, nil
+}
+
+// 拼接https://
+func BindHttpSch(path string) string {
+ if strings.HasPrefix(path, "http") {
+ return path
+ } else {
+ return "https://" + path
+ }
+}
+
+func BindUrl(hostname string, path ...string) string {
+ if hostname == "" {
+ return strings.TrimLeft(filepath.Join(path...), "/")
+ }
+ if len(path) == 0 {
+ return hostname
+ }
+ su := strings.TrimLeft(filepath.Join(path...), "/")
+ pr := strings.TrimRight(hostname, "/")
+ return pr + "/" + su
+}
+
+func ObjectIDs2String(obj []primitive.ObjectID) []string {
+ arr := []string{}
+ if len(obj) == 0 {
+ return arr
+ }
+ for _, o := range obj {
+ arr = append(arr, o.Hex())
+ }
+ return arr
+}
+
+func String2ObjectID(obj []string) []primitive.ObjectID {
+ arr := []primitive.ObjectID{}
+ for _, o := range obj {
+ oid, err := primitive.ObjectIDFromHex(o)
+ if err != nil {
+ continue
+ }
+ arr = append(arr, oid)
+ }
+ return arr
+}
+
+// PercentOf - calculate what percent [number1] is of [number2].
+// ex. 300 is 12.5% of 2400
+func PercentOf(part int, total int) float64 {
+ return (float64(part) * float64(100)) / float64(total)
+}
+
+func MergeMap(src, dest map[string]interface{}) map[string]interface{} {
+ if len(src) == 0 {
+ return dest
+ }
+ if len(dest) == 0 {
+ return src
+ }
+ if len(src) == 0 && len(dest) == 0 {
+ return make(map[string]interface{})
+ }
+ for k, v := range dest {
+ src[k] = v
+ }
+ return src
+}
diff --git a/pub/common/utils/copy.go b/pub/common/utils/copy.go
new file mode 100644
index 0000000..51e0b2c
--- /dev/null
+++ b/pub/common/utils/copy.go
@@ -0,0 +1,130 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/gob"
+ "reflect"
+ "time"
+)
+
+// DeepCopy 深copy
+func DeepCopy(dst, src interface{}) error {
+ var buf bytes.Buffer
+ if err := gob.NewEncoder(&buf).Encode(src); err != nil {
+ return err
+ }
+ return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
+}
+
+// Interface for delegating copy process to type
+type Interface interface {
+ DeepCopy() interface{}
+}
+
+// Iface is an alias to Copy; this exists for backwards compatibility reasons.
+func Iface(iface interface{}) interface{} {
+ return Copy(iface)
+}
+
+// Copy creates a deep copy of whatever is passed to it and returns the copy
+// in an interface{}. The returned value will need to be asserted to the
+// correct type.
+func Copy(src interface{}) interface{} {
+ if src == nil {
+ return nil
+ }
+
+ // Make the interface a reflect.Value
+ original := reflect.ValueOf(src)
+
+ // Make a copy of the same type as the original.
+ cpy := reflect.New(original.Type()).Elem()
+
+ // Recursively copy the original.
+ copyRecursive(original, cpy)
+
+ // Return the copy as an interface.
+ return cpy.Interface()
+}
+
+// copyRecursive does the actual copying of the interface. It currently has
+// limited support for what it can handle. Add as needed.
+func copyRecursive(original, cpy reflect.Value) {
+ // check for implement deepcopy.Interface
+ if original.CanInterface() {
+ if copier, ok := original.Interface().(Interface); ok {
+ cpy.Set(reflect.ValueOf(copier.DeepCopy()))
+ return
+ }
+ }
+
+ // handle according to original's Kind
+ switch original.Kind() {
+ case reflect.Ptr:
+ // Get the actual value being pointed to.
+ originalValue := original.Elem()
+
+ // if it isn't valid, return.
+ if !originalValue.IsValid() {
+ return
+ }
+ cpy.Set(reflect.New(originalValue.Type()))
+ copyRecursive(originalValue, cpy.Elem())
+
+ case reflect.Interface:
+ // If this is a nil, don't do anything
+ if original.IsNil() {
+ return
+ }
+ // Get the value for the interface, not the pointer.
+ originalValue := original.Elem()
+
+ // Get the value by calling Elem().
+ copyValue := reflect.New(originalValue.Type()).Elem()
+ copyRecursive(originalValue, copyValue)
+ cpy.Set(copyValue)
+
+ case reflect.Struct:
+ t, ok := original.Interface().(time.Time)
+ if ok {
+ cpy.Set(reflect.ValueOf(t))
+ return
+ }
+ // Go through each field of the struct and copy it.
+ for i := 0; i < original.NumField(); i++ {
+ // The Type's StructField for a given field is checked to see if StructField.PkgPath
+ // is set to determine if the field is exported or not because CanSet() returns false
+ // for settable fields. I'm not sure why. -mohae
+ if original.Type().Field(i).PkgPath != "" {
+ continue
+ }
+ copyRecursive(original.Field(i), cpy.Field(i))
+ }
+
+ case reflect.Slice:
+ if original.IsNil() {
+ return
+ }
+ // Make a new slice and copy each element.
+ cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
+ for i := 0; i < original.Len(); i++ {
+ copyRecursive(original.Index(i), cpy.Index(i))
+ }
+
+ case reflect.Map:
+ if original.IsNil() {
+ return
+ }
+ cpy.Set(reflect.MakeMap(original.Type()))
+ for _, key := range original.MapKeys() {
+ originalValue := original.MapIndex(key)
+ copyValue := reflect.New(originalValue.Type()).Elem()
+ copyRecursive(originalValue, copyValue)
+ copyKey := Copy(key.Interface())
+ cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
+ }
+
+ default:
+ cpy.Set(original)
+ }
+}
diff --git a/pub/common/utils/file.go b/pub/common/utils/file.go
new file mode 100644
index 0000000..c595e6f
--- /dev/null
+++ b/pub/common/utils/file.go
@@ -0,0 +1,56 @@
+package utils
+
+import "os"
+
+// CreateEmptyFile 创建一个固定size大小的空文件
+// 如果seek文件失败,则删除文件,并返回错误信息
+func CreateEmptyFile(name string, size int64) (f *os.File, err error) {
+ defer func() {
+ if err != nil && f != nil {
+ f.Close()
+ os.Remove(name)
+ }
+ }()
+ f, err = os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
+ if err != nil {
+ return
+ }
+ if _, err = f.Seek(size-1, 0); err != nil {
+ return
+ }
+ _, err = f.Write([]byte{0})
+ return
+}
+
+func WriteFile(data []byte, filename string) (err error) {
+ defer func() {
+ if err != nil {
+ os.Remove(filename)
+ }
+ }()
+
+ pi := PathForInfo(filename)
+ if pi.Dir != "" {
+ if err = os.MkdirAll(pi.Dir, 0755); err != nil {
+ return
+ }
+ }
+
+ f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
+ if err != nil {
+ return
+ }
+ _, err = f.Write(data)
+ f.Close()
+ return
+}
+func PathExists(path string) (bool, error) {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true, nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
diff --git a/pub/common/utils/hash.go b/pub/common/utils/hash.go
new file mode 100644
index 0000000..568035c
--- /dev/null
+++ b/pub/common/utils/hash.go
@@ -0,0 +1,12 @@
+package utils
+
+// // MakeCrc
+// func MakeCrc(s string) uint32 {
+// return crc32.ChecksumIEEE([]byte(s))
+// }
+
+// func Md5V(str string) string {
+// h := md5.New()
+// h.Write([]byte(str))
+// return hex.EncodeToString(h.Sum(nil))
+// }
\ No newline at end of file
diff --git a/pub/common/utils/http.go b/pub/common/utils/http.go
new file mode 100644
index 0000000..1467d0e
--- /dev/null
+++ b/pub/common/utils/http.go
@@ -0,0 +1,283 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+)
+
+var once sync.Once
+var _client *http.Client
+
+func GetHttpClient() *http.Client {
+ once.Do(func() {
+ _client = &http.Client{
+ Timeout: time.Minute * 1, //设置超时时间
+ Transport: &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).DialContext,
+ TLSHandshakeTimeout: 10 * time.Second, //限制 TLS握手的时间
+ ResponseHeaderTimeout: 60 * time.Second, //限制读取response header的时间,默认 timeout + 5*time.Second
+ ExpectContinueTimeout: 1 * time.Second, //限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待。
+ MaxIdleConns: 100, //所有host的连接池最大连接数量,默认无穷大
+ MaxIdleConnsPerHost: 300, //每个host的连接池最大空闲连接数,默认2
+ MaxConnsPerHost: 100, //每个host的最大连接数量
+ IdleConnTimeout: 3 * time.Minute, //how long an idle connection is kept in the connection pool.
+ },
+ }
+ })
+ return _client
+}
+
+func getBytes(request interface{}) ([]byte, error) {
+ var body []byte
+ switch t := request.(type) {
+ case []byte:
+ body = t
+ case string:
+ body = []byte(t)
+ default:
+ var err error
+ body, err = json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return body, nil
+}
+
+type OnHttpResponse func(res *http.Response) error
+
+func HttpRequest(req *http.Request, onResponse OnHttpResponse) error {
+ client := GetHttpClient()
+ res, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if onResponse != nil {
+ err = onResponse(res)
+ }
+ return err
+}
+
+func HttpMethod(url, method string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return err
+ }
+ if header != nil {
+ req.Header = header
+ }
+ return HttpRequest(req, onResponse)
+}
+
+func HttpGet(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ return HttpMethod(url, http.MethodGet, header, body, onResponse)
+}
+
+func HttpPost(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ return HttpMethod(url, http.MethodPost, header, body, onResponse)
+}
+
+func HttpDelete(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ return HttpMethod(url, http.MethodDelete, header, body, onResponse)
+}
+
+func HttpPostRequestJson(url string, request interface{}, resp interface{}) error {
+ header := make(http.Header)
+ header.Set("Content-Type", "application/json")
+ body, err := getBytes(request)
+ if err != nil {
+ return err
+ }
+ return HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, resp)
+ })
+}
+
+func POSTJson(url string, headers map[string]string, data interface{}) (*http.Response, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ body, err := getBytes(data)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest("POST", url, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header = header
+ client := GetHttpClient()
+ return client.Do(req)
+}
+
+func GetWithParam(url string, headers map[string]string, params ...interface{}) (*http.Response, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ for _, p := range params {
+ url = addParams(url, toUrlValues(p))
+ }
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header = header
+ client := GetHttpClient()
+ return client.Do(req)
+}
+
+func GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ for _, p := range params {
+ url = addParams(url, toUrlValues(p))
+ }
+ code := http.StatusBadRequest
+ err := HttpGet(url, header, nil, func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+func POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ body, err := getBytes(params)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ code := http.StatusBadRequest
+ err = HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+func POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ header.Set("Content-Type", "application/json")
+ body, err := getBytes(data)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ code := http.StatusBadRequest
+ err = HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+func DeleteWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ body, err := getBytes(params)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ code := http.StatusBadRequest
+ err = HttpDelete(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+// Add params to a url string.
+func addParams(url_ string, params url.Values) string {
+ if len(params) == 0 {
+ return url_
+ }
+
+ if !strings.Contains(url_, "?") {
+ url_ += "?"
+ }
+
+ if strings.HasSuffix(url_, "?") || strings.HasSuffix(url_, "&") {
+ url_ += params.Encode()
+ } else {
+ url_ += "&" + params.Encode()
+ }
+
+ return url_
+}
+
+func toUrlValues(v interface{}) url.Values {
+ switch t := v.(type) {
+ case url.Values:
+ return t
+ case map[string][]string:
+ return url.Values(t)
+ case map[string]string:
+ rst := make(url.Values)
+ for k, v := range t {
+ rst.Add(k, v)
+ }
+ return rst
+ case nil:
+ return make(url.Values)
+ default:
+ panic("Invalid value")
+ }
+}
diff --git a/pub/common/utils/jwt.go b/pub/common/utils/jwt.go
new file mode 100644
index 0000000..5810a0b
--- /dev/null
+++ b/pub/common/utils/jwt.go
@@ -0,0 +1,127 @@
+package utils
+
+import (
+ "errors"
+ "strings"
+ "time"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+const (
+ userIDKey = "userId"
+ ipKey = "ip"
+ uaKey = "ua"
+)
+
+type IPClaims struct {
+ Ip string
+ Expire time.Duration
+ UA string
+}
+
+func GenerateIpToken(claims IPClaims, secret string) (string, error) {
+ return generateToken(jwt.MapClaims{
+ ipKey: claims.Ip,
+ uaKey: claims.UA,
+ "exp": Now().Add(claims.Expire).Unix(),
+ "issuer": "com.butterfly",
+ "sub": "asign",
+ "aud": "public",
+ }, []byte(secret))
+}
+
+func ParseIpToken(tokenString string, secret string) (pass bool, ip, ua string, err error) {
+ t, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ })
+ var isExpire *bool
+ if vErr, ok := err.(*jwt.ValidationError); ok {
+ e := vErr.Errors&jwt.ValidationErrorExpired > 0
+ isExpire = &e
+ }
+ if err != nil || isExpire == nil {
+ return
+ }
+ var ok bool
+ var jc jwt.MapClaims
+ jc, ok = t.Claims.(jwt.MapClaims)
+ if !ok {
+ err = errors.New("parse token fail")
+ return
+ }
+ ip, ok = jc[ipKey].(string)
+ if !ok {
+ err = errors.New("parse token fail")
+ return
+ }
+ ua, ok = jc[uaKey].(string)
+ if !ok {
+ err = errors.New("parse token fail")
+ return
+ }
+ pass = true
+ return
+}
+
+type UserClaims struct {
+ UserID int64
+ Expire time.Duration
+}
+
+func generateToken(claims jwt.MapClaims, secret []byte) (string, error) {
+ return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
+}
+
+func GenerateUserToken(claims UserClaims, secret string) (string, error) {
+ return generateToken(jwt.MapClaims{
+ userIDKey: claims.UserID,
+ "exp": Now().Add(claims.Expire).Unix(),
+ "issuer": "com.butterfly",
+ "sub": "asign",
+ "aud": "public",
+ }, []byte(secret))
+}
+
+// ParseToken 解析Token
+func ParseUserToken(token string, secret string) (userId int64, newToken string, err error) {
+ t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ })
+ var isExpire *bool
+ if vErr, ok := err.(jwt.ValidationError); ok {
+ e := vErr.Errors&jwt.ValidationErrorExpired > 0
+ isExpire = &e
+ }
+ if err != nil && isExpire == nil {
+ return
+ }
+
+ jc, ok := t.Claims.(jwt.MapClaims)
+ if !ok {
+ err = errors.New("unknown token error1")
+ return
+ }
+
+ id, ok := jc[userIDKey].(float64)
+ if !ok {
+ err = errors.New("unknown token error1")
+ return
+ }
+ userId = int64(id)
+ if isExpire != nil && *isExpire {
+ newToken, err = GenerateUserToken(UserClaims{UserID: userId, Expire: 30 * 24 * time.Hour}, secret)
+ return
+ }
+ err = nil
+ return
+}
+
+// Strips 'Bearer ' prefix from bearer token string
+func StripBearerPrefixFromTokenString(tok string) string {
+ if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " {
+ return tok[7:]
+ } else {
+ return tok
+ }
+}
\ No newline at end of file
diff --git a/pub/common/utils/num.go b/pub/common/utils/num.go
new file mode 100644
index 0000000..8d0c572
--- /dev/null
+++ b/pub/common/utils/num.go
@@ -0,0 +1,20 @@
+package utils
+
+import (
+ "math/rand"
+ "time"
+)
+
+// CountDigits 有几位数字
+func CountDigits(i int) (count int) {
+ for i != 0 {
+ i /= 10
+ count++
+ }
+ return
+}
+
+func RandInt(min int, max int) int {
+ rand.Seed(time.Now().UnixNano())
+ return min + rand.Intn(max-min)
+}
diff --git a/pub/common/utils/page.go b/pub/common/utils/page.go
new file mode 100644
index 0000000..796db6e
--- /dev/null
+++ b/pub/common/utils/page.go
@@ -0,0 +1,19 @@
+package utils
+
+func GetPageRange(totalLen, pageNum, pageSize int) (start, end int) {
+ if pageNum < 1 {
+ pageNum = 1
+ }
+ if pageSize < 1 {
+ pageSize = 10
+ }
+ start = (pageNum - 1) * pageSize
+ if start > totalLen {
+ start = totalLen
+ }
+ end = start + pageSize
+ if end > totalLen {
+ end = totalLen
+ }
+ return
+}
diff --git a/pub/common/utils/path.go b/pub/common/utils/path.go
new file mode 100644
index 0000000..c1cd232
--- /dev/null
+++ b/pub/common/utils/path.go
@@ -0,0 +1,109 @@
+package utils
+
+import (
+ "fmt"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/PuerkitoBio/purell"
+)
+
+// PathSegment slug 优先,空则回退数字 id。
+func PathSegment(slug string, id int64) string {
+ if s := strings.TrimSpace(slug); s != "" {
+ return s
+ }
+ if id > 0 {
+ return strconv.FormatInt(id, 10)
+ }
+ return ""
+}
+
+// PathInfo 路径信息
+type PathInfo struct {
+ Dir string // 目录
+ Ext string // 扩展名
+ Filename string // 没有扩展名的文件名名称
+ FullFileName string // 文件全名
+}
+
+// PathForInfo 解析路径
+// 返回文件,路径,文件名等信息
+func PathForInfo(path string) (pi PathInfo) {
+ if path == "" {
+ return
+ }
+ ext := filepath.Ext(path)
+ dir, name := filepath.Split(path)
+
+ pi.Dir = dir
+ pi.Ext = ext
+ pi.Filename = strings.TrimRight(name, ext)
+ pi.FullFileName = name
+ return
+}
+
+// JoinUrlPath 域名与uri拼接(走 purell 标准化,会剥掉尾部斜杠)。
+// 适用于 CDN 文件链接(m3u8 分段、vtt、静态图等)这类"不需要尾部斜杠"的场景。
+// 需要保留尾部斜杠的页面 URL(sitemap/rss/indexnow)请使用 JoinUrlPathKeepSlash。
+func JoinUrlPath(host string, uri string) (urlpath string) {
+ urlpath = joinRawUrlPath(host, uri)
+ if urlpath == "" {
+ return
+ }
+ // 标准化 URL
+ normalized, err := purell.NormalizeURLString(urlpath, purell.FlagsUsuallySafeGreedy)
+ if err != nil {
+ return urlpath
+ }
+ return normalized
+}
+
+// JoinUrlPathKeepSlash 域名与uri拼接,保留 uri 原始的尾部斜杠,不做任何标准化。
+// sitemap/rss/indexnow 的页面 URL 必须与路由注册的 /archives/:id/ 等完全一致,
+// 否则爬虫每次访问都会被 gin 的 RedirectTrailingSlash 301 到带 / 的地址,
+// 浪费抓取预算并造成重定向链。
+func JoinUrlPathKeepSlash(host string, uri string) string {
+ return joinRawUrlPath(host, uri)
+}
+
+// JoinUrlPathAppendEnd 域名与uri拼接 追加/结尾
+func JoinUrlPathAppendEnd(host string, uri string) (urlpath string) {
+ if uri == "" {
+ return
+ }
+ switch {
+ case strings.HasSuffix(host, "/") && strings.HasPrefix(uri, "/"):
+ urlpath = host[0:len(host)-1] + uri
+ case !strings.HasSuffix(host, "/") && !strings.HasPrefix(uri, "/"):
+ urlpath = host + "/" + uri
+ default:
+ urlpath = host + uri
+ }
+ // 标准化 URL
+ normalized, err := purell.NormalizeURLString(urlpath, purell.FlagsUsuallySafeGreedy)
+ if err != nil {
+ return urlpath
+ }
+ if !strings.HasSuffix(normalized, "/") {
+ normalized = fmt.Sprintf("%s/", normalized)
+ }
+ return normalized
+}
+
+// joinRawUrlPath 纯拼接,不做任何标准化。
+func joinRawUrlPath(host string, uri string) (urlpath string) {
+ if uri == "" {
+ return
+ }
+ switch {
+ case strings.HasSuffix(host, "/") && strings.HasPrefix(uri, "/"):
+ urlpath = host[0:len(host)-1] + uri
+ case !strings.HasSuffix(host, "/") && !strings.HasPrefix(uri, "/"):
+ urlpath = host + "/" + uri
+ default:
+ urlpath = host + uri
+ }
+ return
+}
diff --git a/pub/common/utils/redis.go b/pub/common/utils/redis.go
new file mode 100644
index 0000000..1064e73
--- /dev/null
+++ b/pub/common/utils/redis.go
@@ -0,0 +1,56 @@
+package utils
+
+import (
+ "bytes"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ sErr "videoweb/pub/common/errors"
+ "videoweb/pub/redis"
+)
+
+const (
+ expireTime = 30 * 24 * time.Hour
+)
+
+type TokenRedis struct {
+}
+
+func NewTokenRedis() *TokenRedis {
+ return &TokenRedis{}
+}
+
+func (tr *TokenRedis) Save(userId int64, token string) (err error) {
+ if err = tr.Delete(userId); err != nil {
+ return
+ }
+ err = redis.Set(redisconst.AppUserTokenKey(userId), token, expireTime)
+ if err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+func (tr *TokenRedis) Exists(userId int64, token string) (has bool, err error) {
+ tokenbytes, err := tr.Get(userId)
+ if err != nil {
+ return
+ }
+ has = bytes.Compare(tokenbytes, []byte(token)) == 0
+ return
+}
+
+func (tr *TokenRedis) Get(userId int64) (token []byte, err error) {
+ token, err = redis.GetBytes(redisconst.AppUserTokenKey(userId))
+ if err != nil {
+ err = sErr.MakeWithErr(sErr.RedisGet, err)
+ }
+ return
+}
+
+func (tr *TokenRedis) Delete(userId int64) (err error) {
+ err = redis.Del(redisconst.AppUserTokenKey(userId))
+ if err != nil {
+ err = sErr.MakeWithErr(sErr.RedisDel, err)
+ }
+ return
+}
diff --git a/pub/common/utils/shellcmd.go b/pub/common/utils/shellcmd.go
new file mode 100644
index 0000000..7e51287
--- /dev/null
+++ b/pub/common/utils/shellcmd.go
@@ -0,0 +1,15 @@
+package utils
+
+import (
+ "bytes"
+ "os/exec"
+)
+
+func ShellCmd(cmdStr string) (string, error) {
+ cmd := exec.Command("/bin/bash", "-c", cmdStr)
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ ec := cmd.Run()
+
+ return out.String(), ec
+}
diff --git a/pub/common/utils/slices.go b/pub/common/utils/slices.go
new file mode 100644
index 0000000..aa5d237
--- /dev/null
+++ b/pub/common/utils/slices.go
@@ -0,0 +1,116 @@
+package utils
+
+import (
+ "golang.org/x/exp/constraints"
+ "sort"
+)
+
+func SliceConstruct[T any](fn func() T) T {
+ return fn()
+}
+
+type SliceWalkCallback func(index int) bool // true 继续
+func SliceWalk(length int, cb SliceWalkCallback) {
+ for i := 0; i < length; i++ {
+ if ok := cb(i); ok {
+ continue
+ } else {
+ break
+ }
+ }
+}
+
+type SliceIncludeFilter func(index int) bool // 返回 true 继续, false 中断
+func SliceInclude(length int, filter SliceIncludeFilter) (int, bool) {
+ for i := 0; i < length; i++ {
+ if ok := filter(i); ok {
+ return i, true
+ }
+ }
+ return -1, false
+}
+
+type SliceConstructNFilter[T any] func(index int) (T, bool) // 返回 是否保留,项值
+func SliceConstructN[T any](length int, filter SliceConstructNFilter[T]) []T {
+ ret := make([]T, 0)
+ for i := 0; i < length; i++ {
+ if item, keep := filter(i); keep {
+ ret = append(ret, item)
+ }
+ }
+ return ret
+}
+
+// SliceUnique 使用泛型实现切片去重
+func SliceUnique[T comparable](s []T) []T {
+ seen := make(map[T]bool)
+ j := 0
+ for _, v := range s {
+ if _, ok := seen[v]; !ok {
+ seen[v] = true
+ s[j] = v
+ j++
+ }
+ }
+ return s[:j]
+}
+
+// SlicesDifference 求两个集合的差集
+func SlicesDifference[T comparable](a, b []T) []T {
+ // 使用 map 存储集合 b 的元素,方便快速判断元素是否存在
+ seen := make(map[T]struct{}, len(b))
+ for _, v := range b {
+ seen[v] = struct{}{}
+ }
+
+ // 遍历集合 a,将不在集合 b 中的元素放入新的切片
+ diff := make([]T, 0)
+ for _, v := range a {
+ if _, ok := seen[v]; !ok {
+ diff = append(diff, v)
+ }
+ }
+
+ return diff
+}
+
+func RemoveSlicesElement[T comparable](slice []T, element T) []T {
+ // 使用两个指针,一个遍历,一个用来记录有效元素的位置
+ j := 0
+ for i := range slice {
+ if slice[i] != element {
+ slice[j] = slice[i]
+ j++
+ }
+ }
+ // 返回截取后的切片
+ return slice[:j]
+}
+
+func SlicesIntersect[T constraints.Ordered | ~string](slice1, slice2 []T) []T {
+ dict := make(map[T]int)
+ ret := make([]T, 0)
+ for _, v1 := range slice1 {
+ dict[v1]++
+ }
+ for _, v2 := range slice2 {
+ if _, exists := dict[v2]; exists {
+ ret = append(ret, v2)
+ }
+ }
+ return ret
+}
+
+func IsSliceEqual[T constraints.Ordered](src []T, dst []T) bool {
+ sort.Slice(src, func(i, j int) bool { return src[i] < src[j] })
+ sort.Slice(dst, func(i, j int) bool { return dst[i] < dst[j] })
+ if len(src) != len(dst) {
+ return false
+ }
+ for i := range src {
+ if src[i] != dst[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/pub/common/utils/slug_source.go b/pub/common/utils/slug_source.go
new file mode 100644
index 0000000..0668024
--- /dev/null
+++ b/pub/common/utils/slug_source.go
@@ -0,0 +1,270 @@
+package utils
+
+import (
+ "regexp"
+ "strings"
+ "unicode"
+)
+
+const slugTitleRuneLimit = 10
+
+var (
+ reSlugFC2Code = regexp.MustCompile(`(?i)fc2[-_ ]?ppv[-_ ]?\d{5,12}`)
+ reSlugNumLetterCode = regexp.MustCompile(`(?i)\d{3}[a-z]{2,6}[-_ ]?\d{2,7}`)
+ reSlugLetterNumCode = regexp.MustCompile(`(?i)[a-z]{2,8}[-_ ]?\d{2,7}`)
+ reSlugLeadingYear = regexp.MustCompile(`^20\d{2}`)
+ reSlugLeadingBracket = regexp.MustCompile(`^[【\[]([^】\]]+)[】\]]|^[((]([^))]+)[))]|^「([^」]+)」`)
+)
+
+// 标题开头常见的无意义前缀,长词优先。
+var slugTitlePrefixNoise = []string{
+ "中文字幕", "中文翻譯", "中文翻译", "无码流出", "無碼流出",
+ "无码破解", "無碼破解", "无修正", "無修正", "完整版", "抢先版",
+ "高清版", "字幕版",
+}
+
+// 技术规格、版本标记,单独出现在标题开头时去掉。
+var slugTitlePrefixTokens = []string{
+ "1080P", "2160P", "720P", "360P", "FHD", "UHD", "4K", "8K", "HD",
+ "FANZA", "MGS", "最新", "高清", "流出", "破解", "字幕", "中字",
+}
+
+// 关键词里过于通用、几乎每条都有的词,不单独拿来做 slug。
+var slugGenericKeywords = map[string]struct{}{
+ "中文字幕": {}, "中文翻译": {}, "中文翻譯": {}, "无码流出": {}, "無碼流出": {},
+ "无码": {}, "無碼": {}, "有码": {}, "有碼": {}, "无修正": {}, "無修正": {},
+ "日本": {}, "国产": {}, "國產": {}, "素人": {}, "人妻": {}, "熟女": {},
+ "中文": {}, "字幕": {}, "高清": {}, "最新": {}, "流出": {}, "破解": {},
+ "hd": {}, "4k": {}, "fhd": {}, "fanza": {},
+}
+
+// SlugSourceFromKeywordsOrTitle 从标题和关键词提取适合转拼音的 slug 源文本。
+// 优先用标题里的番号或去掉无意义前缀后的有效片段,不再固定取第一个关键词。
+func SlugSourceFromKeywordsOrTitle(keywords []string, title string) string {
+ title = strings.TrimSpace(title)
+ if code := firstContentCode(title, keywords); code != "" {
+ return code
+ }
+ if slice := meaningfulTitleSlice(cleanTitleForSlug(title)); slice != "" {
+ return slice
+ }
+ if kw := pickDistinctKeyword(keywords); kw != "" {
+ return kw
+ }
+ if title != "" {
+ return GetPrefix(title, slugTitleRuneLimit)
+ }
+ return firstNonEmptyKeyword(keywords)
+}
+
+func firstContentCode(title string, keywords []string) string {
+ if code := extractContentCode(title); code != "" {
+ return code
+ }
+ for _, kw := range keywords {
+ if code := extractContentCode(kw); code != "" {
+ return code
+ }
+ }
+ return ""
+}
+
+func extractContentCode(s string) string {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return ""
+ }
+ if m := reSlugFC2Code.FindString(s); m != "" {
+ return m
+ }
+ if m := reSlugNumLetterCode.FindString(s); m != "" {
+ return m
+ }
+ if m := reSlugLetterNumCode.FindString(s); m != "" && !isQualityToken(m) {
+ return m
+ }
+ return ""
+}
+
+func isQualityToken(s string) bool {
+ s = strings.ToUpper(strings.TrimSpace(s))
+ s = strings.ReplaceAll(s, " ", "")
+ switch s {
+ case "1080P", "2160P", "720P", "360P", "4K", "8K", "HD", "FHD", "UHD":
+ return true
+ default:
+ return false
+ }
+}
+
+func cleanTitleForSlug(title string) string {
+ s := strings.TrimSpace(title)
+ for i := 0; i < 8 && s != ""; i++ {
+ next := stripOnceTitlePrefix(s)
+ if next == s {
+ break
+ }
+ s = next
+ }
+ return strings.TrimSpace(s)
+}
+
+func stripOnceTitlePrefix(s string) string {
+ s = strings.TrimLeftFunc(s, func(r rune) bool {
+ return unicode.IsSpace(r) || strings.ContainsRune(`-_~|/.,;::、·•*#@`, r)
+ })
+ if s == "" {
+ return ""
+ }
+ if loc := reSlugLeadingBracket.FindStringSubmatch(s); len(loc) > 0 {
+ inner := firstSubmatch(loc)
+ whole := loc[0]
+ if extractContentCode(inner) != "" {
+ return strings.TrimSpace(inner + s[len(whole):])
+ }
+ if isNoiseText(inner) || runeLen(inner) <= 4 {
+ return strings.TrimSpace(s[len(whole):])
+ }
+ }
+ lower := strings.ToLower(s)
+ for _, prefix := range slugTitlePrefixNoise {
+ if strings.HasPrefix(s, prefix) || strings.HasPrefix(lower, strings.ToLower(prefix)) {
+ return strings.TrimSpace(s[len([]byte(prefixMatch(s, prefix))):])
+ }
+ }
+ for _, token := range slugTitlePrefixTokens {
+ if consumed, ok := consumeLeadingToken(s, token); ok {
+ return consumed
+ }
+ }
+ if loc := reSlugLeadingYear.FindStringIndex(s); loc != nil && loc[0] == 0 {
+ rest := strings.TrimSpace(s[loc[1]:])
+ if rest != "" {
+ return rest
+ }
+ }
+ return s
+}
+
+func prefixMatch(s, prefix string) string {
+ if strings.HasPrefix(s, prefix) {
+ return prefix
+ }
+ lower := strings.ToLower(s)
+ p := strings.ToLower(prefix)
+ if strings.HasPrefix(lower, p) {
+ return s[:len(prefix)]
+ }
+ return prefix
+}
+
+func consumeLeadingToken(s, token string) (string, bool) {
+ if len(s) < len(token) {
+ return s, false
+ }
+ if !strings.HasPrefix(strings.ToLower(s), strings.ToLower(token)) {
+ return s, false
+ }
+ rest := s[len(token):]
+ if rest == "" {
+ return "", true
+ }
+ r := []rune(rest)[0]
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
+ return s, false
+ }
+ return strings.TrimSpace(rest), true
+}
+
+func firstSubmatch(m []string) string {
+ for i := 1; i < len(m); i++ {
+ if strings.TrimSpace(m[i]) != "" {
+ return strings.TrimSpace(m[i])
+ }
+ }
+ return ""
+}
+
+func meaningfulTitleSlice(title string) string {
+ title = strings.TrimSpace(title)
+ if title == "" {
+ return ""
+ }
+ return GetPrefix(title, slugTitleRuneLimit)
+}
+
+func pickDistinctKeyword(keywords []string) string {
+ best := ""
+ bestScore := 0
+ for _, kw := range keywords {
+ kw = strings.TrimSpace(kw)
+ if kw == "" || isGenericKeyword(kw) {
+ continue
+ }
+ score := runeLen(kw)
+ if score < 3 {
+ continue
+ }
+ if score > bestScore {
+ best = kw
+ bestScore = score
+ }
+ }
+ return best
+}
+
+func firstNonEmptyKeyword(keywords []string) string {
+ for _, kw := range keywords {
+ if s := strings.TrimSpace(kw); s != "" {
+ return s
+ }
+ }
+ return ""
+}
+
+func isGenericKeyword(s string) bool {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return true
+ }
+ return isGenericKeywordExact(s) || isNoiseText(s)
+}
+
+func isNoiseText(s string) bool {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return true
+ }
+ if isGenericKeywordExact(s) || isQualityToken(s) {
+ return true
+ }
+ for _, prefix := range slugTitlePrefixNoise {
+ if s == prefix {
+ return true
+ }
+ }
+ parts := strings.FieldsFunc(s, func(r rune) bool {
+ return strings.ContainsRune(` /|、,,-_+`, r)
+ })
+ if len(parts) > 1 {
+ for _, p := range parts {
+ if !isNoiseText(p) {
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+
+func isGenericKeywordExact(s string) bool {
+ if _, ok := slugGenericKeywords[s]; ok {
+ return true
+ }
+ _, ok := slugGenericKeywords[strings.ToLower(s)]
+ return ok
+}
+
+func runeLen(s string) int {
+ return len([]rune(s))
+}
diff --git a/pub/common/utils/strings.go b/pub/common/utils/strings.go
new file mode 100644
index 0000000..eb50807
--- /dev/null
+++ b/pub/common/utils/strings.go
@@ -0,0 +1,100 @@
+package utils
+
+import (
+ "math/rand/v2"
+ "strings"
+
+ "github.com/gosimple/slug"
+)
+
+// BinarySearch BinarySearch
+func BinarySearch(s []string, t string, n int) (idx int) {
+ idx = -1
+ if n == 0 {
+ return
+ }
+ start, end := 0, n-1
+ for start <= end {
+ mid := (start + end) >> 1
+ if s[mid] < t {
+ start = mid + 1
+ } else if s[mid] > t {
+ end = mid - 1
+ } else {
+ idx = mid
+ break
+ }
+ }
+ return
+}
+
+var YesOrNo = map[bool]string{
+ true: "是",
+ false: "否",
+}
+
+func GetRandomLetters(n int) string {
+ const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letters[rand.IntN(len(letters))]
+ }
+ return string(b)
+}
+
+func GetPrefix(s string, n int) string {
+ runes := []rune(s)
+ if len(runes) <= n {
+ return s
+ }
+ return string(runes[:n])
+}
+
+func GetSlugInfo(text string) string {
+ return SanitizeSlug(slug.Make(text))
+}
+
+// SanitizeSlug 去掉 slug 中的横杠等特殊符号,只保留字母和数字
+func SanitizeSlug(s string) string {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return ""
+ }
+ var b strings.Builder
+ b.Grow(len(s))
+ for _, r := range s {
+ if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
+ b.WriteRune(r)
+ }
+ }
+ return b.String()
+}
+
+// IsSlugAllDigits 判断 slug 是否全为数字
+func IsSlugAllDigits(s string) bool {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return false
+ }
+ for _, r := range s {
+ if r < '0' || r > '9' {
+ return false
+ }
+ }
+ return true
+}
+
+// MakeUniqueSlug 生成唯一 slug;exists 返回 true 表示已被占用。生成结果不能全是数字。
+func MakeUniqueSlug(source string, exists func(string) bool) string {
+ pinyin := GetSlugInfo(source)
+ if pinyin == "" {
+ pinyin = GetRandomLetters(5)
+ } else if IsSlugAllDigits(pinyin) {
+ pinyin = pinyin + GetRandomLetters(3)
+ }
+ finalSlug := pinyin
+ for exists(finalSlug) {
+ finalSlug = pinyin + GetRandomLetters(3)
+ }
+ return finalSlug
+}
diff --git a/pub/common/utils/structToMap.go b/pub/common/utils/structToMap.go
new file mode 100644
index 0000000..f16d5ce
--- /dev/null
+++ b/pub/common/utils/structToMap.go
@@ -0,0 +1,52 @@
+package utils
+
+import (
+ "fmt"
+ "github.com/mitchellh/mapstructure"
+ "reflect"
+)
+
+func Struct2Map(obj interface{}) map[string]interface{} {
+ t := reflect.TypeOf(obj)
+ v := reflect.ValueOf(obj)
+ if t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ v = reflect.Indirect(v)
+ }
+ var data = make(map[string]interface{})
+ //func (v Value) NumField() int, 返回v持有的结构体类型值的字段数,如果v的Kind不是Struct会panic
+ for i := 0; i < t.NumField(); i++ {
+ n := t.Field(i).Name
+ vv := v.Field(i).Interface()
+ data[Capitalize(n)] = vv
+ }
+ return data
+}
+
+func Map2Struct(obj interface{}, mapInfo map[string]interface{}) interface{} {
+ err := mapstructure.Decode(mapInfo, obj)
+ if err != nil {
+ fmt.Println(err)
+ }
+ return obj
+}
+
+// Capitalize 字符首字母大写
+func Capitalize(str string) string {
+ var upperStr string
+ vv := []rune(str) // 后文有介绍
+ for i := 0; i < len(vv); i++ {
+ if i == 0 {
+ if vv[i] >= 65 && vv[i] <= 96 { // 后文有介绍
+ vv[i] += 32 // string的码表相差32位
+ upperStr += string(vv[i])
+ } else {
+ fmt.Println("Not begins with uppercase letter,")
+ return str
+ }
+ } else {
+ upperStr += string(vv[i])
+ }
+ }
+ return upperStr
+}
diff --git a/pub/common/utils/time.go b/pub/common/utils/time.go
new file mode 100644
index 0000000..27e9f2d
--- /dev/null
+++ b/pub/common/utils/time.go
@@ -0,0 +1,303 @@
+package utils
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+var (
+ LocBJ = mustBeijingLocation()
+)
+
+func mustBeijingLocation() *time.Location {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil || loc == nil {
+ return time.FixedZone("CST", 8*3600)
+ }
+ return loc
+}
+
+func init() {
+ time.Local = LocBJ
+}
+
+func Now() time.Time {
+ return time.Now().In(LocBJ)
+}
+
+func NowMillisecond() int64 {
+ return Now().UnixNano() / int64(time.Millisecond)
+}
+
+func BeginOfToday() time.Time {
+ t := Now()
+ return BeginOfTime(t)
+}
+
+func BeginOfTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func GetNextMonthTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month+1, day, 0, 0, 0, 0, t.Location())
+}
+func GetNextQuarterTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month+3, day, 0, 0, 0, 0, t.Location())
+}
+
+func GetNextYearTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year+1, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func GetForeverTime() time.Time {
+ t := time.Now()
+ year, month, day := t.Date()
+ return time.Date(year+999, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func IsoWeek() string {
+ year, week := time.Now().ISOWeek()
+ return fmt.Sprintf("%d%02d", year, week)
+}
+
+// Deprecated
+func EndOfToday() time.Time {
+ t := Now()
+ return EndOfTime(t)
+}
+
+func EndOfTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 23, 59, 59, 0, t.Location())
+}
+
+func GetDateString(t time.Time) string {
+ return t.In(LocBJ).Format(pubdef.DateISO)
+}
+
+func GetTodayDate(now time.Time) string {
+ return GetDateString(now.In(time.Local))
+}
+
+func GetYesterdayDate(now time.Time) string {
+ return GetDateString(now.Local().AddDate(0, 0, -1))
+}
+
+func GetDayBeforeYesterdayDate(now time.Time) string {
+ return GetDateString(now.Local().AddDate(0, 0, -2))
+}
+
+func GetDayRange(t time.Time) (dayStart time.Time, dayEnd time.Time) {
+ dayStart = BeginOfTime(t)
+ dayEnd = EndOfTime(t)
+ return
+}
+
+// GetDayRangeByDate 返回一天的开始和结束时间, 00:00:00 - 23:59:59
+func GetDayRangeByDate(date string) (dayStart time.Time, dayEnd time.Time, err error) {
+ var d time.Time
+ d, err = time.ParseInLocation(pubdef.DateISO, date, LocBJ)
+ if err != nil {
+ return
+ }
+ dayStart, dayEnd = GetDayRange(d)
+ return
+}
+
+func GetDayEnd(date string) time.Time {
+ d, _ := time.ParseInLocation(pubdef.DateISO, date, LocBJ)
+ return EndOfTime(d)
+}
+
+func GetDayBegin(date string) time.Time {
+ d, _ := time.ParseInLocation(pubdef.DateISO, date, LocBJ)
+ return BeginOfTime(d)
+}
+
+func GetDayEndByTime(date string) time.Time {
+ d, _ := time.ParseInLocation(pubdef.TimeISO, date, LocBJ)
+ return EndOfTime(d)
+}
+
+func GetDayBeginByTime(date string) time.Time {
+ d, _ := time.ParseInLocation(pubdef.TimeISO, date, LocBJ)
+ return BeginOfTime(d)
+}
+
+func BeginWithThisMonth() time.Time {
+ t := Now()
+ year, month, _ := t.Date()
+ return time.Date(year, month, 1, 0, 0, 0, 0, t.Location())
+}
+
+func BeginOfMonth(t time.Time) time.Time {
+ year, month, _ := t.In(LocBJ).Date()
+ return time.Date(year, month, 1, 0, 0, 0, 0, LocBJ)
+}
+
+func BeginOfYear(t time.Time) time.Time {
+ year, _, _ := t.In(LocBJ).Date()
+ return time.Date(year, 1, 1, 0, 0, 0, 0, LocBJ)
+}
+
+func EndOfMonth(t time.Time) time.Time {
+ return BeginOfMonth(t).AddDate(0, 1, 0).Add(-time.Nanosecond)
+}
+
+func EndOfLastMonth(t time.Time) time.Time {
+ return BeginOfMonth(t).Add(-time.Nanosecond)
+}
+
+func EndOfLastMonthDate(t time.Time) string {
+ return GetDateString(EndOfLastMonth(t))
+}
+
+func EndOfLastWeek(t time.Time) time.Time {
+ return BeginOfWeekByTime(t).Add(-time.Nanosecond)
+}
+
+func EndOfLastWeekDate(t time.Time) string {
+ return GetDateString(EndOfLastWeek(t))
+}
+
+// Deprecated
+func BeginOfWeek() time.Time {
+ return BeginOfWeekByTime(Now())
+}
+
+func WeekDateRange(t time.Time) (string, string) {
+ b := BeginOfWeekByTime(t)
+ e := EndOfWeekByTime(t)
+ return GetDateString(b), GetDateString(e)
+}
+
+func MonthDateRange(t time.Time) (string, string) {
+ b := BeginOfMonth(t)
+ e := EndOfMonth(t)
+ return GetDateString(b), GetDateString(e)
+}
+
+func BeginOfWeekByTime(t time.Time) time.Time {
+ t = t.In(LocBJ)
+ bt := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, LocBJ)
+ weekday := int(t.Weekday())
+ weekStartDayInt := int(time.Monday)
+ if weekday < weekStartDayInt {
+ weekday = weekday + 7 - weekStartDayInt
+ } else {
+ weekday = weekday - weekStartDayInt
+ }
+ return bt.AddDate(0, 0, -weekday)
+}
+
+func EndOfWeekByTime(t time.Time) time.Time {
+ return BeginOfWeekByTime(t).AddDate(0, 0, 7).Add(-time.Nanosecond)
+}
+
+func BeginTodayMillsecond() int64 {
+ return BeginOfToday().UnixNano() / int64(time.Millisecond)
+}
+
+// ServerCurrMillisecond 获得时间毫秒数 13位
+func ServerCurrMillisecond(t time.Time) int64 {
+ return t.UnixNano() / 1000000
+}
+
+func BeginWithTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func EndWithTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 23, 59, 59, 0, t.Location())
+}
+
+func IsToday(t time.Time) bool {
+ if t.IsZero() {
+ return false
+ }
+ today := Now()
+
+ return t.Year() == today.Year() &&
+ t.Month() == today.Month() &&
+ t.Day() == today.Day()
+}
+
+func IsSameYearAndMonth(t1, t2 time.Time) bool {
+ return t1.Year() == t2.Year() &&
+ t1.Month() == t2.Month()
+}
+
+// 获取月的第一天
+func GetFirstDateOfMonth(d time.Time) time.Time {
+ d = d.AddDate(0, 0, -d.Day()+1)
+ return GetZeroTime(d)
+}
+
+// 获取月的最后一天
+func GetLastDateOfMonth(d time.Time) time.Time {
+ return GetFirstDateOfMonth(d).AddDate(0, 1, -1)
+}
+
+// 获取周的星期一(星期一~星期日)
+func GetMondayOfWeek(d time.Time) time.Time {
+ offset := (int(d.Weekday()) + 6) % 7
+ d = d.AddDate(0, 0, -offset)
+ return GetZeroTime(d)
+}
+
+// 获取周的星期日(星期一~星期日)
+func GetSundayOfWeek(d time.Time) time.Time {
+ offset := 6 - (int(d.Weekday())+6)%7
+ d = d.AddDate(0, 0, offset)
+ return GetZeroTime(d)
+}
+
+// 获取某一天的0点时间
+func GetZeroTime(d time.Time) time.Time {
+ return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location())
+}
+
+// 获取当前整点时间
+func GetWholeTime(d time.Time) time.Time {
+ return time.Date(d.Year(), d.Month(), d.Day(), d.Hour(), 0, 0, 0, d.Location())
+}
+
+func MustParseTime(s string) time.Time {
+ layouts := []string{
+ time.RFC3339,
+ pubdef.DateISO,
+ pubdef.TimeISO,
+ }
+ for _, layout := range layouts {
+ t, err := time.ParseInLocation(layout, s, time.Local)
+ if err != nil {
+ continue
+ }
+ return t
+ }
+ panic(fmt.Errorf("cannot parse %s as time.Time", s))
+}
+
+// 查看两个时间差了多少天,差一秒都算一天
+func TimeSub(t1, t2 time.Time) int64 {
+ t1 = time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, time.Local)
+ t2 = time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, time.Local)
+
+ return int64(t1.Sub(t2).Hours() / 24)
+}
+func RecentMinute(tim time.Time, scaleMinute int64) time.Time {
+ minute := int64(tim.Minute())
+ lave := minute % scaleMinute
+ formatMinute := minute - lave
+ return time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), int(formatMinute), 0, 0, tim.Location())
+}
+func GetTimeString(t time.Time) string {
+ return t.In(LocBJ).Format(pubdef.TimeISO)
+}
diff --git a/pub/common/utils/time_test.go b/pub/common/utils/time_test.go
new file mode 100644
index 0000000..14bbd78
--- /dev/null
+++ b/pub/common/utils/time_test.go
@@ -0,0 +1,169 @@
+package utils
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func Test_GetDayRangeByDate(t *testing.T) {
+ begin, end, _ := GetDayRangeByDate("2022-04-08")
+ fmt.Println(begin, end)
+}
+
+func Test_GetDayRange(t *testing.T) {
+ now := time.Now()
+ fmt.Println(GetDayRange(now))
+}
+
+func Test_GetDateString(t *testing.T) {
+ fmt.Println(GetDateString(time.Now()))
+}
+
+func Test_GetTodayDate(t *testing.T) {
+ fmt.Println(GetTodayDate(time.Now()))
+}
+
+func Test_GetYesterdayDate(t *testing.T) {
+ fmt.Println(GetYesterdayDate(time.Now()))
+}
+
+func Test_GetDayBeforeYesterdayDate(t *testing.T) {
+ fmt.Println(GetDayBeforeYesterdayDate(time.Now()))
+}
+
+func Test_BeginOfYear(t *testing.T) {
+ got := BeginOfYear(time.Date(2026, 9, 12, 15, 4, 5, 0, LocBJ))
+ want := time.Date(2026, 1, 1, 0, 0, 0, 0, LocBJ)
+ if !got.Equal(want) {
+ t.Fatalf("BeginOfYear = %v, want %v", got, want)
+ }
+
+ // UTC 2025-12-31 16:30 已是北京时间 2026-01-01 00:30,应按 2026 年切。
+ utc := time.Date(2025, 12, 31, 16, 30, 0, 0, time.UTC)
+ if got = BeginOfYear(utc); !got.Equal(want) {
+ t.Fatalf("BeginOfYear(UTC year boundary) = %v, want %v", got, want)
+ }
+}
+
+func Test_BeginOfMonthBeijing(t *testing.T) {
+ // UTC 2026-08-31 16:30 已是北京时间 2026-09-01 00:30,应按 9 月切。
+ utc := time.Date(2026, 8, 31, 16, 30, 0, 0, time.UTC)
+ want := time.Date(2026, 9, 1, 0, 0, 0, 0, LocBJ)
+ if got := BeginOfMonth(utc); !got.Equal(want) {
+ t.Fatalf("BeginOfMonth(UTC month boundary) = %v, want %v", got, want)
+ }
+}
+
+func Test_BeginOfWeekByTime(t *testing.T) {
+ {
+ // 如果当前是周日,获取到的结果应该是这周周一而不是下周周一
+ now := time.Date(2022, 4, 3, 1, 0, 0, 0, time.Local)
+ actual := BeginOfWeekByTime(now)
+ expect := time.Date(2022, 3, 28, 0, 0, 0, 0, time.Local)
+ if !reflect.DeepEqual(expect, actual) {
+ t.Fatalf("fail")
+ }
+ }
+ {
+ now := time.Date(2022, 3, 28, 0, 0, 0, 0, time.Local)
+ actual := BeginOfWeekByTime(now)
+ expect := time.Date(2022, 3, 28, 0, 0, 0, 0, time.Local)
+ if !reflect.DeepEqual(expect, actual) {
+ t.Fatalf("fail")
+ }
+ }
+ {
+ // UTC 2026-09-06 16:30 已是北京时间周一 00:30,应按本周一切。
+ utc := time.Date(2026, 9, 6, 16, 30, 0, 0, time.UTC)
+ want := time.Date(2026, 9, 7, 0, 0, 0, 0, LocBJ)
+ if got := BeginOfWeekByTime(utc); !got.Equal(want) {
+ t.Fatalf("BeginOfWeekByTime(UTC week boundary) = %v, want %v", got, want)
+ }
+ }
+ {
+ // UTC 2026-09-06 15:30 仍是北京时间周日 23:30,应落在上一周周一。
+ utc := time.Date(2026, 9, 6, 15, 30, 0, 0, time.UTC)
+ want := time.Date(2026, 8, 31, 0, 0, 0, 0, LocBJ)
+ if got := BeginOfWeekByTime(utc); !got.Equal(want) {
+ t.Fatalf("BeginOfWeekByTime(UTC sunday) = %v, want %v", got, want)
+ }
+ }
+}
+
+func Test_GetDayEnd(t *testing.T) {
+ fmt.Println(GetDayEnd("2020-04-09"))
+}
+
+func Test_GetDayBegin(t *testing.T) {
+ fmt.Println(GetDayBegin("2020-04-09"))
+}
+
+func Test_EndOfWeekByTime(t *testing.T) {
+ now := time.Now()
+ fmt.Println(EndOfWeekByTime(now))
+}
+
+func Test_EndOfMonth(t *testing.T) {
+ now := time.Now()
+ fmt.Println(BeginOfMonth(now))
+ fmt.Println(EndOfMonth(now))
+}
+
+func Test_WeekDateRange(t *testing.T) {
+ fmt.Println(WeekDateRange(time.Now()))
+}
+
+func Test_MonthDateRange(t *testing.T) {
+ fmt.Println(MonthDateRange(time.Now()))
+}
+
+func Test_EndOfLastWeek(t *testing.T) {
+ fmt.Println(EndOfLastWeek(time.Now()))
+}
+
+func Test_EndOfLastMonth(t *testing.T) {
+ fmt.Println(EndOfLastMonth(time.Now()))
+}
+
+func Test_EndOfLastWeekDate(t *testing.T) {
+ fmt.Println(EndOfLastWeekDate(time.Now()))
+}
+
+func Test_EndOfLastMonthDate(t *testing.T) {
+ fmt.Println(EndOfLastMonthDate(time.Now()))
+}
+
+func TestMustParseTime(t *testing.T) {
+ type args struct {
+ s string
+ }
+ tests := []struct {
+ name string
+ args args
+ want time.Time
+ }{
+ {
+ name: "case1",
+ args: args{
+ s: "2022-04-25",
+ },
+ want: time.Date(2022, 4, 25, 0, 0, 0, 0, time.Local),
+ },
+ {
+ name: "case2",
+ args: args{
+ s: "2022-04-25 11:12:13",
+ },
+ want: time.Date(2022, 4, 25, 11, 12, 13, 0, time.Local),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := MustParseTime(tt.args.s); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("MustParseTime() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pub/common/utils/uasplit.go b/pub/common/utils/uasplit.go
new file mode 100644
index 0000000..3d033eb
--- /dev/null
+++ b/pub/common/utils/uasplit.go
@@ -0,0 +1,30 @@
+package utils
+
+import (
+ "strings"
+ "videoweb/pub/common/enum/consts"
+)
+
+func UASplit(useragent string) (phoneModel string, phoneSys string, appVer string) {
+ if useragent == "" {
+ return
+ }
+ infos := strings.Split(useragent, ";")
+ if len(infos) != 3 {
+ return
+ }
+ phoneinfos := strings.Split(strings.Trim(infos[1], " "), " ")
+ if len(phoneinfos) == 2 {
+ phoneModel = strings.Trim(phoneinfos[0], " ")
+ phoneSys = strings.Trim(phoneinfos[1], " ")
+ }
+ appVer = strings.Trim(infos[2], " ")
+ return
+}
+
+func UADeviceType(ua string) string {
+ if strings.Contains(ua, "iPhone") {
+ return consts.IOS
+ }
+ return consts.Android
+}
diff --git a/pub/common/utils/utils.go b/pub/common/utils/utils.go
new file mode 100644
index 0000000..8c64d51
--- /dev/null
+++ b/pub/common/utils/utils.go
@@ -0,0 +1,63 @@
+package utils
+
+import (
+ "fmt"
+ "math/rand"
+ "runtime/debug"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+
+ "golang.org/x/exp/constraints"
+)
+
+func RecoverError(onError func(err error)) {
+ if r := recover(); r != nil {
+ err := fmt.Errorf("internal error: %v", r)
+ if onError != nil {
+ onError(err)
+ }
+ log.Error(fmt.Sprintf("[Panic] RecoverError err: %+v\n", r) + string(debug.Stack()))
+ }
+}
+
+func GetRandom(min, max int) int64 {
+ rand.Seed(time.Now().UnixNano())
+ return int64(rand.Intn(max-min+1) + min)
+}
+func EnsureRange[T constraints.Ordered](value, min, max T) T {
+ if value < min {
+ value = min
+ }
+ if value > max {
+ value = max
+ }
+ return value
+}
+
+func InRange[T constraints.Ordered](value, min, max T) bool {
+ if value < min {
+ return false
+ }
+ if value > max {
+ return false
+ }
+ return true
+}
+
+// Shuffle 使用 Fisher-Yates 算法对切片进行随机排序
+func Shuffle[T any](slice []T) {
+ rand.Seed(time.Now().UnixNano())
+ n := len(slice)
+ for i := n - 1; i > 0; i-- {
+ j := rand.Intn(i + 1)
+ slice[i], slice[j] = slice[j], slice[i]
+ }
+}
+func GetRandomMacAddress() string {
+ buf := make([]byte, 6)
+ rand.Read(buf)
+ buf[0] |= 2
+ mac := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5])
+ return strings.ToUpper(mac)
+}
diff --git a/pub/common/utils/verify.go b/pub/common/utils/verify.go
new file mode 100644
index 0000000..fb20ae5
--- /dev/null
+++ b/pub/common/utils/verify.go
@@ -0,0 +1,10 @@
+package utils
+
+import "regexp"
+
+// mobile verify
+func VerifyMobileFormat(mobileNum string) bool {
+ regular := "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"
+ reg := regexp.MustCompile(regular)
+ return reg.MatchString(mobileNum)
+}
diff --git a/pub/common/utils/version.go b/pub/common/utils/version.go
new file mode 100644
index 0000000..e2769d8
--- /dev/null
+++ b/pub/common/utils/version.go
@@ -0,0 +1,43 @@
+package utils
+
+import (
+ "strconv"
+ "strings"
+)
+
+func getSubVersions(ver string) []int {
+ var vers [4]int
+ vv := strings.Split(ver, ".")
+ for i, v := range vv {
+ vers[i], _ = strconv.Atoi(v)
+ }
+ return vers[:]
+}
+
+// VersionCompare
+func VersionCompare(a, b string) int {
+
+ aa := getSubVersions(a)
+ bb := getSubVersions(b)
+
+ for i := 0; i < len(aa) && i < len(bb); i++ {
+
+ if aa[i] == bb[i] {
+ continue
+ }
+
+ if aa[i] < bb[i] {
+ return -1
+ }
+
+ return 1
+ }
+
+ if len(aa) == len(bb) {
+ return 0
+ }
+ if len(aa) < len(bb) {
+ return -1
+ }
+ return 1
+}
diff --git a/pub/common/utils/version_test.go b/pub/common/utils/version_test.go
new file mode 100644
index 0000000..b1adcd7
--- /dev/null
+++ b/pub/common/utils/version_test.go
@@ -0,0 +1,19 @@
+package utils
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestVersionCompare(t *testing.T) {
+
+ fmt.Printf("2.10.9 vs 2.11 = %d\n", VersionCompare("2.10.9", "2.11"))
+ fmt.Printf("2.11.9 vs 2.11 = %d\n", VersionCompare("2.11.9", "2.11"))
+ fmt.Printf("1.10.9 vs 2.11 = %d\n", VersionCompare("1.10.9", "2.11"))
+ fmt.Printf("2.10.9 vs 2.10.1 = %d\n", VersionCompare("2.10.9", "2.11.1"))
+ fmt.Printf("2.10.9 vs 2.10.10 = %d\n", VersionCompare("2.10.9", "2.10.10"))
+ fmt.Printf("2.10.9 vs 2.10.9.0 = %d\n", VersionCompare("2.10.9", "2.10.9.0"))
+ fmt.Printf("1 vs 1.0.0.0 = %d\n", VersionCompare("2.10.9", "2.10.9.0"))
+ fmt.Printf("2.10.9 vs 2.10.9.-1 = %d\n", VersionCompare("2.10.9", "2.10.9.-1"))
+
+}
diff --git a/pub/common/v10/upgradev10.go b/pub/common/v10/upgradev10.go
new file mode 100644
index 0000000..8908c46
--- /dev/null
+++ b/pub/common/v10/upgradev10.go
@@ -0,0 +1,55 @@
+package v10
+
+import (
+ "github.com/go-playground/validator/v10"
+ "reflect"
+ "sync"
+
+ "github.com/gin-gonic/gin/binding"
+)
+
+type DefaultValidator struct {
+ once sync.Once
+ validate *validator.Validate
+}
+
+var _ binding.StructValidator = &DefaultValidator{}
+
+func (v *DefaultValidator) ValidateStruct(obj interface{}) error {
+
+ if kindOfData(obj) == reflect.Struct {
+
+ v.lazyinit()
+
+ if err := v.validate.Struct(obj); err != nil {
+ return error(err)
+ }
+ }
+
+ return nil
+}
+
+func (v *DefaultValidator) Engine() interface{} {
+ v.lazyinit()
+ return v.validate
+}
+
+func (v *DefaultValidator) lazyinit() {
+ v.once.Do(func() {
+ v.validate = validator.New()
+ v.validate.SetTagName("binding")
+
+ // add any custom validations etc. here
+ })
+}
+
+func kindOfData(data interface{}) reflect.Kind {
+
+ value := reflect.ValueOf(data)
+ valueType := value.Kind()
+
+ if valueType == reflect.Ptr {
+ valueType = value.Elem().Kind()
+ }
+ return valueType
+}
diff --git a/pub/common/v10/validate.go b/pub/common/v10/validate.go
new file mode 100644
index 0000000..ef5ca53
--- /dev/null
+++ b/pub/common/v10/validate.go
@@ -0,0 +1,171 @@
+package v10
+
+import (
+ "github.com/nyaruka/phonenumbers"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// IsPureChar 匹配中文、字母、数字
+func IsPureChar(src string) bool {
+ return regexp.MustCompile("^[a-z0-9A-Z\u4e00-\u9fa5]+$").MatchString(src)
+}
+
+// IsPureChineseChar 匹配中文
+func IsPureChineseChar(src string) bool {
+ return regexp.MustCompile("[\u4e00-\u9fa5]").MatchString(src)
+}
+
+// 提取文字 包括数字 英文 不包含符号
+func ExtractPureEngChar(src string) string {
+ pureArray := regexp.MustCompile("[a-z0-9A-Z\u005f]").FindAllString(src, -1)
+ return strings.Join(pureArray, "")
+}
+
+// 提取文字 包括中文 数字 英文 不包含符号
+func ExtractPureChar(src string) string {
+ src = strings.TrimSpace(src)
+ pureArray := regexp.MustCompile("[a-z0-9A-Z\u4e00-\u9fa5]+").FindAllString(src, -1)
+ return strings.Join(pureArray, "")
+}
+
+// IsPhoneNumber 匹配所有国际化的手机号码(手机卡 + 上网卡)
+func IsGlobalizationPhoneNumber(phoneNumber string) bool {
+ num, _ := phonenumbers.Parse(phoneNumber, "CN")
+ return phonenumbers.IsValidNumber(num)
+}
+
+// IsEn 是否是英文的正则表达式验证
+func IsEn(data string) bool {
+ return regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(data)
+}
+
+// IsEnZh 是否是中英文的正则表达式验证
+func IsEnZh(data string) bool {
+ return regexp.MustCompile("^[a-zA-Z|\u4e00-\u9fa5]+$").MatchString(data)
+}
+
+// IsEnNum 是否是英文数字的正则表达式验证
+func IsEnNum(data string) bool {
+ return regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(data)
+}
+
+// IsEnNumSpe 是否是英文数字特殊符号的验证
+func IsEnNumSpe(data string) bool {
+ return regexp.MustCompile(`^[a-zA-Z0-9|~!@#$%^&*()/\|,.<>?"'();:_+-=\[\]{}]+$`).MatchString(data)
+}
+
+// IsObjectID 是否objectID的验证
+func IsObjectID(data string) bool {
+ _, err := primitive.ObjectIDFromHex(data)
+ if err != nil {
+ return false
+ }
+ return true
+}
+
+// IsLengthBetween 判断data字符串长度是否在范围内
+func IsLengthBetween(data string, min int, max int) bool {
+ return len(data) >= min && len(data) <= max
+}
+
+// IsNil nil判断
+func IsNil(i interface{}) bool {
+ vi := reflect.ValueOf(i)
+ if vi.Kind() == reflect.Ptr {
+ return vi.IsNil()
+ }
+ return false
+}
+
+// Phone 验证手机号码
+func Phone(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ if data, ok := field.Interface().(string); ok {
+ if len(data) <= 10 || len(data) >= 15 {
+ return false
+ }
+ if !IsGlobalizationPhoneNumber(data) {
+ return false
+ }
+ return true
+ }
+ return false
+}
+
+// Time_Utc 转换成UTC时间
+func Time_Utc(fl validator.FieldLevel) bool {
+ timeParse, err := time.Parse(time.RFC3339, fl.Field().String())
+ if err != nil {
+ return false
+ }
+ timevalue := reflect.ValueOf(timeParse)
+ fl.Field().Set(timevalue)
+ return true
+}
+
+// StrLength 验证长度
+func StrLength(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ num, err := strconv.Atoi(param)
+ if err != nil {
+ return false
+ }
+ if data, ok := field.Interface().(string); ok {
+ if len(data) == num {
+ return true
+ }
+ }
+ return false
+}
+
+// NeedType 需要类型
+func NeedType(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ if data, ok := field.Interface().(string); ok {
+ switch param {
+ case "en": // 英文
+ return IsEn(data)
+ case "enZh": // 中英文
+ return IsEnZh(data)
+ case "enNum": // 英文数字
+ return IsEnNum(data)
+ case "enNumSpe": // 英文数字特殊符号
+ return IsEnNumSpe(data)
+ case "objectID": // 是否为mongo的id
+ return IsObjectID(data)
+ }
+ }
+ return false
+}
+
+// RegisterValidator 注解自定义参数验证
+func RegisterValidator() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
+ //v.RegisterValidation("phone", Phone)
+ // v.RegisterValidation("len", StrLength)
+ // v.RegisterValidation("min", validator.IsGte)
+ // v.RegisterValidation("max", validator.IsLte)
+ // v.RegisterValidation("eq", validator.IsEq)
+ // v.RegisterValidation("type", NeedType)
+ v.RegisterValidation("time_utc", Time_Utc)
+ }
+ c.Next()
+ }
+}
diff --git a/pub/common/webpath/paths.go b/pub/common/webpath/paths.go
new file mode 100644
index 0000000..37202b6
--- /dev/null
+++ b/pub/common/webpath/paths.go
@@ -0,0 +1,105 @@
+package webpath
+
+import (
+ "net/url"
+ "strings"
+)
+
+// LegacyPathRemap 旧版 app 导航路径 → 当前 h5 SSR 实际路由。
+var LegacyPathRemap = map[string]string{
+ "/avtopic/": "/video/all",
+ "/avtopic": "/video/all",
+ "/dh/": "/nav",
+ "/dh": "/nav",
+}
+
+// SkipPublicPaths 当前 web 无对应页面、或不应对外暴露的路径。
+var SkipPublicPaths = map[string]struct{}{
+ "/topic/": {},
+ "/topic": {},
+ "/history/": {},
+ "/history": {},
+ "/tag/all/": {},
+ "/tag/all": {},
+ "/useragreement/": {},
+ "/useragreement": {},
+ "/privacypolicy/": {},
+ "/privacypolicy": {},
+ "/dmca/": {},
+ "/dmca": {},
+}
+
+// NormalizePublicURL 将 URL 规范为 web 可访问地址;第二个返回值 false 表示应跳过。
+func NormalizePublicURL(raw string) (string, bool) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return "", false
+ }
+
+ parsed, err := url.Parse(raw)
+ if err != nil {
+ return "", false
+ }
+
+ path := parsed.Path
+ if path == "" {
+ path = "/"
+ }
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+
+ normalizedPath := normalizePathKey(path)
+ if _, skip := SkipPublicPaths[normalizedPath]; skip {
+ return "", false
+ }
+ if mapped, ok := LegacyPathRemap[normalizedPath]; ok {
+ path = mapped
+ parsed.Path = path
+ }
+
+ if parsed.Path != "/" {
+ parsed.Path = strings.TrimRight(parsed.Path, "/")
+ if parsed.Path == "" {
+ parsed.Path = "/"
+ }
+ }
+
+ return parsed.String(), true
+}
+
+// NormalizePublicPath 规范单条站内 path(不含域名)。
+func NormalizePublicPath(path string) (string, bool) {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return "", false
+ }
+ if strings.Contains(path, "://") {
+ return NormalizePublicURL(path)
+ }
+ base := "https://example.com"
+ normalized, ok := NormalizePublicURL(base + path)
+ if !ok {
+ return "", false
+ }
+ parsed, err := url.Parse(normalized)
+ if err != nil {
+ return "", false
+ }
+ return parsed.Path, true
+}
+
+func normalizePathKey(path string) string {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return "/"
+ }
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+ lower := strings.ToLower(path)
+ if lower != "/" && !strings.HasSuffix(lower, "/") {
+ lower += "/"
+ }
+ return lower
+}
diff --git a/pub/common/webpath/paths_test.go b/pub/common/webpath/paths_test.go
new file mode 100644
index 0000000..f728da9
--- /dev/null
+++ b/pub/common/webpath/paths_test.go
@@ -0,0 +1,42 @@
+package webpath
+
+import "testing"
+
+func TestNormalizePublicURL(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ raw string
+ want string
+ allow bool
+ }{
+ {"https://91jxporn.com/avtopic/", "https://91jxporn.com/video/all", true},
+ {"https://91jxporn.com/dh/", "https://91jxporn.com/nav", true},
+ {"https://91jxporn.com/richtext?title=test", "https://91jxporn.com/richtext?title=test", true},
+ {"https://91jxporn.com/topic/", "", false},
+ {"https://91jxporn.com/tag/all/", "", false},
+ {"https://91jxporn.com/useragreement/", "", false},
+ }
+
+ for _, tt := range tests {
+ got, ok := NormalizePublicURL(tt.raw)
+ if ok != tt.allow {
+ t.Fatalf("NormalizePublicURL(%q) allow = %v, want %v", tt.raw, ok, tt.allow)
+ }
+ if !tt.allow {
+ continue
+ }
+ if got != tt.want {
+ t.Fatalf("NormalizePublicURL(%q) = %q, want %q", tt.raw, got, tt.want)
+ }
+ }
+}
+
+func TestNormalizePublicPath(t *testing.T) {
+ t.Parallel()
+
+ got, ok := NormalizePublicPath("/avtopic/")
+ if !ok || got != "/video/all" {
+ t.Fatalf("NormalizePublicPath(/avtopic/) = %q, %v", got, ok)
+ }
+}
diff --git a/pub/common/wordsfilter/filter.go b/pub/common/wordsfilter/filter.go
new file mode 100644
index 0000000..fa03ce9
--- /dev/null
+++ b/pub/common/wordsfilter/filter.go
@@ -0,0 +1,113 @@
+package wordsfilter
+
+import (
+ "bufio"
+ "io"
+ "net/http"
+ "os"
+ "regexp"
+ "time"
+)
+
+// Filter 敏感词过滤器
+type Filter struct {
+ trie *Trie
+ noise *regexp.Regexp
+}
+
+// New 返回一个敏感词过滤器
+func New() *Filter {
+ return &Filter{
+ trie: NewTrie(),
+ noise: regexp.MustCompile(`[\|\s&%$@*]+`),
+ }
+}
+
+// UpdateNoisePattern 更新去噪模式
+func (filter *Filter) UpdateNoisePattern(pattern string) {
+ filter.noise = regexp.MustCompile(pattern)
+}
+
+// LoadWordDict 加载敏感词字典
+func (filter *Filter) LoadWordDict(path string) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ return filter.Load(f)
+}
+
+// LoadNetWordDict 加载网络敏感词字典
+func (filter *Filter) LoadNetWordDict(url string) error {
+ c := http.Client{
+ Timeout: 5 * time.Second,
+ }
+ rsp, err := c.Get(url)
+ if err != nil {
+ return err
+ }
+ defer rsp.Body.Close()
+
+ return filter.Load(rsp.Body)
+}
+
+// Load common method to add words
+func (filter *Filter) Load(rd io.Reader) error {
+ buf := bufio.NewReader(rd)
+ for {
+ line, _, err := buf.ReadLine()
+ if err != nil {
+ if err != io.EOF {
+ return err
+ }
+ break
+ }
+ filter.trie.Add(string(line))
+ }
+
+ return nil
+}
+
+// AddWord 添加敏感词
+func (filter *Filter) AddWord(words ...string) {
+ filter.trie.Add(words...)
+}
+
+// DelWord 删除敏感词
+func (filter *Filter) DelWord(words ...string) {
+ filter.trie.Del(words...)
+}
+
+// Filter 过滤敏感词
+func (filter *Filter) Filter(text string) string {
+ return filter.trie.Filter(text)
+}
+
+// Replace 和谐敏感词
+func (filter *Filter) Replace(text string, repl rune) string {
+ return filter.trie.Replace(text, repl)
+}
+
+// FindIn 检测敏感词
+func (filter *Filter) FindIn(text string) (bool, string) {
+ text = filter.RemoveNoise(text)
+ return filter.trie.FindIn(text)
+}
+
+// FindAll 找到所有匹配词
+func (filter *Filter) FindAll(text string) []string {
+ return filter.trie.FindAll(text)
+}
+
+// Validate 检测字符串是否合法
+func (filter *Filter) Validate(text string) (bool, string) {
+ text = filter.RemoveNoise(text)
+ return filter.trie.Validate(text)
+}
+
+// RemoveNoise 去除空格等噪音
+func (filter *Filter) RemoveNoise(text string) string {
+ return filter.noise.ReplaceAllString(text, "")
+}
diff --git a/pub/common/wordsfilter/filter_test.go b/pub/common/wordsfilter/filter_test.go
new file mode 100644
index 0000000..49f7662
--- /dev/null
+++ b/pub/common/wordsfilter/filter_test.go
@@ -0,0 +1,396 @@
+package wordsfilter
+
+import (
+ "io"
+ "reflect"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+func TestLoadDict(t *testing.T) {
+ filter := New()
+ err := filter.LoadWordDict("./dict/dict.txt")
+ if err != nil {
+ t.Errorf("fail to load dict %v", err)
+ }
+}
+
+func TestLoadNetWordDict(t *testing.T) {
+ filter := New()
+ err := filter.LoadNetWordDict("https://raw.githubusercontent.com/importcjj/sensitive/master/dict/dict.txt")
+ if err != nil {
+ t.Errorf("fail to load dict %v", err)
+ }
+ if len(filter.trie.Root.Children) == 0 {
+ t.Errorf("load dict empty")
+ }
+}
+
+func TestLoad(t *testing.T) {
+ filter := New()
+ var r io.Reader
+ r = strings.NewReader("read")
+ err := filter.Load(r)
+ if err != nil {
+ t.Errorf("fail to load dict %v", err)
+ }
+ if len(filter.trie.Root.Children) == 0 {
+ t.Errorf("load dict empty")
+ }
+}
+
+func TestSensitiveFilter(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect string
+ DelWords []string
+ ThenExpect string
+ }{
+ {"我有一个东东西", "我有东", []string{"一个"}, "我有一"},
+ {"我有一个东西", "我", []string{"有一个东西"}, "我有"},
+ {"一个东西", "", []string{"一个", "东西"}, ""},
+ {"两个东西", "两西", []string{"个东"}, "两个"},
+ {"一个物体", "物体", []string{"一个"}, "一个物体"},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.Filter(tc.Text); got != tc.Expect {
+ t.Errorf("filter %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+
+ filter.DelWord(tc.DelWords...)
+
+ if got := filter.Filter(tc.Text); got != tc.ThenExpect {
+ t.Errorf("after del, filter %s, got %s, expect %s", tc.Text, got, tc.ThenExpect)
+ }
+
+ filter.AddWord(tc.DelWords...)
+ }
+
+}
+func TestSensitiveValidateSingleword(t *testing.T) {
+ filter := New()
+ filter.AddWord("东")
+
+ testcases := []struct {
+ Text string
+ ExpectPass bool
+ ExpectFirst string
+ }{
+ {"两个东西", false, "东"},
+ }
+
+ for _, tc := range testcases {
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ExpectFirst)
+ }
+ }
+
+}
+
+func TestSensitiveValidate(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+
+ testcases := []struct {
+ Text string
+ ExpectPass bool
+ ExpectFirst string
+ DelWords []string
+ ThenExpectPass bool
+ ThenExpectFirst string
+ }{
+ {"我有一@ |个东东西", false, "一个", []string{"一个"}, false, "个东"},
+ {"我有一个东东西", false, "一个", []string{"一个"}, false, "个东"},
+ {"我有一个东西", false, "有一个东西", []string{"有一个东西", "一个"}, false, "一个东西"},
+ {"一个东西", false, "一个", []string{"个东", "一个"}, false, "一个东西"},
+ {"两个东西", false, "个东", []string{"个东", "东西"}, true, ""},
+ {"一样东西", false, "东西", []string{"东西"}, true, ""},
+ }
+
+ for _, tc := range testcases {
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ExpectFirst)
+ }
+
+ filter.DelWord(tc.DelWords...)
+
+ if pass, first := filter.Validate(tc.Text); pass != tc.ThenExpectPass || first != tc.ThenExpectFirst {
+ t.Errorf("after del, validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ThenExpectPass, tc.ThenExpectFirst)
+ }
+
+ filter.AddWord(tc.DelWords...)
+ }
+
+}
+
+func TestSensitiveReplace(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect string
+ DelWords []string
+ ThenExpect string
+ }{
+ {"我有一个东东西", "我有**东**", []string{"一个"}, "我有一****"},
+ {"我有一个东西", "我*****", []string{"有一个东西"}, "我有****"},
+ {"一个东西", "****", []string{"一个东西", "一个", "东西"}, "一**西"},
+ {"两个东西", "两**西", []string{"个东"}, "两个**"},
+ {"一个物体", "**物体", []string{"一个"}, "一个物体"},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.Replace(tc.Text, '*'); got != tc.Expect {
+ t.Errorf("replace %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+
+ filter.DelWord(tc.DelWords...)
+
+ if got := filter.Replace(tc.Text, '*'); got != tc.ThenExpect {
+ t.Errorf("after del word, replace %s, got %s, expect %s", tc.Text, got, tc.ThenExpect)
+ }
+
+ filter.AddWord(tc.DelWords...)
+ }
+
+}
+
+func TestSensitiveFindAll(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect []string
+ }{
+ {"我有一个东东西", []string{"一个", "个东", "东西"}},
+ {"我有一个东西", []string{"有一个东西", "一个", "一个东西", "个东", "东西"}},
+ {"一个东西", []string{"一个", "一个东西", "个东", "东西"}},
+ {"两个东西", []string{"个东", "东西"}},
+ {"一个物体", []string{"一个"}},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.Expect, got) {
+ t.Errorf("findall %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+ }
+}
+
+func TestSensitiveFindallSingleword(t *testing.T) {
+ filter := New()
+ filter.AddWord("东")
+
+ testcases := []struct {
+ Text string
+ Expect []string
+ }{
+ {"两个东西", []string{"东"}},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.Expect, got) {
+ t.Errorf("findall %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+ }
+
+}
+
+func TestFindAllAfterDeleteSingleWord(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect []string
+ DelWord string
+ ThenExpect []string
+ }{
+ {"我有一个东东西", []string{"一个", "个东", "东西"}, "一个", []string{"个东", "东西"}},
+ {"我有一个东西", []string{"有一个东西", "一个", "一个东西", "个东", "东西"}, "有一个东西", []string{"一个", "一个东西", "个东", "东西"}},
+ {"一个东西", []string{"一个", "一个东西", "个东", "东西"}, "一个东西", []string{"一个", "个东", "东西"}},
+ {"两个东西", []string{"个东", "东西"}, "东西", []string{"个东"}},
+ {"一个物体", []string{"一个"}, "一个", nil},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.Expect, got) {
+ t.Errorf("findall %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+
+ filter.DelWord(tc.DelWord)
+
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.ThenExpect, got) {
+ t.Errorf("after del a word, findall %s, got %s, expect %s", tc.Text, got, tc.ThenExpect)
+ }
+
+ filter.AddWord(tc.DelWord)
+ }
+}
+
+func TestDeleteWordNotExists(t *testing.T) {
+ filter := New()
+ filter.DelWord("Any")
+ filter.DelWord("Go", "Bad")
+}
+
+func TestDeleteWord2(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("有一个东")
+ filter.AddWord("有一个")
+
+ testcases := []struct {
+ Text string
+ ExpectPass bool
+ ExpectFirst string
+ DelWord string
+ ThenExpectFirst string
+ }{
+ {"我有一个东西", false, "有一个", "有一个", "有一个东"},
+ }
+
+ for _, tc := range testcases {
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ExpectFirst)
+ }
+
+ filter.DelWord(tc.DelWord)
+
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ThenExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ThenExpectFirst)
+ }
+
+ filter.AddWord(tc.DelWord)
+ }
+}
+
+func TestFilter_LoadWordDict(t *testing.T) {
+ type fields struct {
+ trie *Trie
+ noise *regexp.Regexp
+ }
+ type args struct {
+ path string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filter := &Filter{
+ trie: tt.fields.trie,
+ noise: tt.fields.noise,
+ }
+ if err := filter.LoadWordDict(tt.args.path); (err != nil) != tt.wantErr {
+ t.Errorf("Filter.LoadWordDict() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestFilter_LoadNetWordDict(t *testing.T) {
+ type fields struct {
+ trie *Trie
+ noise *regexp.Regexp
+ }
+ type args struct {
+ url string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filter := &Filter{
+ trie: tt.fields.trie,
+ noise: tt.fields.noise,
+ }
+ if err := filter.LoadNetWordDict(tt.args.url); (err != nil) != tt.wantErr {
+ t.Errorf("Filter.LoadNetWordDict() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestFilter_Load(t *testing.T) {
+ type fields struct {
+ trie *Trie
+ noise *regexp.Regexp
+ }
+ type args struct {
+ rd io.Reader
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filter := &Filter{
+ trie: tt.fields.trie,
+ noise: tt.fields.noise,
+ }
+ if err := filter.Load(tt.args.rd); (err != nil) != tt.wantErr {
+ t.Errorf("Filter.Load() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestIssue13(t *testing.T) {
+ filter := New()
+ filter.AddWord("words")
+ filter.AddWord("警察局")
+
+ r := filter.FindAll("words words words kill myself myself suicide警察局kill my self")
+ expected := []string{"words", "警察局"}
+
+ if !reflect.DeepEqual(r, expected) {
+ t.Errorf("%v expected, got %v", expected, r)
+ }
+}
diff --git a/pub/common/wordsfilter/trie_tree.go b/pub/common/wordsfilter/trie_tree.go
new file mode 100644
index 0000000..d06bcc4
--- /dev/null
+++ b/pub/common/wordsfilter/trie_tree.go
@@ -0,0 +1,272 @@
+package wordsfilter
+
+// Trie 短语组成的Trie树.
+type Trie struct {
+ Root *Node
+}
+
+// Node Trie树上的一个节点.
+type Node struct {
+ isRootNode bool
+ isPathEnd bool
+ Character rune
+ Children map[rune]*Node
+}
+
+// NewTrie 新建一棵Trie
+func NewTrie() *Trie {
+ return &Trie{
+ Root: NewRootNode(0),
+ }
+}
+
+// Add 添加若干个词
+func (tree *Trie) Add(words ...string) {
+ for _, word := range words {
+ tree.add(word)
+ }
+}
+
+func (tree *Trie) add(word string) {
+ var current = tree.Root
+ var runes = []rune(word)
+ for position := 0; position < len(runes); position++ {
+ r := runes[position]
+ if next, ok := current.Children[r]; ok {
+ current = next
+ } else {
+ newNode := NewNode(r)
+ current.Children[r] = newNode
+ current = newNode
+ }
+ if position == len(runes)-1 {
+ current.isPathEnd = true
+ }
+ }
+}
+
+func (tree *Trie) Del(words ...string) {
+ for _, word := range words {
+ tree.del(word)
+ }
+}
+
+func (tree *Trie) del(word string) {
+ var current = tree.Root
+ var runes = []rune(word)
+ for position := 0; position < len(runes); position++ {
+ r := runes[position]
+ if next, ok := current.Children[r]; !ok {
+ return
+ } else {
+ current = next
+ }
+
+ if position == len(runes)-1 {
+ current.SoftDel()
+ }
+ }
+}
+
+// Replace 词语替换
+func (tree *Trie) Replace(text string, character rune) string {
+ var (
+ parent = tree.Root
+ current *Node
+ runes = []rune(text)
+ length = len(runes)
+ left = 0
+ found bool
+ )
+
+ for position := 0; position < len(runes); position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found || (!current.IsPathEnd() && position == length-1) {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ // println(string(current.Character), current.IsPathEnd(), left)
+ if current.IsPathEnd() && left <= position {
+ for i := left; i <= position; i++ {
+ runes[i] = character
+ }
+ }
+
+ parent = current
+ }
+
+ return string(runes)
+}
+
+// Filter 直接过滤掉字符串中的敏感词
+func (tree *Trie) Filter(text string) string {
+ var (
+ parent = tree.Root
+ current *Node
+ left = 0
+ found bool
+ runes = []rune(text)
+ length = len(runes)
+ resultRunes = make([]rune, 0, length)
+ )
+
+ for position := 0; position < length; position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found || (!current.IsPathEnd() && position == length-1) {
+ resultRunes = append(resultRunes, runes[left])
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ if current.IsPathEnd() {
+ left = position + 1
+ parent = tree.Root
+ } else {
+ parent = current
+ }
+
+ }
+
+ resultRunes = append(resultRunes, runes[left:]...)
+ return string(resultRunes)
+}
+
+// Validate 验证字符串是否合法,如不合法则返回false和检测到
+// 的第一个敏感词
+func (tree *Trie) Validate(text string) (bool, string) {
+ const (
+ Empty = ""
+ )
+ var (
+ parent = tree.Root
+ current *Node
+ runes = []rune(text)
+ length = len(runes)
+ left = 0
+ found bool
+ )
+
+ for position := 0; position < len(runes); position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found || (!current.IsPathEnd() && position == length-1) {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ if current.IsPathEnd() && left <= position {
+ return false, string(runes[left : position+1])
+ }
+
+ parent = current
+ }
+
+ return true, Empty
+}
+
+// FindIn 判断text中是否含有词库中的词
+func (tree *Trie) FindIn(text string) (bool, string) {
+ validated, first := tree.Validate(text)
+ return !validated, first
+}
+
+// FindAll 找有所有包含在词库中的词
+func (tree *Trie) FindAll(text string) []string {
+ var matches []string
+ var (
+ parent = tree.Root
+ current *Node
+ runes = []rune(text)
+ length = len(runes)
+ left = 0
+ found bool
+ )
+
+ for position := 0; position < length; position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ if current.IsPathEnd() && left <= position {
+ matches = append(matches, string(runes[left:position+1]))
+ }
+
+ if position == length-1 {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ parent = current
+ }
+
+ var i = 0
+ if count := len(matches); count > 0 {
+ set := make(map[string]struct{})
+ for i < count {
+ _, ok := set[matches[i]]
+ if !ok {
+ set[matches[i]] = struct{}{}
+ i++
+ continue
+ }
+ count--
+ copy(matches[i:], matches[i+1:])
+ }
+ return matches[:count]
+ }
+
+ return nil
+}
+
+// NewNode 新建子节点
+func NewNode(character rune) *Node {
+ return &Node{
+ Character: character,
+ Children: make(map[rune]*Node, 0),
+ }
+}
+
+// NewRootNode 新建根节点
+func NewRootNode(character rune) *Node {
+ return &Node{
+ isRootNode: true,
+ Character: character,
+ Children: make(map[rune]*Node, 0),
+ }
+}
+
+// IsLeafNode 判断是否叶子节点
+func (node *Node) IsLeafNode() bool {
+ return len(node.Children) == 0
+}
+
+// IsRootNode 判断是否为根节点
+func (node *Node) IsRootNode() bool {
+ return node.isRootNode
+}
+
+// IsPathEnd 判断是否为某个路径的结束
+func (node *Node) IsPathEnd() bool {
+ return node.isPathEnd
+}
+
+// SoftDel 置软删除状态
+func (node *Node) SoftDel() {
+ node.isPathEnd = false
+}
diff --git a/pub/common/wordsfilter/trie_tree_test.go b/pub/common/wordsfilter/trie_tree_test.go
new file mode 100644
index 0000000..46311f3
--- /dev/null
+++ b/pub/common/wordsfilter/trie_tree_test.go
@@ -0,0 +1,14 @@
+package wordsfilter
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestTrieTree(t *testing.T) {
+ tree := NewTrie()
+
+ tree.Add("习近平", "习大大")
+ fmt.Println(tree.Replace("你好吗 我支持习大大, 他的名字叫做习近平", '*'))
+ fmt.Println(tree.Filter("你好吗 我支持习大大, 他的名字叫做习近平"))
+}
diff --git a/pub/dataReport/handle.go b/pub/dataReport/handle.go
new file mode 100644
index 0000000..e85d13c
--- /dev/null
+++ b/pub/dataReport/handle.go
@@ -0,0 +1,235 @@
+package dataReport
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/pubdef"
+
+ "github.com/google/uuid"
+ "github.com/moul/http2curl"
+)
+
+var config = Config{}
+
+func Init(c Config) {
+ config = c
+ // 根据运行模式设置SDK配置API地址
+ switch c.RunMod {
+ case "prod", "release":
+ sdkConfigAPIURL = sdkConfigAPIProd
+ default:
+ sdkConfigAPIURL = sdkConfigAPIDev
+ }
+}
+
+// GenerateGlobalEventID 生成全局唯一的 event_id
+func GenerateGlobalEventID() string {
+ // 生成一个新的 UUID
+ return strings.ReplaceAll(uuid.New().String(), "-", "")
+}
+
+func AsyncReport(eventData interface{}, retryCount int) {
+ // TODO 后续可改成kafka异步处理
+ godef.Go(func() {
+ // 自动重试
+ baseDelay := 1 * time.Second
+ for i := 0; i < retryCount; i++ {
+ err := report(eventData)
+ if err != nil {
+ log.Error("Event.report failed", log.Any("retryCount", retryCount), log.Any("eventData", eventData), log.E(err))
+ // 指数退避 + 随机抖动
+ sleepTime := time.Duration(math.Pow(2, float64(i)))*baseDelay + time.Duration(rand.Intn(1000))*time.Millisecond
+ time.Sleep(sleepTime)
+ continue
+ }
+ break
+ }
+ })
+
+}
+
+// 上报事件
+func report(eventData interface{}) error {
+ // 将结构体转换为表单数据
+ formData, err := structToFormData(eventData)
+ if err != nil {
+ return err
+ }
+ apiUrl := config.ApiUrl + "/api/eventTracking/report.json"
+ req, err := http.NewRequest("POST", apiUrl, bytes.NewBufferString(formData))
+ if err != nil {
+ log.Error("Event report NewRequest fail", log.E(err))
+ return fmt.Errorf("failed to create request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ client := &http.Client{
+ Timeout: 5 * time.Second, // 设置超时时间为 5 秒
+ }
+ command, _ := http2curl.GetCurlCommand(req)
+ fmt.Println(command)
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Error("Event report fail", log.E(err))
+ return fmt.Errorf("failed to send request: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ log.Error("Event report fail", log.Any("http.code", resp.StatusCode))
+ return fmt.Errorf("failed to report event, http.code: %d", resp.StatusCode)
+ }
+ // 读取返回的 body
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ log.Error("Event report fail", log.E(err))
+ return fmt.Errorf("failed to read response body: %v", err)
+ }
+ res := &Response{}
+ err = json.Unmarshal(body, res)
+ if err != nil {
+ log.Error("Event report json.Unmarshal(body, res) fail", log.Any("body", string(body)), log.E(err))
+ return err
+ }
+ if res.Code != 0 {
+ log.Error("Event report fail", log.Any("code", res.Code), log.Any("msg", res.Msg), log.Any("data", res.Data))
+ return fmt.Errorf("failed to report event, res.code: %d", res.Code)
+ }
+ // 输出响应状态和返回的 body
+ log.Info("Event report Response", log.Any("eventData", eventData), log.Any("body", string(body)))
+ return nil
+}
+
+func structToFormData(data interface{}) (string, error) {
+ // 序列化结构体为 JSON 字符串
+ jsonData, err := json.Marshal(data)
+ if err != nil {
+ return "", err
+ }
+
+ // 反序列化为 map
+ var dataMap map[string]interface{}
+ err = json.Unmarshal(jsonData, &dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ var formData []string
+
+ // 遍历 map,将其转换为 form 数据
+ for key, value := range dataMap {
+ if value == nil {
+ continue // 如果值为 nil,跳过
+ }
+
+ // 将值转换为字符串
+ valueStr := fmt.Sprintf("%v", value)
+
+ // 使用 URL 编码
+ formData = append(formData, fmt.Sprintf(
+ "%s=%s",
+ url.QueryEscape(key),
+ url.QueryEscape(valueStr),
+ ))
+ }
+
+ return strings.Join(formData, "&"), nil
+}
+
+func NewCommonField(event string, districtCode string, ua useragent.UA, ip string, uid int64) CommonField {
+ return NewCommonFieldAt(event, districtCode, ua, ip, uid, time.Now().Unix())
+}
+
+func NewCommonFieldAt(event string, districtCode string, ua useragent.UA, ip string, uid int64, clientTS int64) CommonField {
+ eventId := GenerateGlobalEventID()
+ // 五项公共字段和 app_version 使用当前请求中的 SDK 原值,旧客户端缺失时留空。
+ // 不能用业务 DevID、用户表会话或推算的平台补齐,否则仍会与客户端埋点不一致。
+ sdk := ua.SDKCommon
+ if missing := sdk.MissingFields(); len(missing) > 0 {
+ log.Warn("【SDK公共参数缺失】NewCommonFieldAt",
+ log.Any("接口", ua.RequestPath), log.Any("事件", event),
+ log.Any("eventId", eventId), log.Any("userId", uid), log.Any("客户端版本", ua.Ver),
+ log.Any("已获取SDK参数", sdk != nil), log.Any("缺失字段", missing))
+ }
+ if sdk == nil {
+ sdk = &useragent.SDKCommonFields{}
+ }
+ return CommonField{
+ UID: fmt.Sprintf("%v", uid),
+ EventID: eventId,
+ Event: event,
+ Channel: districtCode,
+ AppID: pubdef.ReportAppId(),
+ AppVersion: sdk.AppVersion,
+ ClientTS: fmt.Sprintf("%v", clientTS),
+ Device: sdk.Device,
+ DeviceID: sdk.DeviceID,
+ DeviceFingerprint: sdk.DeviceFingerprint,
+ FPVersion: sdk.FPVersion,
+ UserAgent: strings.TrimSpace(ua.UserAgent),
+ DeviceModel: strings.TrimSpace(ua.DevType),
+ SID: sdk.SID,
+ IP: ip,
+ }
+}
+
+func JsonNumber(v int64) json.Number {
+ return json.Number(strconv.FormatInt(v, 10))
+}
+func JsonRawMessage(v interface{}) json.RawMessage {
+ b, _ := json.Marshal(v)
+ return b
+}
+
+// 批量上报事件
+func BatchReport(ctx context.Context, data interface{}) error {
+ apiUrl := config.ApiUrl + "/api/eventTracking/batchReport.json"
+ reqBody, err := json.Marshal(data)
+ if err != nil {
+ log.Error("Event report json.Marshal fail", log.E(err))
+ return err
+ }
+ req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(reqBody))
+ if err != nil {
+ log.Error("Event report NewRequest fail", log.E(err))
+ return fmt.Errorf("failed to create request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ curl, _ := http2curl.GetCurlCommand(req)
+ client := &http.Client{
+ Timeout: 5 * time.Second, // 设置超时时间为 5 秒
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Error("Event report fail", log.Any("curl", curl), log.E(err))
+ return fmt.Errorf("failed to send request: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ log.Error("Event report fail", log.Any("curl", curl), log.Any("http.code", resp.StatusCode))
+ return fmt.Errorf("failed to report event, status code: %d", resp.StatusCode)
+ }
+ // 读取返回的 body
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ log.Error("Event report fail", log.Any("curl", curl), log.E(err))
+ return fmt.Errorf("failed to read response body: %v", err)
+ }
+ // 输出响应状态和返回的 body
+ log.Info("Event report Response", log.Any("body", string(body)))
+ return nil
+}
diff --git a/pub/dataReport/sdk_config.go b/pub/dataReport/sdk_config.go
new file mode 100644
index 0000000..e8c748f
--- /dev/null
+++ b/pub/dataReport/sdk_config.go
@@ -0,0 +1,131 @@
+package dataReport
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/redis"
+
+ "github.com/robfig/cron/v3"
+)
+
+const (
+ sdkConfigAPIProd = "https://api.dc-server.cc/sdk/getAvailableDomainList"
+ sdkConfigAPIDev = "https://dcapi.dc-dev.cc/sdk/getAvailableDomainList"
+ sdkConfigCacheKey = "report:sdk:domain_config"
+ sdkConfigCacheTTL = 30 * time.Minute
+ sdkConfigFetchInterval = "0 */10 * * * *"
+)
+
+var (
+ sdkConfigAPIURL = sdkConfigAPIProd // 默认正式环境,Init时会根据RunMod覆盖
+ sdkConfigCron *cron.Cron
+ sdkConfigOnce sync.Once
+
+ sdkConfigMu sync.RWMutex
+ sdkDomainConfig string
+ sdkConfigUpdatedAt time.Time
+)
+
+type sdkConfigResponse struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data string `json:"data"`
+}
+
+// StartSDKConfigCron 定时拉取 SDK 域名配置串,服务端只缓存并原样透传。
+func StartSDKConfigCron() {
+ sdkConfigOnce.Do(func() {
+ if err := RefreshSDKDomainConfig(); err != nil {
+ log.Error("RefreshSDKDomainConfig first run fail", log.E(err))
+ }
+
+ sdkConfigCron = cron.New(cron.WithSeconds())
+ if _, err := sdkConfigCron.AddFunc(sdkConfigFetchInterval, func() {
+ if err := RefreshSDKDomainConfig(); err != nil {
+ log.Error("RefreshSDKDomainConfig cron fail", log.E(err))
+ }
+ }); err != nil {
+ log.Error("StartSDKConfigCron AddFunc fail", log.E(err))
+ return
+ }
+ sdkConfigCron.Start()
+ })
+}
+
+func StopSDKConfigCron() {
+ if sdkConfigCron == nil {
+ return
+ }
+ ctx := sdkConfigCron.Stop()
+ <-ctx.Done()
+}
+
+func RefreshSDKDomainConfig() error {
+ data, err := fetchSDKDomainConfig(sdkConfigAPIURL, &http.Client{Timeout: 5 * time.Second})
+ if err != nil {
+ return err
+ }
+ setSDKDomainConfig(data)
+ if redis.Client() == nil {
+ return nil
+ }
+ return redis.Set(sdkConfigCacheKey, data, sdkConfigCacheTTL)
+}
+
+func GetSDKDomainConfig() string {
+ if redis.Client() != nil {
+ data, err := redis.Get(sdkConfigCacheKey)
+ if err != nil {
+ log.Error("GetSDKDomainConfig redis get fail", log.E(err))
+ } else if data != nil && *data != "" {
+ setSDKDomainConfig(*data)
+ return *data
+ }
+ }
+
+ sdkConfigMu.RLock()
+ defer sdkConfigMu.RUnlock()
+ return sdkDomainConfig
+}
+
+func setSDKDomainConfig(data string) {
+ sdkConfigMu.Lock()
+ defer sdkConfigMu.Unlock()
+ sdkDomainConfig = data
+ sdkConfigUpdatedAt = time.Now()
+}
+
+func fetchSDKDomainConfig(apiURL string, client *http.Client) (string, error) {
+ req, err := http.NewRequest(http.MethodGet, apiURL, nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Accept", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("sdk config http status: %d", resp.StatusCode)
+ }
+
+ var result sdkConfigResponse
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return "", err
+ }
+ if result.Code != 0 {
+ return "", fmt.Errorf("sdk config code: %d, msg: %s", result.Code, result.Msg)
+ }
+ if strings.TrimSpace(result.Data) == "" {
+ return "", fmt.Errorf("sdk config data is empty")
+ }
+ return result.Data, nil
+}
diff --git a/pub/dataReport/type.go b/pub/dataReport/type.go
new file mode 100644
index 0000000..ba7089b
--- /dev/null
+++ b/pub/dataReport/type.go
@@ -0,0 +1,224 @@
+package dataReport
+
+import (
+ "context"
+ "encoding/json"
+)
+
+type Response struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data interface{} `json:"data"`
+}
+
+type Config struct {
+ ApiUrl string `json:"apiUrl"`
+ RunMod string `json:"runMod"` // 运行模式: dev/test/prod/release
+}
+
+const (
+ EventTypeAdClick = "ad_click"
+ EventTypeUserLogin = "user_login"
+ EventTypeUserRegister = "user_register"
+ EventTypeOrderCreated = "order_created"
+ EventTypeOrderPaid = "order_paid"
+ EventTypeCoinConsume = "coin_consume"
+ EventTypeVideoPurchase = "video_purchase"
+ EventTypeVideoLike = "video_like"
+ EventTypeVideoCollect = "video_collect"
+ EventTypePlayletLike = "playlet_like"
+ EventTypeVideoComment = "video_comment"
+ EventTypeVideoStatusChange = "video_status_change" // 视频状态变更
+ EventTypeDramaLike = "drama_like"
+ EventTypeDramaCollect = "drama_collect"
+ EventTypeDramaComment = "drama_comment"
+ EventTypeDramaPurchase = "drama_purchase"
+
+ DramaContentTypeShortDrama = "short_drama"
+ DramaUnlockTypeCoin = "coin"
+ DramaUnlockTypeVIP = "vip_subscription"
+ DramaUnlockTypeAd = "ad"
+ DramaUnlockTypeOther = "other"
+)
+
+// Event 定义事件的接口
+type Event interface {
+ Generate(ctx context.Context, retryCount int)
+ AsyncReport(ctx context.Context, retryCount int) error // 执行事件上报
+}
+
+// 定义公共字段和事件数据结构
+type CommonField struct {
+ Event string `json:"event"` // 事件类型
+ Channel string `json:"channel"` // 渠道码
+ EventID string `json:"event_id"` // 事件唯一标识符
+ AppID string `json:"app_id"` // 应用ID
+ AppVersion string `json:"app_version"` // 客户端通过 X-SDK-Common 传入的版本,缺失时留空
+ UID string `json:"uid"` // 用户ID
+ SID string `json:"sid"` // 会话ID
+ ClientTS string `json:"client_ts"` // 客户端时间戳
+ Device string `json:"device"` // 设备类型(如:Android, iOS, PC)
+ DeviceID string `json:"device_id"` // 设备ID
+ DeviceFingerprint string `json:"device_fingerprint"` // SDK 设备指纹,SDK 原值为空时也保留字段。
+ FPVersion string `json:"fp_version"` // SDK 指纹算法版本
+ UserAgent string `json:"user_agent"` // 用户代理信息
+ DeviceBrand string `json:"device_brand"` // 设备品牌(如:HUAWEI)
+ DeviceModel string `json:"device_model"` // 设备型号
+ SystemName string `json:"system_name"` // 系统名称
+ SystemVersion string `json:"system_version"` // 系统版本
+ IP string `json:"ip"` // 用户IP地址
+ Payload json.RawMessage `json:"payload"` // 事件数据
+}
+
+// UserRegisterEvent 注册事件
+type UserRegisterEvent struct {
+ Type string `json:"type"` // 注册方式:phone, deviceid, email, username
+ TraceID string `json:"trace_id"` // 唯一标识,落地页点击生成
+ CreateTime json.Number `json:"create_time"` // 注册时间,10位时间戳
+}
+
+type UserLoginEvent struct {
+ Type string `json:"type"` // 登录方式
+}
+
+// OrderCreatedEvent 订单创建
+type OrderCreatedEvent struct {
+ OrderID string `json:"order_id"` // 订单号(唯一标识)
+ OrderType string `json:"order_type"` // 订单类型
+ ProductID string `json:"product_id"` // 商品ID
+ ProductName string `json:"product_name"` // 商品名称
+ VIPDurationID string `json:"vip_duration_type"`
+ VIPDurationName string `json:"vip_duration_name"`
+ Amount json.Number `json:"amount"` // 订单金额(分)
+ Currency string `json:"currency"` // 货币类型
+ CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
+ SourcePageKey string `json:"source_page_key"` // 来源页面标识
+ SourcePageName string `json:"source_page_name"` // 来源页面名称
+ CreateTime json.Number `json:"create_time"` // 订单创建时间
+}
+
+// OrderPaidEvent 订单支付成功
+type OrderPaidEvent struct {
+ OrderID string `json:"order_id"` // 订单号(关联创建事件)
+ OrderType string `json:"order_type"` // 订单类型:coin_purchase, vip_subscription
+ ProductID string `json:"product_id"` // 商品ID
+ Amount json.Number `json:"amount"` // 实际支付金额(分)
+ Currency string `json:"currency"` // 货币类型:CNY, USD
+ CoinQuantity json.Number `json:"coin_quantity"` // 金币数量(仅金币订单)
+ VIPExpirationTime json.Number `json:"vip_expiration_time"` // VIP过期时间(10位时间戳)
+ PayType string `json:"pay_type"` // 支付方式:wechat, alipay, bank_card
+ PayChannel string `json:"pay_channel"` // 支付渠道:银行名称
+ TransactionID string `json:"transaction_id"` // 第三方支付交易号
+ CreateTime json.Number `json:"create_time"`
+}
+
+// CoinConsumeEvent 金币消耗
+type CoinConsumeEvent struct {
+ ProductID string `json:"product_id"` // 商品ID
+ ProductName string `json:"product_name"` // 商品名称
+ CoinConsumeAmount json.Number `json:"coin_consume_amount"` // 金币消耗数量
+ CoinBalanceBefore json.Number `json:"coin_balance_before"` // 消耗前金币余额
+ CoinBalanceAfter json.Number `json:"coin_balance_after"` // 消耗后金币余额
+ ConsumeReasonKey string `json:"consume_reason_key"` // 消耗原因
+ ConsumeReasonName string `json:"consume_reason_name"` // 消耗原因名称
+ CreateTime json.Number `json:"create_time"` // 订单创建时间
+ OrderId string `json:"order_id"` // 订单id
+}
+
+// VideoPurchaseEvent 视频购买事件
+type VideoPurchaseEvent struct {
+ MediaId string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段)
+ VideoID string `json:"video_id"` // 视频ID
+ VideoTitle string `json:"video_title"` // 视频标题
+ VideoTypeID string `json:"video_type_id"` // 视频分类ID
+ VideoTypeName string `json:"video_type_name"` // 视频分类名称
+ VideoContentType string `json:"video_content_type"` // 视频类型: video(长视频)、short_video(短视频)
+ RecommendTraceId string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
+ CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
+ OrderID string `json:"order_id"` // 订单号
+}
+
+type (
+ VideoEventPub struct {
+ MediaId string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段)
+ VideoID string `json:"video_id"` // 视频ID
+ VideoTitle string `json:"video_title"` // 视频标题
+ VideoTypeID string `json:"video_type_id"` // 视频分类ID
+ VideoTypeName string `json:"video_type_name"` // 视频分类名称
+ VideoContentType string `json:"video_content_type"` // 视频类型: video(长视频)、short_video(短视频)
+ RecommendTraceId string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
+ }
+
+ VideoLikeEvent struct {
+ VideoEventPub
+ Flag json.Number `json:"flag"` // 点赞状态:1(点赞), 2(取消点赞)
+ }
+
+ VideoCollectEvent struct {
+ VideoEventPub
+ Flag json.Number `json:"flag"` // 收藏状态:1(收藏), 2(取消收藏)
+ }
+
+ VideoCommentEvent struct {
+ VideoEventPub
+ CommentContent string `json:"comment_content"` // 评论内容
+ }
+
+ // VideoStatusChangeEvent 视频状态变更事件
+ VideoStatusChangeEvent struct {
+ MediaId string `json:"media_id"` // 老司机媒体资源ID
+ VideoID string `json:"video_id"` // 视频ID
+ VideoTitle string `json:"video_title"` // 视频标题
+ VideoTypeID string `json:"video_type_id"` // 视频分类ID
+ VideoTypeName string `json:"video_type_name"` // 视频分类名称
+ Flag json.Number `json:"flag"` // 状态标识
+ }
+
+ DramaEventPub struct {
+ DramaID string `json:"drama_id"` // 剧集章节ID(PlayletChapter.Id)
+ DramaTitle string `json:"drama_title"` // 剧集名称
+ Episode json.Number `json:"episode"` // 第几集,默认1
+ VideoID string `json:"video_id"` // 媒资中心章节ID(PlayletChapter.MediaContentId)
+ TotalEpisode json.Number `json:"total_episode"` // 已上线总集数
+ DramaTypeID string `json:"drama_type_id"` // 剧集分类ID
+ DramaTypeName string `json:"drama_type_name"` // 剧集分类名称
+ DramaContentType string `json:"drama_content_type"` // short_drama
+ DramaTagKey string `json:"drama_tag_key"` // 标签KEY,逗号分隔
+ DramaTagName string `json:"drama_tag_name"` // 标签名称,逗号分隔
+ DramaDuration json.Number `json:"drama_duration"` // 剧集时长(秒)
+ }
+
+ DramaLikeEvent struct {
+ DramaEventPub
+ Flag json.Number `json:"flag"` // 点赞状态:1(点赞), 2(取消点赞)
+ }
+
+ DramaCollectEvent struct {
+ DramaEventPub
+ Flag json.Number `json:"flag"` // 收藏状态:1(收藏), 2(取消收藏)
+ }
+
+ DramaCommentEvent struct {
+ DramaEventPub
+ CommentContent string `json:"comment_content"` // 评论内容
+ }
+
+ DramaPurchaseEvent struct {
+ DramaEventPub
+ CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
+ OrderID string `json:"order_id"` // 订单号,与 coin_consume 保持一致
+ UnlockType string `json:"unlock_type"` // coin / vip_subscription / ad / other
+ AdID string `json:"ad_id"` // 广告解锁时的广告ID
+ }
+)
+
+type AdClickEvent struct {
+ CommonField
+ PageKey string `json:"page_key"` // 页面标识
+ PageName string `json:"page_name"` // 页面名称
+ AdSlotKey string `json:"ad_slot_key"` // 广告位标识
+ AdSlotName string `json:"ad_slot_name"` // 广告位名称
+ AdID string `json:"ad_id"` // 广告ID
+ CreativeID string `json:"creative_id"` // 素材ID(可选)
+ AdType string `json:"ad_type"` // 广告类型
+}
diff --git a/pub/data_center/datacenter.go b/pub/data_center/datacenter.go
new file mode 100644
index 0000000..bcd0ab2
--- /dev/null
+++ b/pub/data_center/datacenter.go
@@ -0,0 +1,437 @@
+// 接入方只需要填自己的业务数据,其余交给 SDK:
+//
+// - data_date 留空自动填当天 UTC 日期(回补历史数据时显式指定)
+// - company 留空自动填「瑞升」
+// - role_id 留空自动补 [](不是 null)
+// - 发送前做必填校验和日期格式校验,问题一次全列出来
+// - JSON 编码、带 API Key 的 POST、超时控制
+//
+// 最小用法:
+//
+// client := datacenter.New(os.Getenv("STUDIO_API_KEY"))
+//
+// resp, err := client.Send(ctx, datacenter.Record{
+// CompanyPlatform: "黄果短剧", // 必填:所属产品/项目名
+// PublishedData: "01/09/2026", // 必填:视频上线时间
+// ScriptName: "剧本A", // 必填:剧本名称
+// DramaName: "短剧A", // 必填:短剧名称
+// DramaNo: "第1集", // 必填:集数
+// VideoURL: "https://cdn.x/a.mp4", // 必填:成片下载链接
+// PlatformURL: "https://x.com/a", // 必填:产品内播放页链接
+//
+// VideoID: "12345", // 选填但建议填
+// Views: 10000,
+// Clicks: 8200,
+// Likes: 300,
+// })
+//
+// 一次可以传多条:client.Send(ctx, r1, r2, r3)。
+package datacenter
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+)
+
+const (
+ // DefaultEndpoint 是数据回流接口地址,Client.Endpoint 留空时用它。
+ DefaultEndpoint = "https://ldschool.up.railway.app/api/v1/video-performance/daily"
+
+ // DefaultCompany 是所属公司的默认值,接入方不用填。
+ DefaultCompany = "瑞升"
+
+ // DateLayout 是接口要求的日期格式:DD/MM/YYYY,对应 Go 的 "02/01/2006"。
+ // 注意和 "2006-01-02" 的区别,写成后者会被本地校验拦下。
+ DateLayout = "02/01/2006"
+)
+
+// Record 是一条视频在某一天的数据。一次请求可以带多条。
+//
+// 字段分三类:
+//
+// 1. SDK 自动填:DataDate、Company、RoleID,接入方留空即可。
+// 2. 必填:CompanyPlatform、PublishedData、ScriptName、DramaName、DramaNo、
+// VideoURL、PlatformURL。留空会在本地就报错,不会白跑一次请求。
+// 3. 选填:其余字段。带 omitempty 的字段留空就整个不出现在请求体里,
+// 不会用空字符串污染服务端数据。
+type Record struct {
+ // ---- 由 SDK 自动填,接入方留空即可 ----
+
+ // DataDate 数据所属日期,格式 DD/MM/YYYY。
+ // 留空 = 当天 UTC 日期;要回补历史数据就显式写,如 "21/09/2026"。
+ DataDate string `json:"data_date"`
+
+ // Company 所属公司,留空 = 瑞升。
+ Company string `json:"company"`
+
+ // RoleID 角色 ID 数组,留空会发送 [](而不是 null)。
+ RoleID []string `json:"role_id"`
+
+ // ---- 必填 ----
+
+ // CompanyPlatform 所属项目/产品名称,如「黄果短剧」。
+ CompanyPlatform string `json:"company_platform"`
+
+ // PublishedData 视频首次上线时间,格式 DD/MM/YYYY。
+ PublishedData string `json:"published_data"`
+
+ // ScriptName 剧本名称,需与剧本 ID 对应一致。
+ ScriptName string `json:"script_name"`
+
+ // DramaName 短剧名称,需与集数保持一致。
+ DramaName string `json:"drama_name"`
+
+ // DramaNo 集数,如「第1集」。
+ DramaNo string `json:"drama_no"`
+
+ // VideoURL 可访问的成片下载/存储链接。
+ VideoURL string `json:"video_url"`
+
+ // PlatformURL 视频在产品内的播放页面链接。
+ PlatformURL string `json:"platform_url"`
+
+ // ---- 选填 ----
+
+ // VideoID 视频唯一标识。选填,但建议填,便于中台对齐同一条视频的历史数据。
+ VideoID string `json:"video_id,omitempty"`
+
+ // ScriptID 剧本唯一编号。
+ ScriptID string `json:"script_id,omitempty"`
+
+ // PlatformVideoID 视频在产品平台内的唯一 ID。
+ PlatformVideoID string `json:"platform_video_id,omitempty"`
+
+ // Views 浏览量:统计周期内的视频曝光/浏览次数。
+ Views int64 `json:"views"`
+
+ // Clicks 播放量:统计周期内的有效播放次数。
+ Clicks int64 `json:"clicks"`
+
+ // Likes 点赞量:统计周期内的累计点赞次数。
+ Likes int64 `json:"likes"`
+
+ // LikeRate 点赞率,字符串带百分号,如 "3.66%"。
+ // 计算公式:点赞量 ÷ 播放量 × 100%,保留两位小数。
+ LikeRate string `json:"like_rate,omitempty"`
+
+ // CompletionRate 完播率,如 "43.00%"。
+ // 没有单条视频数据时,可以填剧本整体的完播率。
+ CompletionRate string `json:"completion_rate,omitempty"`
+
+ // PaidRate 付费率,如 "0.00%"。
+ // 计算公式:付费用户 ÷ 播放用户 × 100%;免费短剧统一填 "0.00%"。
+ PaidRate string `json:"paid_rate,omitempty"`
+
+ // Comments 评论量。用指针是为了区分「没填」和「填了 0」:
+ // 填 datacenter.Int(0) 会真的发出 0,留 nil 则整个字段省略。
+ Comments *int64 `json:"comments,omitempty"`
+
+ // Shares 分享量,和 Comments 同理。
+ Shares *int64 `json:"shares,omitempty"`
+
+ // CompletionRateScope 完播率口径,取值 video / script / unknown。
+ CompletionRateScope string `json:"completion_rate_scope,omitempty"`
+}
+
+// Int 返回指向 v 的指针,用来给 Comments / Shares 填 0。
+//
+// Comments: datacenter.Int(0) // 发出 "comments": 0
+// Comments: nil // 不发送 comments 字段
+func Int(v int64) *int64 { return &v }
+
+// request 是接口的请求体,数据都包在 records 数组里。
+type request struct {
+ Records []Record `json:"records"`
+}
+
+// Response 是接口返回,方便直接打日志。
+type Response struct {
+ StatusCode int // HTTP 状态码
+ Body []byte // 响应体原文,最多读 1MB
+}
+
+// OK 表示接口返回 200。
+func (r *Response) OK() bool { return r != nil && r.StatusCode == http.StatusOK }
+
+// ValidationError 表示本地校验没过。Problems 里逐条说明是第几条、缺什么,
+// 一次列全,不用改一个跑一次。
+type ValidationError struct {
+ Problems []string // 例:`records[0]: 缺少必填字段: script_name, video_url`
+}
+
+func (e *ValidationError) Error() string {
+ return "datacenter: 数据校验失败:\n " + strings.Join(e.Problems, "\n ")
+}
+
+// Client 是回流客户端。创建一次就能一直复用,多个 goroutine 同时用也没问题。
+type Client struct {
+ APIKey string // 领取时展示的完整 Key,以 ld_ 开头;建议从环境变量读,别写进代码
+ Endpoint string // 接口地址,留空用 DefaultEndpoint
+ HTTPClient *http.Client // 留空用 30 秒超时的默认 client
+
+ // OnError 是异步上报失败时的回调,可选。不设置就打到 stderr。
+ // 校验失败、网络失败、队列满被丢弃,都会走这里。
+ // 回调在后台 goroutine 里执行,里面别做耗时操作。
+ OnError func(error)
+
+ // QueueSize 是异步队列能缓存的批次数,留空用 DefaultQueueSize。
+ // 队列满了会丢弃新数据(不阻塞主流程)并回调 OnError。
+ QueueSize int
+
+ // 以下为异步内部状态,接入方不用管。
+ mu sync.Mutex
+ queue chan []Record
+ done chan struct{}
+ started bool
+ closed bool
+}
+
+// DefaultQueueSize 是异步队列的默认长度(批次数)。
+const DefaultQueueSize = 1000
+
+// asyncTimeout 是后台单次请求的超时,比 HTTPClient 的 30 秒留一点余量。
+const asyncTimeout = 60 * time.Second
+
+// New 用 API Key 创建客户端,地址和 HTTP client 都用默认值。
+func New(apiKey string) *Client {
+ return &Client{
+ APIKey: strings.TrimSpace(apiKey),
+ Endpoint: DefaultEndpoint,
+ HTTPClient: &http.Client{Timeout: 30 * time.Second},
+ QueueSize: DefaultQueueSize,
+ }
+}
+
+// Send 上报一批数据:先补齐默认值(日期/公司/角色数组),再校验,最后整体 POST。
+//
+// 校验不通过返回 *ValidationError(不发请求);服务端返回非 200 时同时返回
+// Response 和 error,Response 里带着状态码和响应体,方便排查。
+//
+// 只打印不发请求的话用 JSON()。
+func (c *Client) Send(ctx context.Context, records ...Record) (*Response, error) {
+ if c.APIKey == "" {
+ return nil, fmt.Errorf("datacenter: APIKey 为空")
+ }
+ if !strings.HasPrefix(c.APIKey, "ld_") {
+ return nil, fmt.Errorf("datacenter: APIKey 需要是领取时展示的完整 Key,以 ld_ 开头")
+ }
+ if len(records) == 0 {
+ return nil, fmt.Errorf("datacenter: 没有要发送的数据")
+ }
+
+ // 复制一份再动,不改调用方传进来的切片。
+ payload := make([]Record, len(records))
+ copy(payload, records)
+
+ // 校验全部走一遍再返回,把问题攒一起报。
+ var problems []string
+ for i := range payload {
+ payload[i].fillDefaults()
+ if err := payload[i].validate(); err != nil {
+ problems = append(problems, fmt.Sprintf("records[%d]: %v", i, err))
+ }
+ }
+ if len(problems) > 0 {
+ return nil, &ValidationError{Problems: problems}
+ }
+
+ body, err := json.Marshal(request{Records: payload})
+ if err != nil {
+ return nil, fmt.Errorf("datacenter: 编码请求失败: %w", err)
+ }
+
+ endpoint := c.Endpoint
+ if endpoint == "" {
+ endpoint = DefaultEndpoint
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
+ if err != nil {
+ return nil, fmt.Errorf("datacenter: 构造请求失败: %w", err)
+ }
+ req.Header.Set("Authorization", "Bearer "+c.APIKey)
+ req.Header.Set("Content-Type", "application/json")
+
+ client := c.HTTPClient
+ if client == nil {
+ client = &http.Client{Timeout: 30 * time.Second}
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("datacenter: 请求失败: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // 只读 1MB,防止对面返回异常大的内容把内存吃掉。
+ respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
+ if err != nil {
+ return nil, fmt.Errorf("datacenter: 读取响应失败: %w", err)
+ }
+ out := &Response{StatusCode: resp.StatusCode, Body: respBody}
+ if !out.OK() {
+ return out, fmt.Errorf("datacenter: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
+ }
+ return out, nil
+}
+
+// SendAsync 异步上报一批数据,立即返回,不会阻塞调用方。
+//
+// 数据先进内存队列,由后台 goroutine 依次发送;同一个 client 的发送顺序
+// 和调用顺序一致。适合放在主流程里(定时任务、接口 handler)用。
+//
+// 三点必须知道:
+//
+// 1. 失败不会返回给调用方,通过 c.OnError 回调(不设置就打到 stderr)。
+// 2. 队列满了会丢弃新数据并回调,宁丢不堵——主流程优先。
+// 3. 进程退出前必须调 c.Close() 等数据发完,否则队列里的会丢。
+//
+// 需要拿到结果、或者只是短脚本发一次就退出,直接用同步的 Send。
+func (c *Client) SendAsync(records ...Record) {
+ if len(records) == 0 {
+ return
+ }
+
+ var err error
+ c.mu.Lock()
+ switch {
+ case c.closed:
+ err = fmt.Errorf("datacenter: 客户端已 Close,丢弃 %d 条数据", len(records))
+ default:
+ if !c.started {
+ size := c.QueueSize
+ if size <= 0 {
+ size = DefaultQueueSize
+ }
+ c.queue = make(chan []Record, size)
+ c.done = make(chan struct{})
+ c.started = true
+ go c.worker()
+ }
+ select {
+ case c.queue <- records:
+ default:
+ err = fmt.Errorf("datacenter: 异步队列已满(%d 批),丢弃 %d 条数据", cap(c.queue), len(records))
+ }
+ }
+ c.mu.Unlock()
+
+ // 回调放在锁外,这样 OnError 里再调 SendAsync 也不会死锁。
+ if err != nil {
+ c.reportErr(err)
+ }
+}
+
+// worker 是后台发送协程,按队列顺序逐批发,发完一批再取下一批。
+func (c *Client) worker() {
+ defer close(c.done)
+ for batch := range c.queue {
+ ctx, cancel := context.WithTimeout(context.Background(), asyncTimeout)
+ _, err := c.Send(ctx, batch...)
+ cancel()
+ if err != nil {
+ c.reportErr(err)
+ }
+ }
+}
+
+// Close 停止接收新的异步数据,并等队列里剩下的发完。
+//
+// 进程/任务退出前一定要调(defer 或任务末尾)。没调过 SendAsync 的话是空操作,
+// 重复调也安全。Close 之后还调 SendAsync 不会 panic,数据会被丢弃并回调 OnError。
+func (c *Client) Close() {
+ c.mu.Lock()
+ c.closed = true
+ if c.started {
+ close(c.queue)
+ }
+ done := c.done
+ c.mu.Unlock()
+
+ if done != nil {
+ <-done // 等 worker 把队列清空
+ }
+}
+
+// reportErr 把异步过程中的错误交给 OnError,没设置就打到 stderr。
+func (c *Client) reportErr(err error) {
+ if c.OnError != nil {
+ c.OnError(err)
+ return
+ }
+ log.Println(err)
+}
+
+// JSON 返回这批数据最终会发出去的 JSON(同样会补默认值),用来本地核对,
+// 不发任何请求。配合 Send 使用可以做到「先看一眼,再真发」。
+func JSON(records ...Record) ([]byte, error) {
+ payload := make([]Record, len(records))
+ copy(payload, records)
+ for i := range payload {
+ payload[i].fillDefaults()
+ }
+ return json.MarshalIndent(request{Records: payload}, "", " ")
+}
+
+// fillDefaults 补齐 SDK 负责的三个字段。显式填过的值不会被覆盖。
+func (r *Record) fillDefaults() {
+ if strings.TrimSpace(r.DataDate) == "" {
+ r.DataDate = time.Now().UTC().Format(DateLayout)
+ }
+ if strings.TrimSpace(r.Company) == "" {
+ r.Company = DefaultCompany
+ }
+ if r.RoleID == nil {
+ // 用空数组而不是 nil,避免 JSON 里出现 "role_id": null。
+ r.RoleID = []string{}
+ }
+}
+
+// validate 检查必填字段和日期格式,返回一条汇总的错误信息。
+func (r *Record) validate() error {
+ // 必填字段,顺序和《参数说明》表格一致。
+ required := []struct {
+ name string // 报错时显示的字段名,用 JSON 里的名字方便对照
+ value string
+ }{
+ {"company_platform", r.CompanyPlatform},
+ {"published_data", r.PublishedData},
+ {"script_name", r.ScriptName},
+ {"drama_name", r.DramaName},
+ {"drama_no", r.DramaNo},
+ {"video_url", r.VideoURL},
+ {"platform_url", r.PlatformURL},
+ }
+ var missing []string
+ for _, f := range required {
+ if strings.TrimSpace(f.value) == "" {
+ missing = append(missing, f.name)
+ }
+ }
+
+ // 从示例代码抄下来忘了改的占位符,也拦一下。
+ if strings.HasPrefix(r.VideoID, "REPLACE_") {
+ missing = append(missing, "video_id")
+ }
+
+ // 日期必须是 DD/MM/YYYY。写成 2026-09-21 这种会在这里被拦下。
+ for _, d := range []struct{ name, value string }{
+ {"data_date", r.DataDate},
+ {"published_data", r.PublishedData},
+ } {
+ if _, err := time.Parse(DateLayout, d.value); err != nil {
+ missing = append(missing, fmt.Sprintf("%s 日期格式应为 DD/MM/YYYY,当前为 %q", d.name, d.value))
+ }
+ }
+
+ if len(missing) == 0 {
+ return nil
+ }
+ return fmt.Errorf("缺少必填字段: %s", strings.Join(missing, ", "))
+}
diff --git a/pub/data_center/datacenter_test.go b/pub/data_center/datacenter_test.go
new file mode 100644
index 0000000..90fd519
--- /dev/null
+++ b/pub/data_center/datacenter_test.go
@@ -0,0 +1,150 @@
+package datacenter
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+)
+
+func sample() Record {
+ return Record{
+ VideoID: "12345",
+ CompanyPlatform: "黄果短剧",
+ PublishedData: "01/09/2026",
+ ScriptName: "剧本A",
+ DramaName: "短剧A",
+ DramaNo: "第1集",
+ VideoURL: "https://cdn.example.com/a.mp4",
+ PlatformURL: "https://example.com/a",
+ Views: 10000,
+ Clicks: 8200,
+ Likes: 300,
+ Comments: Int(0),
+ }
+}
+
+func TestFillDefaults(t *testing.T) {
+ r := sample()
+ r.fillDefaults()
+ if r.Company != DefaultCompany {
+ t.Fatalf("company = %q", r.Company)
+ }
+ if r.DataDate != time.Now().UTC().Format(DateLayout) {
+ t.Fatalf("data_date = %q", r.DataDate)
+ }
+ if r.RoleID == nil {
+ t.Fatal("role_id 应为空数组而不是 null")
+ }
+ // 显式传的日期不能被覆盖
+ r2 := sample()
+ r2.DataDate = "21/09/2026"
+ r2.fillDefaults()
+ if r2.DataDate != "21/09/2026" {
+ t.Fatalf("data_date 被覆盖为 %q", r2.DataDate)
+ }
+}
+
+func TestValidate(t *testing.T) {
+ s := sample()
+ if err := s.validate(); err != nil && !strings.Contains(err.Error(), "data_date") {
+ t.Fatalf("sample 不应报错: %v", err)
+ }
+ bad := sample()
+ bad.ScriptName = ""
+ bad.VideoURL = ""
+ if err := bad.validate(); err == nil ||
+ !strings.Contains(err.Error(), "script_name") ||
+ !strings.Contains(err.Error(), "video_url") {
+ t.Fatalf("应报缺 script_name/video_url: %v", err)
+ }
+ wrongDate := sample()
+ wrongDate.DataDate = "2026-09-21"
+ if err := wrongDate.validate(); err == nil || !strings.Contains(err.Error(), "DD/MM/YYYY") {
+ t.Fatalf("应报日期格式错误: %v", err)
+ }
+}
+
+func TestJSONShape(t *testing.T) {
+ b, err := JSON(sample())
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got struct {
+ Records []map[string]any `json:"records"`
+ }
+ if err := json.Unmarshal(b, &got); err != nil {
+ t.Fatal(err)
+ }
+ row := got.Records[0]
+ if row["data_date"] != time.Now().UTC().Format(DateLayout) {
+ t.Fatalf("data_date = %v", row["data_date"])
+ }
+ if row["company"] != DefaultCompany {
+ t.Fatalf("company = %v", row["company"])
+ }
+ if comments, ok := row["comments"].(float64); !ok || comments != 0 {
+ t.Fatalf("comments 应该发出 0,实际 %v", row["comments"])
+ }
+ if shares, ok := row["shares"]; ok {
+ t.Fatalf("shares 没填就不应出现,实际 %v", shares)
+ }
+}
+
+// TestSendAsync 验证异步上报不阻塞、按顺序发完,且 Close 会等队列清空。
+func TestSendAsync(t *testing.T) {
+ var mu sync.Mutex
+ var got []string // 收到的 video_id,按到达顺序
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ Records []struct {
+ VideoID string `json:"video_id"`
+ } `json:"records"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ t.Errorf("解码请求失败: %v", err)
+ }
+ mu.Lock()
+ for _, rec := range req.Records {
+ got = append(got, rec.VideoID)
+ }
+ mu.Unlock()
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer srv.Close()
+
+ c := New("ld_test")
+ c.Endpoint = srv.URL
+ c.OnError = func(err error) { t.Errorf("异步上报不应报错: %v", err) }
+
+ for _, id := range []string{"a", "b", "c"} {
+ r := sample()
+ r.VideoID = id
+ c.SendAsync(r)
+ }
+ c.Close() // 等发完
+
+ mu.Lock()
+ defer mu.Unlock()
+ if strings.Join(got, ",") != "a,b,c" {
+ t.Fatalf("发送顺序不对: %v", got)
+ }
+}
+
+// TestSendAsyncAfterClose 验证 Close 之后再上报不会 panic,而是走 OnError。
+func TestSendAsyncAfterClose(t *testing.T) {
+ c := New("ld_test")
+ var reported []error
+ c.OnError = func(err error) { reported = append(reported, err) }
+
+ c.Close() // 没调过 SendAsync,应该是空操作
+ c.SendAsync(sample())
+ c.Close() // 重复调也安全
+
+ if len(reported) != 1 || !strings.Contains(reported[0].Error(), "已 Close") {
+ t.Fatalf("应报一次「已 Close」: %v", reported)
+ }
+}
diff --git a/pub/data_center/example/main.go b/pub/data_center/example/main.go
new file mode 100644
index 0000000..5f54e70
--- /dev/null
+++ b/pub/data_center/example/main.go
@@ -0,0 +1,119 @@
+// 短剧数据回流 demo。
+//
+// 用法:
+//
+// go run ./example # 只看将发送的 JSON,不发请求
+// STUDIO_API_KEY='ld_xxx' go run ./example -send # 真正上报
+//
+// 接入时照着这个文件抄:改 records 里的数据,把 main 里的发送逻辑搬进你们自己的任务里。
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ datacenter "videoweb/pub/data_center"
+)
+
+// ============================================================
+// 1. 要上报的数据。一条 = 一个视频在某一天的表现。
+//
+// 不用填的(SDK 自动补):
+// - data_date 数据所属日期 → 当天 UTC 日期
+// - company 所属公司 → 瑞升
+// - role_id 角色 ID 数组 → []
+//
+// 必须填的(漏了会在本地就报错,不会白跑请求):
+// - company_platform 所属产品/项目名
+// - published_data 视频上线时间
+// - script_name 剧本名称
+// - drama_name 短剧名称
+// - drama_no 集数
+// - video_url 成片下载链接
+// - platform_url 产品内播放页链接
+//
+// 其余都是选填,不填就不会出现在请求体里。
+// ============================================================
+var records = []datacenter.Record{
+ {
+ // ---- 必填 ----
+ CompanyPlatform: "黄果短剧", // 你们的产品/项目名
+ PublishedData: "01/09/2026",
+ ScriptName: "剧本A",
+ DramaName: "短剧A",
+ DramaNo: "第1集",
+ VideoURL: "https://cdn.example.com/a.mp4",
+ PlatformURL: "https://example.com/a",
+
+ // ---- 选填,建议填:方便中台把同一条视频的历史数据串起来 ----
+ VideoID: "12345",
+ ScriptID: "S-001",
+ PlatformVideoID: "pv-12345",
+
+ // ---- 数据 ----
+ Views: 10000, // 浏览量
+ Clicks: 8200, // 播放量
+ Likes: 300, // 点赞量
+
+ // ---- 选填,不用就整行删掉 ----
+ // LikeRate: "3.66%", // 点赞量 ÷ 播放量 × 100%
+ // CompletionRate: "43.00%", // 完播率,没单条数据可填剧本整体完播率
+ // PaidRate: "0.00%", // 免费短剧统一 0.00%
+ // CompletionRateScope: "video", // video | script | unknown
+ // Comments: datacenter.Int(14), // 填 0 也会正常发出,删掉则不发这个字段
+ // Shares: datacenter.Int(0),
+ },
+ // 想一次发多条就继续往这里加,SDK 会打包成一个请求发出去。
+ // {
+ // CompanyPlatform: "黄果短剧",
+ // ...
+ // },
+}
+
+func main() {
+ send := flag.Bool("send", false, "真正 POST。默认只打印 JSON")
+ flag.Parse()
+
+ // ------------------------------------------------------------
+ // 第 1 步:本地核对。打印补齐默认值之后最终会发出的 JSON。
+ // ------------------------------------------------------------
+ body, err := datacenter.JSON(records...)
+ if err != nil {
+ fatal(err)
+ }
+ fmt.Println(string(body))
+
+ if !*send {
+ fmt.Println("\n未发送。确认无误后执行:")
+ fmt.Println(" STUDIO_API_KEY='ld_...' go run ./example -send")
+ return
+ }
+
+ // ------------------------------------------------------------
+ // 第 2 步:异步上报。
+ //
+ // SendAsync 把数据丢进内存队列就立即返回,HTTP 请求在后台发,
+ // 不会拖慢你们的主流程(接口 handler、定时任务都适用)。
+ //
+ // 两条铁律:
+ // 1. 失败不会返回给你,用 OnError 接住写日志。
+ // 2. 进程退出前必须 Close(),否则队列里的数据会丢。
+ // ------------------------------------------------------------
+ client := datacenter.New(os.Getenv("STUDIO_API_KEY")) // 创建一次,全程复用
+
+ client.OnError = func(err error) {
+ // 漏填字段、网络失败、队列满被丢弃,都会走到这里。
+ fmt.Fprintln(os.Stderr, "上报失败:", err)
+ }
+
+ client.SendAsync(records...) // 立即返回,不阻塞
+
+ client.Close() // 等队列里的数据发完再退出
+ fmt.Println("\n上报完成")
+}
+
+func fatal(err error) {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+}
diff --git a/pub/esmanager/client.go b/pub/esmanager/client.go
new file mode 100644
index 0000000..94883bf
--- /dev/null
+++ b/pub/esmanager/client.go
@@ -0,0 +1,205 @@
+package esmanager
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/elastic/go-elasticsearch/v7/esapi"
+)
+
+const (
+ AnalyzerIkSmart = "ik_smart"
+ AnalyzerIkMaxWord = "ik_max_word"
+ NumberOfShards = 1
+ NumberOfReplicas = 1
+)
+
+func (m *Manager) InitIndices(indexName string, setting M) error {
+ res, err := m.client.Indices.Create(indexName, func(request *esapi.IndicesCreateRequest) {
+ request.Body = marshalSearchM(setting)
+ })
+ defer res.Body.Close()
+ return err
+}
+
+// GetTokens 获取分词
+func (m *Manager) Tokens(index string, bind interface{}, query M, o ...func(*esapi.IndicesAnalyzeRequest)) error {
+ o = append(o,
+ m.client.Indices.Analyze.WithContext(context.Background()),
+ m.client.Indices.Analyze.WithIndex(index),
+ m.client.Indices.Analyze.WithBody(marshalSearchM(query)),
+ m.client.Indices.Analyze.WithPretty(),
+ )
+
+ res, err := m.client.Indices.Analyze(o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ err = unmarshalAnalyzeBody(bind, res)
+ return err
+}
+
+// search 搜索
+func (m *Manager) Search(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) error {
+ marshalSearchM(query)
+ o = append(o,
+ m.client.Search.WithContext(context.Background()),
+ m.client.Search.WithIndex(index),
+ m.client.Search.WithBody(marshalSearchM(query)),
+ m.client.Search.WithTrackTotalHits(true),
+ m.client.Search.WithPretty(),
+ )
+ res, err := m.client.Search(o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ err = unmarshalBody(bind, res)
+ return err
+}
+
+// 批量插入
+func (m *Manager) Bulk(index string, source M, o ...func(*esapi.BulkRequest)) error {
+ o = append(o,
+ m.client.Bulk.WithIndex(index),
+ m.client.Bulk.WithContext(context.Background()),
+ )
+ res, err := m.client.Bulk(marshalBulkM(source), o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", res))
+ }
+ var bulkResp struct {
+ Errors bool `json:"errors"`
+ Items []map[string]struct {
+ Status int `json:"status"`
+ Error interface{} `json:"error"`
+ } `json:"items"`
+ }
+ if err := json.NewDecoder(res.Body).Decode(&bulkResp); err != nil {
+ return err
+ }
+ if bulkResp.Errors {
+ errs := make([]string, 0, 3)
+ for _, item := range bulkResp.Items {
+ for op, result := range item {
+ if result.Status >= http.StatusBadRequest {
+ errs = append(errs, fmt.Sprintf("%s status=%d error=%v", op, result.Status, result.Error))
+ if len(errs) >= 3 {
+ return errors.New(fmt.Sprintf("elastic bulk item errors: %v", errs))
+ }
+ }
+ }
+ }
+ return errors.New(fmt.Sprintf("elastic bulk item errors: %v", errs))
+ }
+ return nil
+}
+
+func (m *Manager) Refresh(index string, o ...func(*esapi.IndicesRefreshRequest)) error {
+ o = append(o,
+ m.client.Indices.Refresh.WithContext(context.Background()),
+ m.client.Indices.Refresh.WithIndex(index),
+ )
+ res, err := m.client.Indices.Refresh(o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode >= http.StatusBadRequest {
+ return errors.New(fmt.Sprintf("%+v", res))
+ }
+ return nil
+}
+
+// Count 获取数量
+func (m *Manager) Count(index string, query M, o ...func(*esapi.CountRequest)) (int64, error) {
+ o = append(o,
+ m.client.Count.WithIndex(index),
+ m.client.Count.WithBody(marshalSearchM(query)),
+ m.client.Count.WithContext(context.Background()),
+ )
+ res, err := m.client.Count(o...)
+ if err != nil {
+ return 0, err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return 0, errors.New(fmt.Sprintf("%+v", res))
+ }
+ type Bind struct {
+ Count int64 `json:"count"`
+ }
+ var bind = Bind{}
+ err = unmarshalStruct(&bind, res)
+ if err != nil {
+ return 0, err
+ }
+ return bind.Count, err
+}
+
+func (m *Manager) Get(index string, id string, bind interface{}, o ...func(*esapi.GetRequest)) error {
+ o = append(o,
+ m.client.Get.WithContext(context.Background()),
+ m.client.Get.WithPretty(),
+ )
+ res, err := m.client.Get(index, id, o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", res))
+ }
+ return unmarshalBody(bind, res)
+}
+
+func (m *Manager) Add(index string, id string, doc interface{}, o ...func(*esapi.IndexRequest)) error {
+ o = append(o,
+ m.client.Index.WithDocumentID(id),
+ m.client.Index.WithContext(context.Background()),
+ m.client.Index.WithPretty(),
+ m.client.Index.WithRefresh("true"),
+ )
+ res, err := m.client.Index(index, marshalStruct(doc), o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ return nil
+}
+
+func (m *Manager) Del(index string, id string, o ...func(*esapi.DeleteRequest)) error {
+ o = append(o,
+ m.client.Delete.WithContext(context.Background()),
+ m.client.Delete.WithPretty(),
+ m.client.Delete.WithRefresh("true"),
+ )
+ res, err := m.client.Delete(index, id, o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ return nil
+}
+
+func (m *Manager) DelMany(indexs []string, query M, o ...func(*esapi.DeleteByQueryRequest)) error {
+ o = append(o,
+ m.client.DeleteByQuery.WithContext(context.Background()),
+ m.client.DeleteByQuery.WithPretty(),
+ m.client.DeleteByQuery.WithRefresh(true),
+ )
+ res, err := m.client.DeleteByQuery(indexs, marshalSearchM(query), o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ return nil
+}
diff --git a/pub/esmanager/elastic.go b/pub/esmanager/elastic.go
new file mode 100644
index 0000000..ba00bbd
--- /dev/null
+++ b/pub/esmanager/elastic.go
@@ -0,0 +1,143 @@
+package esmanager
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/elastic/go-elasticsearch/v7/esapi"
+)
+
+type M map[string]interface{}
+
+type A []map[string]interface{}
+
+type MGetBody struct {
+ ID string `json:"_id"`
+}
+
+// ElasticResp 响应
+type Response = esapi.Response
+
+type Options struct {
+ Address []string
+ MaxIdleConnPerHost int
+ IdleConnTimeout time.Duration
+}
+
+func unmarshalAggregateBody(bind interface{}, resp *Response) error {
+ var v map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return err
+ }
+ if v != nil && len(v) > 0 {
+ if v, ok := v["aggregations"].(map[string]interface{}); ok {
+ b, _ := json.Marshal(v)
+ _ = json.Unmarshal(b, &bind)
+ }
+ }
+ return nil
+}
+
+func unmarshalAnalyzeBody(bind interface{}, resp *Response) error {
+ var v map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return err
+ }
+ if v != nil && len(v) > 0 {
+ b, _ := json.Marshal(v)
+ _ = json.Unmarshal(b, &bind)
+ }
+ return nil
+}
+
+func unmarshalBody(bind interface{}, resp *Response) error {
+ var data map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&data)
+ if err != nil {
+ return err
+ }
+ if data != nil && len(data) > 0 {
+ if hits1, ok := data["hits"].(map[string]interface{}); ok {
+ b, _ := json.Marshal(hits1)
+ _ = json.Unmarshal(b, &bind)
+ } else if _, ok := data["docs"].([]interface{}); ok {
+ b, _ := json.Marshal(data["docs"])
+ _ = json.Unmarshal(b, &bind)
+ } else if _, ok := data["_source"].(map[string]interface{}); ok {
+ b, _ := json.Marshal(data)
+ _ = json.Unmarshal(b, &bind)
+ }
+ }
+ return nil
+}
+
+func marshalSearchM(query M) io.Reader {
+ var buf bytes.Buffer
+ if query == nil || len(query) == 0 {
+ return nil
+ }
+ err := json.NewEncoder(&buf).Encode(query)
+ if err != nil {
+ return nil
+ }
+ return bytes.NewReader(buf.Bytes())
+}
+
+func marshalBulkM(source M) io.Reader {
+ var buf bytes.Buffer
+ for k, v := range source {
+ var meta = []byte(fmt.Sprintf(`{ "index" : { "_id" : "%s" } }%s`, k, "\n"))
+ var data, _ = json.Marshal(v)
+ data = append(data, "\n"...)
+ buf.Grow(len(meta) + len(data))
+ buf.Write(meta)
+ buf.Write(data)
+ }
+ return bytes.NewReader(buf.Bytes())
+}
+
+func marshalStruct(v interface{}) io.Reader {
+ var buf bytes.Buffer
+ if v == nil {
+ return nil
+ }
+ err := json.NewEncoder(&buf).Encode(v)
+ if err != nil {
+ return nil
+ }
+ return bytes.NewReader(buf.Bytes())
+}
+
+func unmarshalStruct(bind interface{}, resp *Response) error {
+ var v map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return err
+ }
+ if v != nil && len(v) > 0 {
+ b, _ := json.Marshal(v)
+ _ = json.Unmarshal(b, &bind)
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/esmanager/manager.go b/pub/esmanager/manager.go
new file mode 100644
index 0000000..0f58f4e
--- /dev/null
+++ b/pub/esmanager/manager.go
@@ -0,0 +1,53 @@
+package esmanager
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/elastic/go-elasticsearch/v7"
+)
+
+var Instance *Manager
+
+type Manager struct {
+ client *elasticsearch.Client
+ conf *elasticsearch.Config
+}
+
+func NewManager(url string, user string, pw string) (*Manager, error) {
+ if url == "" {
+ return nil, errors.New(fmt.Sprintf("Elastic NewManager url is empty!"))
+ }
+ cfg := elasticsearch.Config{
+ Addresses: []string{url},
+ Transport: &http.Transport{
+ MaxIdleConnsPerHost: 10,
+ IdleConnTimeout: 30,
+ },
+ Username: user,
+ Password: pw,
+ }
+ return &Manager{conf: &cfg}, nil
+}
+
+func (m *Manager) Client() *elasticsearch.Client {
+ return m.client
+}
+
+func (m *Manager) ClientConnect() error {
+ c, err := elasticsearch.NewClient(*m.conf)
+ if err != nil {
+ return err
+ }
+ resp, err := c.Ping(c.Ping.WithContext(context.Background()))
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", resp))
+ }
+ m.client = c
+ return nil
+}
\ No newline at end of file
diff --git a/pub/google/googleAuth.go b/pub/google/googleAuth.go
new file mode 100644
index 0000000..1ba566d
--- /dev/null
+++ b/pub/google/googleAuth.go
@@ -0,0 +1,92 @@
+package google
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base32"
+ "encoding/binary"
+ "fmt"
+ "strings"
+ "time"
+)
+
+type GoogleAuth struct {
+}
+
+func NewGoogleAuth() *GoogleAuth {
+ return &GoogleAuth{}
+}
+
+func (ga *GoogleAuth) un() int64 {
+ return time.Now().UnixNano() / 1000 / 30
+}
+
+func (ga *GoogleAuth) hmacSha1(key, data []byte) []byte {
+ h := hmac.New(sha1.New, key)
+ if total := len(data); total > 0 {
+ h.Write(data)
+ }
+ return h.Sum(nil)
+}
+
+func (ga *GoogleAuth) base32encode(src []byte) string {
+ return base32.StdEncoding.EncodeToString(src)
+}
+
+func (ga *GoogleAuth) base32decode(s string) ([]byte, error) {
+ return base32.StdEncoding.DecodeString(s)
+}
+
+func (ga *GoogleAuth) toBytes(value int64) []byte {
+ var result []byte
+ mask := int64(0xFF)
+ shifts := [8]uint16{56, 48, 40, 32, 24, 16, 8, 0}
+ for _, shift := range shifts {
+ result = append(result, byte((value>>shift)&mask))
+ }
+ return result
+}
+
+func (ga *GoogleAuth) toUint32(bts []byte) uint32 {
+ return (uint32(bts[0]) << 24) + (uint32(bts[1]) << 16) +
+ (uint32(bts[2]) << 8) + uint32(bts[3])
+}
+
+func (ga *GoogleAuth) oneTimePassword(key []byte, data []byte) uint32 {
+ hash := ga.hmacSha1(key, data)
+ offset := hash[len(hash)-1] & 0x0F
+ hashParts := hash[offset : offset+4]
+ hashParts[0] = hashParts[0] & 0x7F
+ number := ga.toUint32(hashParts)
+ return number % 1000000
+}
+
+func (ga *GoogleAuth) GetSecret() string {
+ var buf bytes.Buffer
+ binary.Write(&buf, binary.BigEndian, ga.un())
+ return strings.ToUpper(ga.base32encode(ga.hmacSha1(buf.Bytes(), nil)))
+}
+
+func (ga *GoogleAuth) GetCode(secret string) (string, error) {
+ secretUpper := strings.ToUpper(secret)
+ secretKey, err := ga.base32decode(secretUpper)
+ if err != nil {
+ return "", err
+ }
+ number := ga.oneTimePassword(secretKey, ga.toBytes(time.Now().Unix()/30))
+ return fmt.Sprintf("%06d", number), nil
+}
+
+func (ga *GoogleAuth) GetQrcode(user, secret string) string {
+ return fmt.Sprintf("otpauth://totp/%s?secret=%s", user, secret)
+}
+
+func (ga *GoogleAuth) VerifyCode(secret, code string) (bool, error) {
+ _code, err := ga.GetCode(secret)
+ fmt.Println(_code, code, err)
+ if err != nil {
+ return false, err
+ }
+ return _code == code, nil
+}
diff --git a/pub/models/actress/actress.go b/pub/models/actress/actress.go
new file mode 100644
index 0000000..f88ed9e
--- /dev/null
+++ b/pub/models/actress/actress.go
@@ -0,0 +1,181 @@
+package actress
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.Actress)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Alias, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Alias, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Rank, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.UpdatedAt, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Works, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Works, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.HotValue, Value: -1},
+ {Key: FN.Actress.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Real.WeekWatchs, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Real.MonthWatchs, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Actress.Status, Value: 1},
+ {Key: FN.Actress.Real.WatchTimes, Value: 1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initActressIndex) Create Actress Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneActress 插入一个导航分类
+func InsertOneActress(ctx context.Context, qa *Actress) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ log.Warn("Actressmod:InsertOneActress db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneActress 删除一个导航分类
+func DelOneActress(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("Actressmod:DelOneActress db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneActress 查找一个导航分类
+func FindOneActress(ctx context.Context, filter bson.M) (
+ *Actress, *srvdef.SrvErr) {
+ var Actress Actress
+ if err := db.FindOneEx(ctx, &Actress, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("Actressmod:FindOneActress db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &Actress, nil
+}
+
+func FindManyActresss(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (ActressList, *srvdef.SrvErr) {
+ data := make(ActressList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyActresss", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountActresss 统计导航分类数量
+func CountActresss(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountActresss db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneActress 更新一个导航分类
+func UpdateOneActress(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneActress db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneActress db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+func UpdateManyActress(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyActress db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/actress/tag.go b/pub/models/actress/tag.go
new file mode 100644
index 0000000..8a55d80
--- /dev/null
+++ b/pub/models/actress/tag.go
@@ -0,0 +1,96 @@
+package actress
+
+type TagActress struct {
+ Meas struct {
+ Gender string // Type: int , Desc: 性别 0:女 1:男 2:其他
+ Height string // Type: int , Desc: 身高 厘米
+ Weight string // Type: int , Desc: 体重 公斤
+ Bust string // Type: int , Desc: 胸围 英寸
+ Waist string // Type: int , Desc: 腰围 英寸
+ Hip string // Type: int , Desc: 臀围 英寸
+ Cup string // Type: int , Desc: 罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I
+ Len string // Type: int , Desc: 长度 毫米(前端可以显示为厘米,如45毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)
+ Dia string // Type: int , Desc: 直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)
+ Blood string // Type: string , Desc: 血型
+ }
+ Id string
+ Alias string
+ Slug string
+ Avatar string
+ Cover string
+ Desc string
+ TranslationCategoryId string
+ Office string
+ DebutDate string
+ BornDate string
+ Region string
+ Score string
+ Rank string
+ HotValue string
+ Status string
+ Works string
+ CreatedAt string
+ UpdatedAt string
+ RealUpdateAt string
+ SeoInfo string
+ IndexNow string
+ Real struct {
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ Likes string
+ WeekWatchs string
+ MonthWatchs string
+ TodayWatchs string
+ }
+}
+
+var FN struct {
+ Actress TagActress
+}
+var TN struct {
+ Actress string
+}
+
+func init() {
+ FN.Actress.Id = "_id"
+ FN.Actress.UpdatedAt = "updatedAt"
+ FN.Actress.Works = "works"
+ FN.Actress.Status = "status"
+ FN.Actress.Rank = "rank"
+ FN.Actress.HotValue = "hotValue"
+ FN.Actress.Score = "score"
+ FN.Actress.Region = "region"
+ FN.Actress.BornDate = "bornDate"
+ FN.Actress.DebutDate = "debutDate"
+ FN.Actress.Office = "office"
+ FN.Actress.Meas.Gender = "meas.gender"
+ FN.Actress.Meas.Height = "meas.height"
+ FN.Actress.Meas.Weight = "meas.weight"
+ FN.Actress.Meas.Bust = "meas.bust"
+ FN.Actress.Meas.Waist = "meas.waist"
+ FN.Actress.Meas.Hip = "meas.hip"
+ FN.Actress.Meas.Cup = "meas.cup"
+ FN.Actress.Meas.Len = "meas.len"
+ FN.Actress.Meas.Dia = "meas.dia"
+ FN.Actress.Meas.Blood = "meas.blood"
+ FN.Actress.Desc = "desc"
+ FN.Actress.Cover = "cover"
+ FN.Actress.Avatar = "avatar"
+ FN.Actress.Alias = "alias"
+ FN.Actress.Slug = "slug"
+ FN.Actress.CreatedAt = "createdAt"
+ FN.Actress.UpdatedAt = "updatedAt"
+ FN.Actress.TranslationCategoryId = "translationCategoryId"
+ FN.Actress.Real.WatchTimes = "watchTimes"
+ FN.Actress.Real.ShowTimes = "showTimes"
+ FN.Actress.Real.Comments = "comments"
+ FN.Actress.Real.Likes = "likes"
+ FN.Actress.Real.WeekWatchs = "weekWatchs"
+ FN.Actress.Real.MonthWatchs = "monthWatchs"
+ FN.Actress.Real.TodayWatchs = "todayWatchs"
+ FN.Actress.SeoInfo = "seoInfo"
+ FN.Actress.IndexNow = "indexNow"
+ FN.Actress.RealUpdateAt = "realUpdateAt"
+ TN.Actress = "actress"
+}
diff --git a/pub/models/actress/type.go b/pub/models/actress/type.go
new file mode 100644
index 0000000..0a0e036
--- /dev/null
+++ b/pub/models/actress/type.go
@@ -0,0 +1,172 @@
+// @title:女优管理
+package actress
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+type Meas struct {
+ Gender int `bson:"gender" json:"gender"` // 性别 0:女 1:男 2:其他
+ Height int `bson:"height" json:"height"` // 身高 厘米
+ Weight int `bson:"weight" json:"weight"` // 体重 公斤
+ Bust int `bson:"bust" json:"bust"` // 胸围 英寸
+ Waist int `bson:"waist" json:"waist"` // 腰围 英寸
+ Hip int `bson:"hip" json:"hip"` // 臀围 英寸
+ Cup int `bson:"cup" json:"cup"` // 罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I
+ Len int `bson:"len" json:"len"` // 长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)
+ Dia int `bson:"dia" json:"dia"` // 直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)
+ Blood string `json:"blood" bson:"blood"` // 血型
+}
+
+func calculateCup(bust int, waist int) int {
+ // 计算胸围和腰围的差值
+ difference := bust - waist
+ // 根据差值大致判断罩杯
+ switch {
+ case difference < 5:
+ return 1 // A杯
+ case difference < 7:
+ return 2 // B杯
+ case difference < 9:
+ return 3 // C杯
+ case difference < 11:
+ return 4 // D杯
+ case difference < 13:
+ return 5 // E杯
+ case difference < 15:
+ return 6 // F杯
+ case difference < 17:
+ return 7 // G杯
+ case difference < 19:
+ return 8 // H杯
+ case difference < 21:
+ return 9 // I杯
+ default:
+ return 0
+ }
+}
+
+func (m *Meas) Parse(text string) {
+ text = strings.ReplaceAll(text, "长度", "尺寸")
+ text = strings.ReplaceAll(text, "長度", "尺寸")
+ text = strings.ReplaceAll(text, "厘米", "cm")
+ text = strings.ReplaceAll(text, "公分", "cm")
+ text = strings.ReplaceAll(text, "公斤", "kg")
+ text = strings.ReplaceAll(text, "體", "体")
+ text = strings.ReplaceAll(text, "圍", "围")
+ text = strings.ReplaceAll(text, "徑", "径")
+ // 将字母转换为对应的数字
+ cupMap := map[string]int{
+ "A": 1, "B": 2, "C": 3, "D": 4,
+ "E": 5, "F": 6, "G": 7, "H": 8, "I": 9,
+ "a": 1, "b": 2, "c": 3, "d": 4,
+ "e": 5, "f": 6, "g": 7, "h": 8, "i": 9,
+ }
+ // 身高提取cm
+ heightReg := regexp.MustCompile(`(?i)身高\s*(\d+)cm`)
+ if matches := heightReg.FindStringSubmatch(text); len(matches) > 1 {
+ m.Height, _ = strconv.Atoi(matches[1])
+ }
+ // 体重提取kg
+ weightReg := regexp.MustCompile(`(?i)体重\s*(\d+)kg`)
+ if matches := weightReg.FindStringSubmatch(text); len(matches) > 1 {
+ m.Weight, _ = strconv.Atoi(matches[1])
+ }
+ // 体重提取斤
+ weightReg = regexp.MustCompile(`(?i)体重\s*(\d+)斤`)
+ if matches := weightReg.FindStringSubmatch(text); len(matches) > 1 {
+ m.Weight, _ = strconv.Atoi(matches[1])
+ m.Weight = m.Weight / 2
+ }
+ // 胸围提取
+ bustReg := regexp.MustCompile(`胸围\s*(\d+)`)
+ if matches := bustReg.FindStringSubmatch(text); len(matches) > 1 {
+ m.Bust, _ = strconv.Atoi(matches[1])
+ }
+ // 三围单个数值提取
+ bustReg = regexp.MustCompile(`三围\s*(\d+)`)
+ if matches := bustReg.FindStringSubmatch(text); len(matches) > 1 {
+ m.Bust, _ = strconv.Atoi(matches[1])
+ }
+ // 罩杯提取
+ cupReg := regexp.MustCompile(`(?i)(\d+)([A-Z])`)
+ if matches := cupReg.FindStringSubmatch(text); len(matches) > 2 {
+ m.Cup = cupMap[matches[2]]
+ }
+ // 长度提取cm,支持浮点数
+ lengthReg := regexp.MustCompile(`(?i)尺寸\s*(\d+\.?\d*)cm`)
+ if matches := lengthReg.FindStringSubmatch(text); len(matches) > 1 {
+ value, _ := strconv.ParseFloat(matches[1], 64)
+ m.Len = int(value * 10) // 转换为mm
+ }
+ // 直径提取cm,支持浮点数
+ diaReg := regexp.MustCompile(`(?i)直径\s*(\d+\.?\d*)cm`)
+ if matches := diaReg.FindStringSubmatch(text); len(matches) > 1 {
+ value, _ := strconv.ParseFloat(matches[1], 64)
+ m.Len = int(value * 10) // 转换为mm
+ }
+ // 提取三围和罩杯
+ threeSizeReg := regexp.MustCompile(`(?i)三围\s*(\d+)([A-Z])/(\d+)/(\d+)`)
+ if matches := threeSizeReg.FindStringSubmatch(text); len(matches) > 1 {
+ m.Bust, _ = strconv.Atoi(matches[1])
+ m.Cup = cupMap[matches[2]]
+ m.Waist, _ = strconv.Atoi(matches[3])
+ m.Hip, _ = strconv.Atoi(matches[4])
+ }
+}
+
+func (m *Meas) convertCupToString(cup int) string {
+ if cup <= 0 {
+ cup = calculateCup(m.Bust, m.Waist)
+ }
+ if cup >= 1 && cup <= 9 {
+ letter := rune('A' + cup - 1)
+ return string(letter)
+ } else {
+ if m.Gender == 1 {
+ return "" // 男性默认返回空字符串
+ }
+ return "B" // 非男性默认返回B杯
+ }
+}
+
+func (m *Meas) Measurements() string {
+ //三围 格式为 35D-24-34 // 胸围(罩杯)-腰围-臀围
+ return fmt.Sprintf("%d%s-%d-%d", m.Bust, m.convertCupToString(m.Cup), m.Waist, m.Hip)
+}
+
+// @title:女优
+// @table:actress
+type Actress struct {
+ Id int64 `json:"id" bson:"_id"` // Id
+ Alias string `bson:"alias" json:"alias"` // 别名
+ Slug string `json:"slug" bson:"slug"` // slug
+ Avatar string `bson:"avatar" json:"avatar"` // 头像
+ Cover string `bson:"cover" json:"cover"` // 封面大图
+ Desc string `bson:"desc" json:"desc"` // 详细描述
+ TranslationCategoryId int64 `bson:"translationCategoryId" json:"translationCategoryId"` // 关联导航分类id
+ Meas Meas `bson:"meas" json:"meas"` // 三围
+ Office string `json:"office" bson:"office"` // 事务所
+ DebutDate time.Time `bson:"debutDate" json:"debutDate" ` // 出道日期
+ BornDate time.Time `bson:"bornDate" json:"bornDate"` // 出生日期
+ Region string `json:"region" bson:"region"` // 地区/国家
+ Score string `json:"score" bson:"score"` // 评分
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ HotValue int64 `json:"hotValue" bson:"hotValue"` // 热度值
+ Status bool `json:"status" bson:"status"` // 是否启用
+ Works int64 `json:"works" bson:"works"` // 作品数
+ Real pubdef.Flow `json:"real" bson:"real"` // 真实流量
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ RealUpdateAt time.Time `json:"realUpdateAt" bson:"realUpdateAt"` // 播放相关数据更新时间
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+
+}
+
+type ActressList []*Actress
diff --git a/pub/models/analyticsdailystat/analytics.go b/pub/models/analyticsdailystat/analytics.go
new file mode 100644
index 0000000..48a0a5a
--- /dev/null
+++ b/pub/models/analyticsdailystat/analytics.go
@@ -0,0 +1,182 @@
+package analyticsdailystat
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.AnalyticsDailyInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsDailyInfo.AppId, Value: 1},
+ {Key: FN.AnalyticsDailyInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsDailyInfo.SumDate, Value: 1},
+ {Key: FN.AnalyticsDailyInfo.AppId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsDailyInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(AnalyticsDailyInfo:initAnalyticsDailyInfoIndex) Create AnalyticsDailyInfo Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneAnalyticsDailyInfo 插入一个评论
+func InsertOneAnalyticsDailyInfo(ctx context.Context, AnalyticsDailyInfo *AnalyticsDailyInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, AnalyticsDailyInfo); err != nil {
+ log.Warn("AnalyticsDailyInfo:InsertOneAnalyticsDailyInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneAnalyticsDailyInfo 删除一个评论
+func DelOneAnalyticsDailyInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:DelOneAnalyticsDailyInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyAnalyticsDailyInfo 删除一个评论评论
+func DelManyAnalyticsDailyInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:DelOneAnalyticsDailyInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneAnalyticsDailyInfo 查找一个评论
+func FindOneAnalyticsDailyInfo(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *AnalyticsDailyInfo, *srvdef.SrvErr) {
+ var AnalyticsDailyInfo AnalyticsDailyInfo
+ if err := db.FindOneEx(
+ ctx, &AnalyticsDailyInfo, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("AnalyticsDailyInfo:FindOneAnalyticsDailyInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &AnalyticsDailyInfo, nil
+}
+
+// CountAnalyticsDailyInfos 统计评论视频评论数量
+func CountAnalyticsDailyInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:CountAnalyticsDailyInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneAnalyticsDailyInfo 更新一个评论
+func UpdateOneAnalyticsDailyInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:UpdateOneAnalyticsDailyInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("AnalyticsDailyInfo:UpdateOneAnalyticsDailyInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyAnalyticsDailyInfo 更新一批评论
+func UpdateManyAnalyticsDailyInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:UpdateManyAnalyticsDailyInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("AnalyticsDailyInfo:UpdateManyAnalyticsDailyInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrAnalyticsDailyInfoNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyAnalyticsDailyInfo 查找一批评论
+func FindManyAnalyticsDailyInfo(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (AnalyticsDailyInfoList, *srvdef.SrvErr) {
+ data := make(AnalyticsDailyInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("AnalyticsDailyInfo:FindManyAnalyticsDailyInfo db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyAnalyticsDailyInfo 插入一批用户
+func InsertManyAnalyticsDailyInfo(ctx context.Context, histrory *AnalyticsDailyInfoList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("AnalyticsDailyInfo:InsertManyAnalyticsDailyInfo db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateAnalyticsDailyInfo 聚合查找一批数据
+func AggregateAnalyticsDailyInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsDailyInfo:AggregateAnalyticsDailyInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateAnalyticsDailyInfo 聚合查找一批数据
+func ObjectAggregateAnalyticsDailyInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsDailyInfo:AggregateAnalyticsDailyInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/analyticsdailystat/tag.go b/pub/models/analyticsdailystat/tag.go
new file mode 100755
index 0000000..d5712a8
--- /dev/null
+++ b/pub/models/analyticsdailystat/tag.go
@@ -0,0 +1,42 @@
+package analyticsdailystat
+
+type TagAnalyticsDailyInfo struct {
+ AppId string
+ AppName string
+ CreatedAt string
+ Id string
+ LeadAmount string
+ LeadNewCount string
+ SumDate string
+ UpdatedAt string
+ YestodayActiveUsers string
+ YestodayNewUsers string
+ YestodayPV string
+ YestodaySessions string
+ YestodayUV string
+}
+
+var FN struct {
+ AnalyticsDailyInfo TagAnalyticsDailyInfo
+}
+
+var TN struct {
+ AnalyticsDailyInfo string
+}
+
+func init() {
+ FN.AnalyticsDailyInfo.AppId = "appId"
+ FN.AnalyticsDailyInfo.AppName = "appName"
+ FN.AnalyticsDailyInfo.CreatedAt = "createdAt"
+ FN.AnalyticsDailyInfo.Id = "_id"
+ FN.AnalyticsDailyInfo.LeadAmount = "leadAmount"
+ FN.AnalyticsDailyInfo.LeadNewCount = "leadNewCount"
+ FN.AnalyticsDailyInfo.SumDate = "sumDate"
+ FN.AnalyticsDailyInfo.UpdatedAt = "updatedAt"
+ FN.AnalyticsDailyInfo.YestodayActiveUsers = "yestodayActiveUsers"
+ FN.AnalyticsDailyInfo.YestodayNewUsers = "yestodayNewUsers"
+ FN.AnalyticsDailyInfo.YestodayPV = "yestodayPV"
+ FN.AnalyticsDailyInfo.YestodaySessions = "yestodaySessions"
+ FN.AnalyticsDailyInfo.YestodayUV = "yestodayUV"
+ TN.AnalyticsDailyInfo = "analytics_daily_info"
+}
\ No newline at end of file
diff --git a/pub/models/analyticsdailystat/type.go b/pub/models/analyticsdailystat/type.go
new file mode 100644
index 0000000..9fa54f7
--- /dev/null
+++ b/pub/models/analyticsdailystat/type.go
@@ -0,0 +1,27 @@
+package analyticsdailystat
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 数据统计
+// @table:analytics_daily_info
+type AnalyticsDailyInfo struct {
+ Id primitive.ObjectID `bson:"_id,omitempty"`
+ SumDate time.Time `json:"sumDate" bson:"sumDate"` // 时间半小时
+ AppId int64 `json:"appId" bson:"appId"` // appId
+ AppName string `json:"appName" bson:"appName"` // APP名称
+ YestodayPV int64 `json:"yestodayPV" bson:"yestodayPV"` // 页面浏览量 (PV)
+ YestodayUV int64 `json:"yestodayUV" bson:"yestodayUV"` // 独立用户数 (UV)
+ YestodayActiveUsers int64 `json:"yestodayActiveUsers" bson:"yestodayActiveUsers"` // 活跃用户数
+ YestodaySessions int64 `json:"yestodaySessions" bson:"yestodaySessions"` // 会话数
+ YestodayNewUsers int64 `json:"yestodayNewUsers" bson:"yestodayNewUsers"` // 新增数量
+ LeadNewCount float64 `json:"leadNewCount" bson:"leadNewCount"` // 日导量新增
+ LeadAmount float64 `json:"leadAmount" bson:"leadAmount"` // 日导量充值
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type AnalyticsDailyInfoList []*AnalyticsDailyInfo
\ No newline at end of file
diff --git a/pub/models/analyticsstat/analytics.go b/pub/models/analyticsstat/analytics.go
new file mode 100644
index 0000000..cf82160
--- /dev/null
+++ b/pub/models/analyticsstat/analytics.go
@@ -0,0 +1,182 @@
+package analyticsstat
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.AnalyticsInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsInfo.AppId, Value: 1},
+ {Key: FN.AnalyticsInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsInfo.SumDate, Value: 1},
+ {Key: FN.AnalyticsInfo.AppId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(AnalyticsInfo:initAnalyticsInfoIndex) Create AnalyticsInfo Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneAnalyticsInfo 插入一个评论
+func InsertOneAnalyticsInfo(ctx context.Context, AnalyticsInfo *AnalyticsInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, AnalyticsInfo); err != nil {
+ log.Warn("AnalyticsInfo:InsertOneAnalyticsInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneAnalyticsInfo 删除一个评论
+func DelOneAnalyticsInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsInfo:DelOneAnalyticsInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyAnalyticsInfo 删除一个评论评论
+func DelManyAnalyticsInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsInfo:DelOneAnalyticsInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneAnalyticsInfo 查找一个评论
+func FindOneAnalyticsInfo(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *AnalyticsInfo, *srvdef.SrvErr) {
+ var AnalyticsInfo AnalyticsInfo
+ if err := db.FindOneEx(
+ ctx, &AnalyticsInfo, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("AnalyticsInfo:FindOneAnalyticsInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &AnalyticsInfo, nil
+}
+
+// CountAnalyticsInfos 统计评论视频评论数量
+func CountAnalyticsInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsInfo:CountAnalyticsInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneAnalyticsInfo 更新一个评论
+func UpdateOneAnalyticsInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsInfo:UpdateOneAnalyticsInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("AnalyticsInfo:UpdateOneAnalyticsInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyAnalyticsInfo 更新一批评论
+func UpdateManyAnalyticsInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsInfo:UpdateManyAnalyticsInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("AnalyticsInfo:UpdateManyAnalyticsInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrAnalyticsInfoNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyAnalyticsInfo 查找一批评论
+func FindManyAnalyticsInfo(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (AnalyticsInfoList, *srvdef.SrvErr) {
+ data := make(AnalyticsInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("AnalyticsInfo:FindManyAnalyticsInfo db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyAnalyticsInfo 插入一批用户
+func InsertManyAnalyticsInfo(ctx context.Context, histrory *AnalyticsInfoList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("AnalyticsInfo:InsertManyAnalyticsInfo db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateAnalyticsInfo 聚合查找一批数据
+func AggregateAnalyticsInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsInfo:AggregateAnalyticsInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateAnalyticsInfo 聚合查找一批数据
+func ObjectAggregateAnalyticsInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsInfo:AggregateAnalyticsInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/analyticsstat/tag.go b/pub/models/analyticsstat/tag.go
new file mode 100755
index 0000000..d386cda
--- /dev/null
+++ b/pub/models/analyticsstat/tag.go
@@ -0,0 +1,42 @@
+package analyticsstat
+
+type TagAnalyticsInfo struct {
+ ActiveUsers string
+ AppId string
+ AppName string
+ CreatedAt string
+ DaySumDate string
+ Id string
+ SumDate string
+ UpdatedAt string
+ YestodayActiveUsers string
+ YestodayNewUsers string
+ YestodayPV string
+ YestodaySessions string
+ YestodayUV string
+}
+
+var FN struct {
+ AnalyticsInfo TagAnalyticsInfo
+}
+
+var TN struct {
+ AnalyticsInfo string
+}
+
+func init() {
+ FN.AnalyticsInfo.ActiveUsers = "activeUsers"
+ FN.AnalyticsInfo.AppId = "appId"
+ FN.AnalyticsInfo.AppName = "appName"
+ FN.AnalyticsInfo.CreatedAt = "createdAt"
+ FN.AnalyticsInfo.DaySumDate = "daySumDate"
+ FN.AnalyticsInfo.Id = "_id"
+ FN.AnalyticsInfo.SumDate = "sumDate"
+ FN.AnalyticsInfo.UpdatedAt = "updatedAt"
+ FN.AnalyticsInfo.YestodayActiveUsers = "yestodayActiveUsers"
+ FN.AnalyticsInfo.YestodayNewUsers = "yestodayNewUsers"
+ FN.AnalyticsInfo.YestodayPV = "yestodayPV"
+ FN.AnalyticsInfo.YestodaySessions = "yestodaySessions"
+ FN.AnalyticsInfo.YestodayUV = "yestodayUV"
+ TN.AnalyticsInfo = "analytics_info"
+}
\ No newline at end of file
diff --git a/pub/models/analyticsstat/type.go b/pub/models/analyticsstat/type.go
new file mode 100644
index 0000000..b60a115
--- /dev/null
+++ b/pub/models/analyticsstat/type.go
@@ -0,0 +1,27 @@
+package analyticsstat
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 数据统计
+// @table:analytics_info
+type AnalyticsInfo struct {
+ Id primitive.ObjectID `bson:"_id,omitempty"`
+ SumDate time.Time `json:"sumDate" bson:"sumDate"` // 时间半小时
+ DaySumDate time.Time `json:"daySumDate" bson:"daySumDate"` // 当天
+ AppId int64 `json:"appId" bson:"appId"` // appId
+ AppName string `json:"appName" bson:"appName"` // APP名称
+ ActiveUsers int64 `json:"activeUsers" bson:"activeUsers"` // 最近半小时活跃数
+ YestodayPV int64 `json:"yestodayPV" bson:"yestodayPV"` // 页面浏览量 (PV)
+ YestodayUV int64 `json:"yestodayUV" bson:"yestodayUV"` // 独立用户数 (UV)
+ YestodayActiveUsers int64 `json:"yestodayActiveUsers" bson:"yestodayActiveUsers"` // 活跃用户数
+ YestodaySessions int64 `json:"yestodaySessions" bson:"yestodaySessions"` // 会话数
+ YestodayNewUsers int64 `json:"yestodayNewUsers" bson:"yestodayNewUsers"` // 新增数量
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type AnalyticsInfoList []*AnalyticsInfo
\ No newline at end of file
diff --git a/pub/models/announcementmod/announcement.go b/pub/models/announcementmod/announcement.go
new file mode 100644
index 0000000..6de89b0
--- /dev/null
+++ b/pub/models/announcementmod/announcement.go
@@ -0,0 +1,102 @@
+package announcementmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.Announcement)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Announcement.Status, Value: 1},
+ {Key: FN.Announcement.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Announcement.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(announcementmod:Init) Create Announcement Index failed! err:%+v:", err))
+ }
+}
+
+func InsertOneAnnouncement(ctx context.Context, doc *Announcement) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, doc); err != nil {
+ log.Warn("announcementmod:InsertOneAnnouncement db.InsertOne failed",
+ log.E(err), log.Any("doc", doc))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+func DelOneAnnouncement(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("announcementmod:DelOneAnnouncement db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+func FindOneAnnouncement(ctx context.Context, filter bson.M) (*Announcement, *srvdef.SrvErr) {
+ var doc Announcement
+ if err := db.FindOneEx(ctx, &doc, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("announcementmod:FindOneAnnouncement db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrAnnouncementNotExsit, "", "")
+ }
+ return &doc, nil
+}
+
+func FindManyAnnouncement(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (AnnouncementList, *srvdef.SrvErr) {
+ data := make(AnnouncementList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("announcementmod:FindManyAnnouncement db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+func CountAnnouncement(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("announcementmod:CountAnnouncement db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+func UpdateOneAnnouncement(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("announcementmod:UpdateOneAnnouncement db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("announcementmod:UpdateOneAnnouncement not matched",
+ log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrAnnouncementNotExsit, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/announcementmod/tag.go b/pub/models/announcementmod/tag.go
new file mode 100644
index 0000000..bb684f5
--- /dev/null
+++ b/pub/models/announcementmod/tag.go
@@ -0,0 +1,30 @@
+package announcementmod
+
+type TagAnnouncement struct {
+ Id string
+ Title string
+ Content string
+ Sort string
+ Status string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ Announcement TagAnnouncement
+}
+
+var TN struct {
+ Announcement string
+}
+
+func init() {
+ FN.Announcement.Id = "_id"
+ FN.Announcement.Title = "title"
+ FN.Announcement.Content = "content"
+ FN.Announcement.Sort = "sort"
+ FN.Announcement.Status = "status"
+ FN.Announcement.CreatedAt = "createdAt"
+ FN.Announcement.UpdatedAt = "updatedAt"
+ TN.Announcement = "announcement"
+}
diff --git a/pub/models/announcementmod/type.go b/pub/models/announcementmod/type.go
new file mode 100644
index 0000000..3fb6d7a
--- /dev/null
+++ b/pub/models/announcementmod/type.go
@@ -0,0 +1,17 @@
+package announcementmod
+
+import "time"
+
+// Announcement 公告
+// @table:announcement
+type Announcement struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 标题
+ Content string `json:"content" bson:"content"` // 内容
+ Sort int64 `json:"sort" bson:"sort"` // 排序,越大越靠前
+ Status bool `json:"status" bson:"status"` // 是否启用
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+
+type AnnouncementList []*Announcement
diff --git a/pub/models/cfg/appkey.go b/pub/models/cfg/appkey.go
new file mode 100644
index 0000000..31f6f3a
--- /dev/null
+++ b/pub/models/cfg/appkey.go
@@ -0,0 +1,42 @@
+package cfg
+
+import (
+ "context"
+ "strings"
+ "sync"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+var appKeyCache struct {
+ sync.RWMutex
+ value string
+ ready bool
+}
+
+// CacheAppKey 把系统配置里的 appKey 放进内存,供媒资写接口读取。
+func CacheAppKey(appKey string) {
+ appKeyCache.Lock()
+ appKeyCache.value = strings.TrimSpace(appKey)
+ appKeyCache.ready = true
+ appKeyCache.Unlock()
+}
+
+// CachedAppKey 返回已缓存的 appKey。缓存未建立时读取 sys_cfg 并写入缓存。
+func CachedAppKey(ctx context.Context) string {
+ appKeyCache.RLock()
+ if appKeyCache.ready {
+ value := appKeyCache.value
+ appKeyCache.RUnlock()
+ return value
+ }
+ appKeyCache.RUnlock()
+
+ sysCfg, serr := FindOneSysCfg(ctx, bson.M{FN.SysCfg.Id: 1})
+ if serr != nil || sysCfg == nil {
+ return ""
+ }
+ value := strings.TrimSpace(sysCfg.AppKey)
+ CacheAppKey(value)
+ return value
+}
diff --git a/pub/models/cfg/dbhelper.go b/pub/models/cfg/dbhelper.go
new file mode 100644
index 0000000..6e8c6d5
--- /dev/null
+++ b/pub/models/cfg/dbhelper.go
@@ -0,0 +1,9 @@
+package cfg
+
+import (
+ "videoweb/pub/mongos"
+)
+
+var (
+ sysCfgColl *mongos.Collection
+)
diff --git a/pub/models/cfg/dbop.go b/pub/models/cfg/dbop.go
new file mode 100644
index 0000000..04ab8b5
--- /dev/null
+++ b/pub/models/cfg/dbop.go
@@ -0,0 +1,55 @@
+package cfg
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ sysCfgColl = mongos.Shop.Coll(TN.SysCfg)
+}
+
+// FindOneSysCfg 查找一个系统配置
+func FindOneSysCfg(ctx context.Context, filter bson.M) (
+ *SysCfg, *srvdef.SrvErr) {
+ var sysCfg SysCfg
+ if err := sysCfgColl.FindOneEx(
+ ctx, &sysCfg, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("cfg:FindOneSysCfg db.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &sysCfg, nil
+}
+
+// UpdateOneSysCfg 更新一个系统配置
+func UpdateOneSysCfg(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := sysCfgColl.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("cfg:UpdateOneSysCfg db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+func DeleteOneSysCfg(ctx context.Context, filter bson.M) *srvdef.SrvErr {
+ _, err := sysCfgColl.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("cfg:DeleteOneSysCfg db.DeleteOne failed", log.Any("filter", filter), log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/cfg/tag.go b/pub/models/cfg/tag.go
new file mode 100755
index 0000000..8d483ed
--- /dev/null
+++ b/pub/models/cfg/tag.go
@@ -0,0 +1,88 @@
+package cfg
+
+type TagSysCfg struct {
+ AppAddr string
+ DownloadVideoUrl string
+ ForeverUrlAddr string
+ GitAddr string
+ GmailAddr string
+ Id string
+ ImgAppCdn string
+ ImgWebCdn string
+ MediaAppCdn string
+ MediaWebCdn string
+ NewUrlAddr string
+ NewUrlAddr1 string
+ NewUrlAddr2 string
+ QQ string
+ Telegram string
+ TelegramAdd string
+ TgGroup string
+ TranslationAddr string
+ TwitterAddr string
+ WatermarkImage string
+ WatermarkMedia string
+ WatermarkMediaTop string
+ WatermarkMediaTopRight string
+ WatermarkMediaBottomLeft string
+ WeiChart string
+ DHNewUrlAddr1 string
+ DHNewUrlAddr2 string
+ DHNewUrlAddr3 string
+ DHForeverUrlAddr string
+ UnencryptedCdn string
+ SitemapDefaultImages string
+ Version string
+ ChannelCode string
+ EnableH265 string
+ M3U8DayRateLimitMax string
+ AppKey string
+}
+
+var FN struct {
+ SysCfg TagSysCfg
+}
+
+var TN struct {
+ SysCfg string
+}
+
+func init() {
+ FN.SysCfg.AppAddr = "appAddr"
+ FN.SysCfg.DownloadVideoUrl = "downloadVideoUrl"
+ FN.SysCfg.ForeverUrlAddr = "foreverUrlAddr"
+ FN.SysCfg.GitAddr = "gitAddr"
+ FN.SysCfg.GmailAddr = "gmailAddr"
+ FN.SysCfg.Id = "_id"
+ FN.SysCfg.ImgAppCdn = "imgAppCdn"
+ FN.SysCfg.ImgWebCdn = "imgWebCdn"
+ FN.SysCfg.MediaAppCdn = "mediaAppCdn"
+ FN.SysCfg.MediaWebCdn = "mediaWebCdn"
+ FN.SysCfg.NewUrlAddr = "newUrlAddr"
+ FN.SysCfg.NewUrlAddr1 = "newUrlAddr1"
+ FN.SysCfg.NewUrlAddr2 = "newUrlAddr2"
+ FN.SysCfg.QQ = "qq"
+ FN.SysCfg.Telegram = "telegram"
+ FN.SysCfg.TelegramAdd = "telegramAdd"
+ FN.SysCfg.TgGroup = "tgGroup"
+ FN.SysCfg.TranslationAddr = "translationAddr"
+ FN.SysCfg.TwitterAddr = "twitterAddr"
+ FN.SysCfg.WatermarkImage = "watermarkImage"
+ FN.SysCfg.WatermarkMedia = "watermarkMedia"
+ FN.SysCfg.WatermarkMediaTop = "watermarkMediaTop"
+ FN.SysCfg.WatermarkMediaTopRight = "watermarkMediaTopRight"
+ FN.SysCfg.WatermarkMediaBottomLeft = "watermarkMediaBottomLeft"
+ FN.SysCfg.WeiChart = "weiChart"
+ FN.SysCfg.DHNewUrlAddr1 = "dHNewUrlAddr1"
+ FN.SysCfg.DHNewUrlAddr2 = "dHNewUrlAddr2"
+ FN.SysCfg.DHNewUrlAddr3 = "dHNewUrlAddr3"
+ FN.SysCfg.DHForeverUrlAddr = "dHForeverUrlAddr"
+ FN.SysCfg.UnencryptedCdn = "unencryptedCdn"
+ FN.SysCfg.SitemapDefaultImages = "sitemapDefaultImages"
+ FN.SysCfg.Version = "version"
+ FN.SysCfg.ChannelCode = "channelCode"
+ FN.SysCfg.EnableH265 = "enableH265"
+ FN.SysCfg.M3U8DayRateLimitMax = "m3u8DayRateLimitMax"
+ FN.SysCfg.AppKey = "appKey"
+ TN.SysCfg = "sys_cfg"
+}
diff --git a/pub/models/cfg/type.go b/pub/models/cfg/type.go
new file mode 100644
index 0000000..5187530
--- /dev/null
+++ b/pub/models/cfg/type.go
@@ -0,0 +1,73 @@
+package cfg
+
+import "strings"
+
+// @table:sys_cfg
+type SysCfg struct {
+ Id int64 `json:"id" bson:"_id"`
+ Version int64 `json:"version" bson:"version"` // 版本号,用于更新乐观锁
+ UnencryptedCdn string `json:"unencryptedCdn" bson:"unencryptedCdn"` // 不加密资源使用的cdn
+ SitemapDefaultImages []string `json:"sitemapDefaultImages" bson:"sitemapDefaultImages"` // Sitemap里面默认封面图
+ ImgAppCdn string `json:"imgAppCdn" bson:"imgAppCdn"` // 图片CDN(app使用)
+ ImgAppUsCdn string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片CDN(海外app使用)
+ ImgWebCdn string `json:"imgWebCdn" bson:"imgWebCdn"` // 图片CDN(web使用)
+ MediaWebCdn string `json:"mediaWebCdn" bson:"mediaWebCdn"` // 视频CDN(web使用)
+ MediaAppCdn string `json:"mediaAppCdn" bson:"mediaAppCdn"` // 视频CDN(app使用)
+ MediaAppUsCdn string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn"` // 视频CDN(海外app使用)
+ WatermarkImage string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
+ WatermarkMedia string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件(底部右边)
+ WatermarkMediaTop string `json:"watermarkMediaTop" bson:"watermarkMediaTop"` // 视频水印文件(顶部左边)
+ WatermarkMediaTopRight string `json:"watermarkMediaTopRight" bson:"watermarkMediaTopRight"` // 视频水印文件(顶部右边)
+ WatermarkMediaBottomLeft string `json:"watermarkMediaBottomLeft" bson:"watermarkMediaBottomLeft"` // 视频水印文件(底部左边)
+ Mp4Addr string `json:"mp4Addr" bson:"mp4Addr"` // mp4地址(web使用)
+ PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
+ //网站管理
+ NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
+ NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
+ NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
+ ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
+ GmailAddr string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
+ AppAddr string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
+ DownloadVideoUrl string `json:"downloadVideoUrl" bson:"downloadVideoUrl"` // 下载视频URL
+ WeiChart string `json:"weiChart" bson:"weiChart"` //微信群
+ QQ string `json:"qq" bson:"qq"` //qq群
+ TgGroup string `json:"tgGroup" bson:"tgGroup"` //tg群
+ PotaoGroup string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
+ GitAddr string `json:"gitAddr" bson:"gitAddr"` //git地址
+ TwitterAddr string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
+ Telegram string `json:"telegram" bson:"telegram"` //商务tg
+ TelegramAdd string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
+ TranslationAddr string `json:"translationAddr" bson:"translationAddr"` //应用导航
+ //SEO
+ WebTitle string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
+ WebKeyworld []string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
+ WebDescription string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
+ WebSiteName string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
+ WebAddr string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
+ DHNewUrlAddr1 string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路1
+ DHNewUrlAddr2 string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
+ DHNewUrlAddr3 string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
+ FreeVPN string `json:"freeVPN" bson:"freeVPN"` // 免费vpn链接
+ UseAddress []string `json:"useAddress" bson:"useAddress"` // 所有使用中的域名
+ DHTitle string `json:"dhTitle" bson:"dhTitle"` // 导航SEO标题
+ DHKeyworld []string `json:"dhKeyworld" bson:"dhKeyworld"` // 导航SEO关键字
+ DHDescription string `json:"dhDescription" bson:"dhDescription"` // 导航SEO描述
+ DHSiteName string `json:"dhSiteName" bson:"dhSiteName"` // 导航SEO网站名称
+ ChannelCode string `json:"channelCode" bson:"channelCode"` // 渠道码
+ EnableH265 bool `json:"enableH265" bson:"enableH265"` // 是否允许客户端使用 H265 地址播放
+ M3U8DayRateLimitMax int64 `json:"m3u8DayRateLimitMax" bson:"m3u8DayRateLimitMax"` // 每 IP 每天最多不同正片数,<=0 使用默认 80
+ AppKey string `json:"appKey" bson:"appKey"` // 媒资库 appKey,调用 media 写接口时传入
+}
+
+// SEOBaseURL 返回与页面 canonical 一致的 SEO 主域。
+// 永久域名未配置时回退到 WebAddr,避免生成空地址。
+func (c *SysCfg) SEOBaseURL() string {
+ if c == nil {
+ return ""
+ }
+ base := strings.TrimSpace(c.ForeverUrlAddr)
+ if base == "" {
+ base = strings.TrimSpace(c.WebAddr)
+ }
+ return strings.TrimRight(base, "/")
+}
diff --git a/pub/models/commentlikemod/comment_like.go b/pub/models/commentlikemod/comment_like.go
new file mode 100644
index 0000000..a28664f
--- /dev/null
+++ b/pub/models/commentlikemod/comment_like.go
@@ -0,0 +1,87 @@
+package commentlikemod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const collectionName = "comment_like"
+
+const CollectionName = collectionName
+
+type CommentLike struct {
+ UserId int64 `bson:"userId"`
+ CommentId int64 `bson:"commentId"`
+ CreatedAt time.Time `bson:"createdAt"`
+}
+
+var coll *mongos.Collection
+
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(collectionName)
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: "userId", Value: 1}, {Key: "commentId", Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: "commentId", Value: 1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("commentlikemod.Init CreateIndex failed: %v", err))
+ }
+}
+
+// Add 新增点赞关系,重复点赞不改变状态
+func Add(ctx context.Context, userId, commentId int64) (bool, *srvdef.SrvErr) {
+ _, err := coll.InsertOne(ctx, &CommentLike{UserId: userId, CommentId: commentId, CreatedAt: time.Now()})
+ if mongo.IsDuplicateKeyError(err) {
+ return false, nil
+ }
+ if err != nil {
+ log.Warn("commentlikemod.Add", log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// Remove 删除点赞关系,重复取消不改变状态
+func Remove(ctx context.Context, userId, commentId int64) (bool, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, bson.M{"userId": userId, "commentId": commentId})
+ if err != nil {
+ log.Warn("commentlikemod.Remove", log.E(err))
+ return false, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count > 0, nil
+}
+
+// FindByUserAndComments 批量查询当前用户的评论点赞关系
+func FindByUserAndComments(ctx context.Context, userId int64, commentIds []int64) ([]CommentLike, *srvdef.SrvErr) {
+ result := make([]CommentLike, 0)
+ if userId <= 0 || len(commentIds) == 0 {
+ return result, nil
+ }
+ err := coll.Find(ctx, &result, bson.M{"userId": userId, "commentId": bson.M{"$in": commentIds}})
+ if err != nil {
+ log.Warn("commentlikemod.FindByUserAndComments", log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return result, nil
+}
+
+// DeleteByCommentIds 清理已删除评论的点赞关系
+func DeleteByCommentIds(ctx context.Context, commentIds []int64) *srvdef.SrvErr {
+ if len(commentIds) == 0 {
+ return nil
+ }
+ _, err := coll.DeleteMany(ctx, bson.M{"commentId": bson.M{"$in": commentIds}})
+ if err != nil {
+ log.Warn("commentlikemod.DeleteByCommentIds", log.E(err))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/commentmod/comment.go b/pub/models/commentmod/comment.go
new file mode 100644
index 0000000..abfd80a
--- /dev/null
+++ b/pub/models/commentmod/comment.go
@@ -0,0 +1,238 @@
+package commentmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.Comment)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.Id, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.Level, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ {Key: FN.Comment.ObjectType, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.Level, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ {Key: FN.Comment.ObjectType, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.Level, Value: 1},
+ {Key: FN.Comment.CountLike, Value: -1},
+ {Key: FN.Comment.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ParentsId, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.TopRank, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(comment:initCommentIndex) Create Comment Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneComment 插入一个评论
+func InsertOneComment(ctx context.Context, Comment *CommentModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, Comment); err != nil {
+ log.Warn("comment:InsertOneComment db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneComment 删除一个评论
+func DelOneComment(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("comment:DelOneComment db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyComment 删除一个评论评论
+func DelManyComment(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("comment:DelOneComment db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneComment 查找一个评论
+func FindOneComment(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *CommentModel, *srvdef.SrvErr) {
+ var Comment CommentModel
+ if err := db.FindOneEx(
+ ctx, &Comment, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("comment:FindOneComment db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &Comment, nil
+}
+
+// CountComments 统计评论视频评论数量
+func CountComments(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("comment:CountComments db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneComment 更新一个评论
+func UpdateOneComment(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("comment:UpdateOneComment db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("comment:UpdateOneComment db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// ChangeLikeCount 原子更新已审核评论的点赞数并返回最新值
+func ChangeLikeCount(ctx context.Context, commentId, delta int64) (int64, *srvdef.SrvErr) {
+ filter := bson.M{FN.Comment.Id: commentId, FN.Comment.ReviewStatus: 1}
+ if delta < 0 {
+ filter[FN.Comment.CountLike] = bson.M{"$gt": 0}
+ }
+ var comment CommentModel
+ err := db.FindOneAndUpdate(ctx, &comment, filter,
+ bson.M{"$inc": bson.M{FN.Comment.CountLike: delta}},
+ options.FindOneAndUpdate().SetReturnDocument(options.After))
+ if err == mongo.ErrNoDocuments {
+ return 0, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ if err != nil {
+ log.Warn("commentmod.ChangeLikeCount", log.E(err))
+ return 0, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return comment.CountLike, nil
+}
+
+// UpdateManyComment 更新一批评论
+func UpdateManyComment(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("comment:UpdateManyComment db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("comment:UpdateManyComment db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrCommentNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyComment 查找一批评论
+func FindManyComment(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (CommentModelList, *srvdef.SrvErr) {
+ data := make(CommentModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("comment:FindManyComment db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyComment 插入一批用户
+func InsertManyComment(ctx context.Context, histrory *CommentModelList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("comment:InsertManyComment db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateComment 聚合查找一批数据
+func AggregateComment(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("comment:AggregateComment db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateComment 聚合查找一批数据
+func ObjectAggregateComment(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("comment:AggregateComment db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/commentmod/tag.go b/pub/models/commentmod/tag.go
new file mode 100755
index 0000000..2c5d1db
--- /dev/null
+++ b/pub/models/commentmod/tag.go
@@ -0,0 +1,52 @@
+package commentmod
+
+type TagComment struct {
+ CreatedAt string
+ Id string
+ Level string
+ ObjectId string
+ ParentsId string
+ ReplyUserName string
+ Text string
+ TopRank string
+ UpdatedAt string
+ UserName string
+ UserId string
+ Avatar string
+ ReviewStatus string
+ CommentCount string
+ CountLike string
+ Ip string
+ ObjectName string
+ ObjectType string
+}
+
+var FN struct {
+ Comment TagComment
+}
+
+var TN struct {
+ Comment string
+}
+
+func init() {
+ FN.Comment.CreatedAt = "createdAt"
+ FN.Comment.Id = "_id"
+ FN.Comment.Level = "level"
+ FN.Comment.ObjectId = "objectId"
+ FN.Comment.ParentsId = "parentsId"
+ FN.Comment.ReplyUserName = "replyUserName"
+ FN.Comment.Text = "text"
+ FN.Comment.TopRank = "topRank"
+ FN.Comment.UpdatedAt = "updatedAt"
+ FN.Comment.UserName = "userName"
+ FN.Comment.ReviewStatus = "reviewStatus"
+ FN.Comment.Ip = "ip"
+ FN.Comment.CommentCount = "commentCount"
+ FN.Comment.CountLike = "countLike"
+ FN.Comment.ObjectName = "objectName"
+ FN.Comment.ObjectType = "objectType"
+ FN.Comment.UserId = "userId"
+ FN.Comment.Avatar = "avatar"
+ TN.Comment = "_comment"
+}
diff --git a/pub/models/commentmod/type.go b/pub/models/commentmod/type.go
new file mode 100644
index 0000000..77c3f81
--- /dev/null
+++ b/pub/models/commentmod/type.go
@@ -0,0 +1,31 @@
+package commentmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// 评论管理
+// @table:_comment
+type CommentModel struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+ Ip string `json:"ip" bson:"ip"` // 评论IP
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectType pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧
+ ObjectName string `json:"objectName" bson:"objectName"` // 对象类型名称 视频/短剧
+ UserName string `json:"userName" bson:"userName"` // 用户名称
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ Avatar string `json:"avatar" bson:"avatar"` // 用户头像
+ Level int64 `json:"level" bson:"level"` // 评论级别
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
+ Text string `json:"text" bson:"text"` // 评论内容
+ CommentCount int64 `json:"commentCount" bson:"commentCount"` // 该评论回复数量
+ CountLike int64 `json:"countLike" bson:"countLike"` // 点赞数
+ 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"` // 置顶排序
+}
+
+type CommentModelList []*CommentModel
diff --git a/pub/models/contactusmod/contactus.go b/pub/models/contactusmod/contactus.go
new file mode 100644
index 0000000..57bdad0
--- /dev/null
+++ b/pub/models/contactusmod/contactus.go
@@ -0,0 +1,189 @@
+package contactusmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.ContactUsModel)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUsModel.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUsModel.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUsModel.Name, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUsModel.Email, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUsModel.Ip, Value: 1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(ContactUsModel:initContactUsModelIndex) Create ContactUsModel Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneContactUsModel 插入一个评论
+func InsertOneContactUsModel(ctx context.Context, ContactUsModel *ContactUsModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, ContactUsModel); err != nil {
+ log.Warn("ContactUsModel:InsertOneContactUsModel db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneContactUsModel 删除一个评论
+func DelOneContactUsModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("ContactUsModel:DelOneContactUsModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyContactUsModel 删除一个评论评论
+func DelManyContactUsModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("ContactUsModel:DelOneContactUsModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneContactUsModel 查找一个评论
+func FindOneContactUsModel(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *ContactUsModel, *srvdef.SrvErr) {
+ var ContactUsModel ContactUsModel
+ if err := db.FindOneEx(
+ ctx, &ContactUsModel, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("ContactUsModel:FindOneContactUsModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &ContactUsModel, nil
+}
+
+// CountContactUsModels 统计评论视频评论数量
+func CountContactUsModels(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("ContactUsModel:CountContactUsModels db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneContactUsModel 更新一个评论
+func UpdateOneContactUsModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("ContactUsModel:UpdateOneContactUsModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("ContactUsModel:UpdateOneContactUsModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyContactUsModel 更新一批评论
+func UpdateManyContactUsModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("ContactUsModel:UpdateManyContactUsModel db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("ContactUsModel:UpdateManyContactUsModel db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrContactUsModelNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyContactUsModel 查找一批评论
+func FindManyContactUsModel(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (ContactUsModelList, *srvdef.SrvErr) {
+ data := make(ContactUsModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("ContactUsModel:FindManyContactUsModel db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyContactUsModel 插入一批用户
+func InsertManyContactUsModel(ctx context.Context, histrory *ContactUsModelList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("ContactUsModel:InsertManyContactUsModel db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateContactUsModel 聚合查找一批数据
+func AggregateContactUsModel(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("ContactUsModel:AggregateContactUsModel db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateContactUsModel 聚合查找一批数据
+func ObjectAggregateContactUsModel(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("ContactUsModel:AggregateContactUsModel db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/contactusmod/tag.go b/pub/models/contactusmod/tag.go
new file mode 100755
index 0000000..3b15090
--- /dev/null
+++ b/pub/models/contactusmod/tag.go
@@ -0,0 +1,34 @@
+package contactusmod
+
+type TagContactUsModel struct {
+ Id string
+ Ip string
+ Name string
+ Email string
+ CategoryName string
+ Content string
+ Status string
+ UpdatedAt string
+ CreatedAt string
+}
+
+var FN struct {
+ ContactUsModel TagContactUsModel
+}
+
+var TN struct {
+ ContactUsModel string
+}
+
+func init() {
+ FN.ContactUsModel.Id = "_id"
+ FN.ContactUsModel.Ip = "ip"
+ FN.ContactUsModel.Name = "name"
+ FN.ContactUsModel.Email = "email"
+ FN.ContactUsModel.CategoryName = "categoryName"
+ FN.ContactUsModel.Content = "content"
+ FN.ContactUsModel.Status = "status"
+ FN.ContactUsModel.UpdatedAt = "updatedAt"
+ FN.ContactUsModel.CreatedAt = "createdAt"
+ TN.ContactUsModel = "contact_us"
+}
diff --git a/pub/models/contactusmod/type.go b/pub/models/contactusmod/type.go
new file mode 100644
index 0000000..bc1a4df
--- /dev/null
+++ b/pub/models/contactusmod/type.go
@@ -0,0 +1,23 @@
+package contactusmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// ContactUsModel 联系我们
+// @table:contact_us
+type ContactUsModel struct {
+ Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` // ID
+ Ip string `json:"ip" bson:"ip"` // 评论IP
+ Name string `json:"name" bson:"name"` // 名字
+ Email string `json:"email" bson:"email"` // 邮件
+ CategoryName string `json:"categoryName" bson:"categoryName"` // 主旨分类
+ Content string `json:"content" bson:"content"` // 内容
+ Status int64 `json:"status" bson:"status"` // 状态 0=初始 1=已回复 2=不予处理 3=已完结
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
+}
+
+type ContactUsModelList []*ContactUsModel
diff --git a/pub/models/contenttagmod/content_tag.go b/pub/models/contenttagmod/content_tag.go
new file mode 100644
index 0000000..04d1e00
--- /dev/null
+++ b/pub/models/contenttagmod/content_tag.go
@@ -0,0 +1,206 @@
+package contenttagmod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 content_tag 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.ContentTag)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.ContentTag.Name, Value: 1},
+ },
+ Options: options.Index().
+ SetUnique(true).
+ SetName("uniq_content_tag_name").
+ SetPartialFilterExpression(bson.M{FN.ContentTag.Name: bson.M{"$type": "string", "$gt": ""}}),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContentTag.Types, Value: 1},
+ {Key: FN.ContentTag.Status, Value: 1},
+ {Key: FN.ContentTag.Rank, Value: -1},
+ {Key: FN.ContentTag.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContentTag.Status, Value: 1},
+ {Key: FN.ContentTag.Rank, Value: -1},
+ {Key: FN.ContentTag.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContentTag.VideoCount, Value: -1},
+ {Key: FN.ContentTag.Rank, Value: -1},
+ {Key: FN.ContentTag.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContentTag.PlayletCount, Value: -1},
+ {Key: FN.ContentTag.Rank, Value: -1},
+ {Key: FN.ContentTag.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(contenttagmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneContentTag 插入一条统一标签数据
+func InsertOneContentTag(ctx context.Context, doc *ContentTag) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.ContentTag.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ log.Warn("contenttagmod:InsertOneContentTag duplicate key", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInputExist, "标签名称已存在", "")
+ }
+ log.Warn("contenttagmod:InsertOneContentTag failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneContentTag 删除一条统一标签数据
+func DelOneContentTag(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("contenttagmod:DelOneContentTag failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneContentTag 查询一条统一标签数据
+func FindOneContentTag(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*ContentTag, *srvdef.SrvErr) {
+ var doc ContentTag
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("contenttagmod:FindOneContentTag failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyContentTag 按条件查询多条统一标签数据
+func FindManyContentTag(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (ContentTagList, *srvdef.SrvErr) {
+ data := make(ContentTagList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("contenttagmod:FindManyContentTag failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// AggregateContentTag 聚合查询统一标签数据
+func AggregateContentTag(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline, opts...); err != nil {
+ log.Warn("contenttagmod:AggregateContentTag 聚合查询统一标签失败", log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// GetMapTags 按标签ID批量查询统一标签映射
+func GetMapTags(ctx context.Context, ids []int64) (ContentTagMap, *srvdef.SrvErr) {
+ result := make(ContentTagMap)
+ if len(ids) == 0 {
+ return result, nil
+ }
+ list, err := FindManyContentTag(ctx, bson.M{
+ FN.ContentTag.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.M{
+ FN.ContentTag.Id: 1,
+ FN.ContentTag.Name: 1,
+ FN.ContentTag.Slug: 1,
+ FN.ContentTag.Types: 1,
+ FN.ContentTag.Status: 1,
+ "seoInfo": 1,
+ FN.ContentTag.Rank: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ if item != nil {
+ result[item.Id] = item
+ }
+ }
+ return result, nil
+}
+
+// CountContentTag 统计满足条件的统一标签数量
+func CountContentTag(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("contenttagmod:CountContentTag failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneContentTag 更新一条统一标签数据
+func UpdateOneContentTag(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ log.Warn("contenttagmod:UpdateOneContentTag duplicate key", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbInputExist, "标签名称已存在", "")
+ }
+ log.Warn("contenttagmod:UpdateOneContentTag failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyContentTag 批量更新统一标签数据
+func UpdateManyContentTag(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("contenttagmod:UpdateManyContentTag failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// BulkWriteContentTag 批量写入/更新统一标签数据
+func BulkWriteContentTag(ctx context.Context, docs []mongo.WriteModel, opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, *srvdef.SrvErr) {
+ result, err := coll.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ log.Warn("contenttagmod:BulkWriteContentTag failed", log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result, nil
+}
diff --git a/pub/models/contenttagmod/tag.go b/pub/models/contenttagmod/tag.go
new file mode 100644
index 0000000..7d079c7
--- /dev/null
+++ b/pub/models/contenttagmod/tag.go
@@ -0,0 +1,51 @@
+package contenttagmod
+
+// TagContentTag 保存 content_tag 集合字段名
+type TagContentTag struct {
+ Id string
+ Name string
+ Slug string
+ Types string
+ Status string
+ Rank string
+ VideoCount string
+ PlayletCount string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ CreatedById string
+ CreatedByName string
+ UpdatedByName string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ ContentTag TagContentTag
+}
+
+var TN struct {
+ ContentTag string
+}
+
+func init() {
+ FN.ContentTag.Id = "_id"
+ FN.ContentTag.Name = "name"
+ FN.ContentTag.Slug = "slug"
+ FN.ContentTag.Types = "types"
+ FN.ContentTag.Status = "status"
+ FN.ContentTag.Rank = "rank"
+ FN.ContentTag.VideoCount = "videoCount"
+ FN.ContentTag.PlayletCount = "playletCount"
+ FN.ContentTag.SeoInfo.Description = "seoInfo.webDescription"
+ FN.ContentTag.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.ContentTag.SeoInfo.Title = "seoInfo.webTitle"
+ FN.ContentTag.CreatedById = "createdById"
+ FN.ContentTag.CreatedByName = "createdByName"
+ FN.ContentTag.UpdatedByName = "updatedByName"
+ FN.ContentTag.CreatedAt = "createdAt"
+ FN.ContentTag.UpdatedAt = "updatedAt"
+ TN.ContentTag = "content_tag"
+}
diff --git a/pub/models/contenttagmod/type.go b/pub/models/contenttagmod/type.go
new file mode 100644
index 0000000..4631521
--- /dev/null
+++ b/pub/models/contenttagmod/type.go
@@ -0,0 +1,40 @@
+package contenttagmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// ContentTagType 统一内容标签支持的内容类型
+type ContentTagType int32
+
+const (
+ ContentTagTypeVideo ContentTagType = 1 // 视频
+ ContentTagTypePlaylet ContentTagType = 5 // 短剧
+)
+
+// ContentTag @table: content_tag
+//
+// ContentTag 内容统一标签集合模型
+type ContentTag struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // ID
+ Name string `json:"name" bson:"name"` // 标签名称
+ Slug string `json:"slug" bson:"slug"` // slug
+ Types []ContentTagType `json:"types" bson:"types"` // 支持类型 1-视频 5-短剧
+ Status bool `json:"status" bson:"status"` // 状态 true-启用 false-停用
+ Rank int64 `json:"rank" bson:"rank"` // 排序,越大越靠前
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 绑定视频数量
+ PlayletCount int64 `json:"playletCount" bson:"playletCount"` // 绑定短剧数量
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ CreatedById int64 `json:"createdById" bson:"createdById"` // 创建人ID
+ CreatedByName string `json:"createdByName" bson:"createdByName"` // 创建人名称
+ UpdatedByName string `json:"updatedByName" bson:"updatedByName"` // 修改人名称
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// ContentTagList 内容统一标签列表
+type ContentTagList []*ContentTag
+
+// ContentTagMap 内容统一标签ID映射
+type ContentTagMap map[int64]*ContentTag
diff --git a/pub/models/detailnavmod/detailnav.go b/pub/models/detailnavmod/detailnav.go
new file mode 100644
index 0000000..b0840bf
--- /dev/null
+++ b/pub/models/detailnavmod/detailnav.go
@@ -0,0 +1,102 @@
+package detailnavmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.DetailNavModel)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.DetailNavModel.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(detailnavmod:Init) Create DetailNavModel Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneDetailNav 插入一条详情页导航
+func InsertOneDetailNav(ctx context.Context, doc *DetailNavModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, doc); err != nil {
+ log.Warn("detailnavmod:InsertOneDetailNav db.InsertOne failed",
+ log.E(err), log.Any("doc", doc))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneDetailNav 删除一条详情页导航
+func DelOneDetailNav(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("detailnavmod:DelOneDetailNav db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneDetailNav 查找一条详情页导航
+func FindOneDetailNav(ctx context.Context, filter bson.M) (*DetailNavModel, *srvdef.SrvErr) {
+ var doc DetailNavModel
+ if err := db.FindOneEx(ctx, &doc, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("detailnavmod:FindOneDetailNav db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyDetailNav 查找多条详情页导航
+func FindManyDetailNav(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (DetailNavModelList, *srvdef.SrvErr) {
+ data := make(DetailNavModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("detailnavmod:FindManyDetailNav db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountDetailNav 统计详情页导航数量
+func CountDetailNav(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("detailnavmod:CountDetailNav db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneDetailNav 更新一条详情页导航
+func UpdateOneDetailNav(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("detailnavmod:UpdateOneDetailNav db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("detailnavmod:UpdateOneDetailNav not matched",
+ log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/detailnavmod/tag.go b/pub/models/detailnavmod/tag.go
new file mode 100644
index 0000000..6bef236
--- /dev/null
+++ b/pub/models/detailnavmod/tag.go
@@ -0,0 +1,30 @@
+package detailnavmod
+
+type TagDetailNavModel struct {
+ CreatedAt string
+ ID string
+ Name string
+ Url string
+ Sort string
+ Status string
+ UpdatedAt string
+}
+
+var FN struct {
+ DetailNavModel TagDetailNavModel
+}
+
+var TN struct {
+ DetailNavModel string
+}
+
+func init() {
+ FN.DetailNavModel.CreatedAt = "createdAt"
+ FN.DetailNavModel.ID = "_id"
+ FN.DetailNavModel.Name = "name"
+ FN.DetailNavModel.Url = "url"
+ FN.DetailNavModel.Sort = "sort"
+ FN.DetailNavModel.Status = "status"
+ FN.DetailNavModel.UpdatedAt = "updatedAt"
+ TN.DetailNavModel = "detail_nav"
+}
diff --git a/pub/models/detailnavmod/type.go b/pub/models/detailnavmod/type.go
new file mode 100644
index 0000000..cb5c91e
--- /dev/null
+++ b/pub/models/detailnavmod/type.go
@@ -0,0 +1,21 @@
+package detailnavmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 详情页导航配置
+// @table:detail_nav
+type DetailNavModel struct {
+ ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
+ Name string `json:"name" bson:"name"` // 导航名称
+ Url string `json:"url" bson:"url"` // 导航链接
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ Status bool `json:"status" bson:"status"` // 是否启用
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+
+type DetailNavModelList []*DetailNavModel
diff --git a/pub/models/esmod/actress.go b/pub/models/esmod/actress.go
new file mode 100644
index 0000000..61db243
--- /dev/null
+++ b/pub/models/esmod/actress.go
@@ -0,0 +1,105 @@
+package esmod
+
+import (
+ "fmt"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+)
+
+// SearchActress 根据关键字搜索媒体
+func SearchActress(content string, from int64, size int64, sort string) (int64, []*ESActress, *srvdef.SrvErr) {
+ fields := []string{FN.ESActress.Alias, FN.ESActress.Office, FN.ESActress.Region, FN.ESActress.Desc}
+ must := esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields},
+ },
+ {"term": esmanager.M{FN.ESActress.Status: true}},
+ },
+ },
+ }
+ switch sort {
+ case string(proto.SearchSortTypeHot):
+ must = esmanager.M{
+ "function_score": esmanager.M{
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields,
+ }},
+ {"term": esmanager.M{FN.ESActress.Status: true}},
+ },
+ },
+ },
+ "functions": esmanager.A{
+ esmanager.M{
+ "field_value_factor": esmanager.M{
+ "field": FN.ESActress.Real.WatchTimes,
+ "factor": 1.0,
+ "modifier": "none",
+ },
+ },
+ },
+ "score_mode": "sum",
+ "boost_mode": "multiply",
+ },
+ }
+ case string(proto.SearchSortTypeNew):
+ must = esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields},
+ },
+ {"term": esmanager.M{FN.ESActress.Status: true}},
+ },
+ "should": esmanager.A{
+ {"range": esmanager.M{FN.ESActress.CreatedAt: esmanager.M{"gte": "now-30d/d", "boost": 2.0}}}, //7天内提升4倍权重
+ {"range": esmanager.M{FN.ESActress.CreatedAt: esmanager.M{"gte": "now-180d/d", "boost": 1.5}}}, // 30天内提升3倍权重
+ {"range": esmanager.M{FN.ESActress.CreatedAt: esmanager.M{"gte": "now-365d/d", "boost": 1.1}}}, // 90天内提升1.5倍权重
+ },
+ "minimum_should_match": 0, // 允许不匹配任何should条件
+ },
+ }
+ }
+
+ query := esmanager.M{
+ "query": must,
+ "sort": esmanager.A{
+ esmanager.M{"_score": esmanager.M{"order": "desc"}},
+ },
+ "from": from,
+ "size": size,
+ }
+
+ actressList := &ESActressHits{}
+ err := esmanager.Instance.Search(actressIndex, actressList, query)
+ if err != nil {
+ info := fmt.Sprintf(
+ "(esmod:SearchActress) Search failed! err:%+v:", err)
+ log.Warn(info,
+ log.Any("keywords", content),
+ log.Any("from", from),
+ log.Any("size", size))
+ return 0, nil, srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ total := int64(0)
+ rslData := make([]*ESActress, 0)
+ if actressList != nil && actressList.Hits != nil && len(actressList.Hits) > 0 {
+ total = actressList.Total.Value
+ for _, video := range actressList.Hits {
+ rslData = append(rslData, video.Source)
+ }
+ }
+ return total, rslData, nil
+}
\ No newline at end of file
diff --git a/pub/models/esmod/eshelper.go b/pub/models/esmod/eshelper.go
new file mode 100644
index 0000000..187ec5e
--- /dev/null
+++ b/pub/models/esmod/eshelper.go
@@ -0,0 +1,62 @@
+package esmod
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+const (
+ FiveMinuteScale = 5 //对齐时间(分钟)
+)
+
+var (
+ videoIndex = "webhjsq_video_index_v1"
+ playletIndex = "webhjsq_playlet_index_v1"
+ actressIndex = "webhjsq_actress_index_v1"
+)
+
+func GetVideoIndex() string {
+ return videoIndex
+}
+func GetPlayletIndex() string {
+ return playletIndex
+}
+func GetActressIndex() string {
+ return actressIndex
+}
+
+func GetRecordByESIndex(ctx context.Context, esIndex string) (
+ *ESTimestamp, *srvdef.SrvErr) {
+
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ esRecord, serr := FindOneESRecord(ctx, filter)
+ if serr != nil {
+ if serr.Code != srvdef.ErrNoData {
+ return nil, serr
+ }
+ return nil, nil
+ }
+ return &esRecord.Timestamp, nil
+}
+
+func SetOneEsRecord(ctx context.Context, esIndex string,
+ timestamp *ESTimestamp) *srvdef.SrvErr {
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ upsertDoc := bson.M{
+ "$set": bson.M{
+ FN.ESRecord.Timestamp.T: timestamp.T,
+ FN.ESRecord.Timestamp.I: timestamp.I,
+ FN.ESRecord.UpdatedAt: time.Now(),
+ },
+ "$setOnInsert": bson.M{
+ FN.ESRecord.Id: primitive.NewObjectID(),
+ FN.ESRecord.CreatedAt: time.Now(),
+ },
+ }
+ serr := UpsertOneESRecord(ctx, filter, upsertDoc)
+ return serr
+}
diff --git a/pub/models/esmod/esop.go b/pub/models/esmod/esop.go
new file mode 100644
index 0000000..7e082c9
--- /dev/null
+++ b/pub/models/esmod/esop.go
@@ -0,0 +1,343 @@
+package esmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ initDBIndex(ctx)
+ initIndex()
+ return
+}
+
+// initIndex 初始化索引
+func initIndex() {
+ if esmanager.Instance == nil {
+ return
+ }
+ initVideoIndex(videoIndex)
+ initPlayletIndex(playletIndex)
+ initActressIndex(actressIndex)
+}
+
+// initDBIndex 初始化数据库索引
+func initDBIndex(ctx context.Context) {
+ //获取数据库表
+ db = mongos.Shop.Coll(TN.ESRecord)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.ESRecord.EsIndex, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(esmod:initDBIndex) Create es_record Index failed! err:%+v:", err))
+ }
+}
+
+func initVideoIndex(esIndex string) {
+ var setting = esmanager.M{
+ "settings": esmanager.M{
+ "number_of_shards": esmanager.NumberOfShards,
+ "number_of_replicas": esmanager.NumberOfReplicas,
+ "analysis": esmanager.M{
+ "analyzer": esmanager.M{
+ "ik": esmanager.M{
+ "tokenizer": esmanager.AnalyzerIkSmart,
+ },
+ },
+ },
+ },
+ "mappings": esmanager.M{
+ "properties": esmanager.M{
+ FN.ESVideo.Title: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.MediaType: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.Remark: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Desc: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Keyworlds: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.CreatorName: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Bango: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.IssuedBrand: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Status: esmanager.M{
+ "type": "integer",
+ },
+ //FN.ESVideo.IsHot: esmanager.M{
+ // "type": "boolean",
+ //},
+ //FN.ESVideo.Sort: esmanager.M{
+ // "type": "integer",
+ //},
+ FN.ESVideo.WatchTimes: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.ShowTimes: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.Comments: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.AddedTime: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ FN.ESVideo.IssuedAt: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ //FN.ESVideo.CreatedAt: esmanager.M{
+ // "type": "date",
+ // "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ //},
+ },
+ },
+ }
+ if err := esmanager.Instance.InitIndices(esIndex, setting); err != nil {
+ panic(fmt.Sprintf("ElasticSearch Index %s initMediaIndex error", esIndex))
+ }
+}
+
+func initPlayletIndex(esIndex string) {
+ var setting = esmanager.M{
+ "settings": esmanager.M{
+ "number_of_shards": esmanager.NumberOfShards,
+ "number_of_replicas": esmanager.NumberOfReplicas,
+ "analysis": esmanager.M{
+ "analyzer": esmanager.M{
+ "ik": esmanager.M{
+ "tokenizer": esmanager.AnalyzerIkSmart,
+ },
+ },
+ },
+ },
+ "mappings": esmanager.M{
+ "properties": esmanager.M{
+ FN.ESPlaylet.Id: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.Status: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESPlaylet.IsFeatured: esmanager.M{
+ "type": "boolean",
+ },
+ FN.ESPlaylet.IsRecommend: esmanager.M{
+ "type": "boolean",
+ },
+ FN.ESPlaylet.ShowOnBanner: esmanager.M{
+ "type": "boolean",
+ },
+ FN.ESPlaylet.Banner: esmanager.M{
+ "type": "keyword",
+ "index": false,
+ },
+ FN.ESPlaylet.OnlineAt: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ FN.ESPlaylet.CategoryIds: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.CategoryNames: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.ThemeIds: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.ThemeNames: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.Title: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.Desc: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.Anchor: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.Cover: esmanager.M{
+ "type": "keyword",
+ "index": false,
+ },
+ FN.ESPlaylet.TagIds: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.TagNames: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.TotalEpisode: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.UpdateStatus: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESPlaylet.CountBrowse: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.HeatValue: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.CountCollect: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.CountLike: esmanager.M{
+ "type": "long",
+ },
+ FN.ESPlaylet.SeoInfo: esmanager.M{
+ "properties": esmanager.M{
+ "title": esmanager.M{
+ "type": "keyword",
+ "index": false,
+ },
+ "keyworld": esmanager.M{
+ "type": "keyword",
+ "index": false,
+ },
+ "description": esmanager.M{
+ "type": "keyword",
+ "index": false,
+ },
+ },
+ },
+ FN.ESPlaylet.UpdatedAt: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ FN.ESPlaylet.CreatedAt: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ },
+ },
+ }
+ if err := esmanager.Instance.InitIndices(esIndex, setting); err != nil {
+ panic(fmt.Sprintf("ElasticSearch Index %s initPlayletIndex error", esIndex))
+ }
+}
+
+func initActressIndex(esIndex string) {
+ var setting = esmanager.M{
+ "settings": esmanager.M{
+ "number_of_shards": esmanager.NumberOfShards,
+ "number_of_replicas": esmanager.NumberOfReplicas,
+ "analysis": esmanager.M{
+ "analyzer": esmanager.M{
+ "ik": esmanager.M{
+ "tokenizer": esmanager.AnalyzerIkSmart,
+ },
+ },
+ },
+ },
+ "mappings": esmanager.M{
+ "properties": esmanager.M{
+ FN.ESActress.Alias: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESActress.Desc: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESActress.Office: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESActress.Region: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESActress.Rank: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.Status: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.Works: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.HotValue: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.Real.WatchTimes: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.Real.WeekWatchs: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.Real.MonthWatchs: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESActress.DebutDate: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ FN.ESActress.DebutDate: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ },
+ },
+ }
+ if err := esmanager.Instance.InitIndices(esIndex, setting); err != nil {
+ panic(fmt.Sprintf("ElasticSearch Index %s initMediaIndex error", esIndex))
+ }
+}
diff --git a/pub/models/esmod/playlet.go b/pub/models/esmod/playlet.go
new file mode 100644
index 0000000..e49ae39
--- /dev/null
+++ b/pub/models/esmod/playlet.go
@@ -0,0 +1,112 @@
+package esmod
+
+import (
+ "fmt"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/playletmod"
+)
+
+// SearchPlaylet 根据关键字搜索短剧
+func SearchPlaylet(content string, from int64, size int64, sort string) (int64, []*ESPlaylet, *srvdef.SrvErr) {
+ fields := []string{
+ FN.ESPlaylet.Title,
+ FN.ESPlaylet.Desc,
+ FN.ESPlaylet.Anchor,
+ FN.ESPlaylet.CategoryNames,
+ FN.ESPlaylet.ThemeNames,
+ FN.ESPlaylet.TagNames,
+ }
+ must := esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields,
+ }},
+ {"term": esmanager.M{FN.ESPlaylet.Status: playletmod.StatusUp}},
+ },
+ },
+ }
+ switch sort {
+ case string(proto.SearchSortTypeHot):
+ must = esmanager.M{
+ "function_score": esmanager.M{
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields,
+ }},
+ {"term": esmanager.M{FN.ESPlaylet.Status: playletmod.StatusUp}},
+ },
+ },
+ },
+ "functions": esmanager.A{
+ esmanager.M{
+ "field_value_factor": esmanager.M{
+ "field": FN.ESPlaylet.CountBrowse,
+ "factor": 1.0,
+ "modifier": "none",
+ },
+ },
+ },
+ "score_mode": "sum",
+ "boost_mode": "multiply",
+ },
+ }
+ case string(proto.SearchSortTypeNew):
+ must = esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields,
+ }},
+ {"term": esmanager.M{FN.ESPlaylet.Status: playletmod.StatusUp}},
+ },
+ "should": esmanager.A{
+ {"range": esmanager.M{FN.ESPlaylet.OnlineAt: esmanager.M{"gte": "now-30d/d", "boost": 2.0}}},
+ {"range": esmanager.M{FN.ESPlaylet.OnlineAt: esmanager.M{"gte": "now-180d/d", "boost": 1.5}}},
+ {"range": esmanager.M{FN.ESPlaylet.OnlineAt: esmanager.M{"gte": "now-365d/d", "boost": 1.1}}},
+ },
+ "minimum_should_match": 0,
+ },
+ }
+ }
+
+ query := esmanager.M{
+ "query": must,
+ "sort": esmanager.A{
+ esmanager.M{"_score": esmanager.M{"order": "desc"}},
+ },
+ "from": from,
+ "size": size,
+ "terminate_after": 1000,
+ }
+
+ playletList := &ESPlayletHits{}
+ err := esmanager.Instance.Search(playletIndex, playletList, query)
+ if err != nil {
+ info := fmt.Sprintf("(esmod:SearchPlaylet) Search failed! err:%+v:", err)
+ log.Warn(info, log.Any("keywords", content), log.Any("from", from), log.Any("size", size))
+ return 0, nil, srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ total := int64(0)
+ result := make([]*ESPlaylet, 0)
+ if playletList != nil && playletList.Hits != nil && len(playletList.Hits) > 0 {
+ total = playletList.Total.Value
+ for _, item := range playletList.Hits {
+ if item != nil && item.Source != nil {
+ result = append(result, item.Source)
+ }
+ }
+ }
+ return total, result, nil
+}
diff --git a/pub/models/esmod/recordop.go b/pub/models/esmod/recordop.go
new file mode 100644
index 0000000..bb01d84
--- /dev/null
+++ b/pub/models/esmod/recordop.go
@@ -0,0 +1,144 @@
+package esmod
+
+import (
+ "context"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// InsertOneESRecord 插入ES操作记录
+func InsertOneESRecord(ctx context.Context, esRecord *ESRecord) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, esRecord); err != nil {
+ log.Warn("esmod:InsertOneESRecord db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneESRecord 删除一个ES操作记录
+func DelOneESRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("esmod:DelOnAppTag db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneESRecord 查找一个ES操作记录
+func FindOneESRecord(ctx context.Context, filter bson.M) (
+ *ESRecord, *srvdef.SrvErr) {
+ var esRecord ESRecord
+ if err := db.FindOneEx(
+ ctx, &esRecord, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("esmod:FindOnESRecord db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &esRecord, nil
+}
+
+// CountESRecords 统计ES操作记录
+func CountESRecords(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("esmod:CountMediaCats db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneESRecord 更新ES操作记录
+func UpdateOneESRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("esmod:UpdateOnESRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("esmod:UpdateOnESRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrMediaRecordNotExsit, "", "")
+ }
+ return nil
+}
+
+// UpsertOneESRecord 插入或更新一条ES操作记录
+func UpsertOneESRecord(ctx context.Context,
+ filter bson.M, update bson.M) *srvdef.SrvErr {
+ if _, err := db.UpsertOne(ctx, filter, update); err != nil {
+ log.Info("esmod:UpsertOneESRecord db.InsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// UpdateManyESRecord 更新一批ES操作记录
+func UpdateManyESRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("esmod:UpdateManyMediaCat db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("esmod:UpdateManyMediaCat db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrMediaTagNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyESRecord 查找一批ES操作记录
+func FindManyESRecord(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (ESRecordList, *srvdef.SrvErr) {
+ data := make(ESRecordList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("esmod:FindManyESRecord db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+func GetRecordTimeByESIndex(ctx context.Context, esIndex string) (*ESRecord, *srvdef.SrvErr) {
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ esRecord, serr := FindOneESRecord(ctx, filter)
+ if serr != nil {
+ if serr.Code != srvdef.ErrNoData {
+ return nil, serr
+ }
+ return nil, nil
+ }
+ return esRecord, nil
+}
+func SetOneEsRecordTime(ctx context.Context, esIndex string, recordTime time.Time) *srvdef.SrvErr {
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ upsertDoc := bson.M{
+ "$set": bson.M{
+ FN.ESRecord.RecordTime: recordTime,
+ FN.ESRecord.UpdatedAt: time.Now(),
+ },
+ "$setOnInsert": bson.M{
+ FN.ESRecord.Id: primitive.NewObjectID(),
+ FN.ESRecord.CreatedAt: time.Now(),
+ },
+ }
+ serr := UpsertOneESRecord(ctx, filter, upsertDoc)
+ return serr
+}
\ No newline at end of file
diff --git a/pub/models/esmod/tag.go b/pub/models/esmod/tag.go
new file mode 100755
index 0000000..8dc7be9
--- /dev/null
+++ b/pub/models/esmod/tag.go
@@ -0,0 +1,227 @@
+package esmod
+
+type TagESRecord struct {
+ CreatedAt string
+ EsIndex string
+ Id string
+ RecordTime string
+ Timestamp struct {
+ I string
+ T string
+ }
+ UpdatedAt string
+}
+type TagESVideo struct {
+ Id string
+ Title string
+ MediaType string
+ AddedTime string
+ CreatorName string
+ Keyworlds string
+ Desc string
+ Remark string
+ Status string
+ IsHot string
+ Sort string
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ UpdatedAt string
+ CreatedAt string
+
+ Keyworld string
+ CreatorId string
+ FirstTag string
+ SecondTags string
+ CoverImg string
+ Nodes string
+ Video string
+ CategoryIds string
+ Tags string
+ Real struct {
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ WeekWatchs string
+ MonthWatchs string
+ TodayWatchs string
+ }
+ IssuedAt string
+ IssuedBrand string
+ Bango string
+}
+type TagESPlaylet struct {
+ Id string
+ Status string
+ IsFeatured string
+ IsRecommend string
+ ShowOnBanner string
+ Banner string
+ OnlineAt string
+ CategoryIds string
+ CategoryNames string
+ ThemeIds string
+ ThemeNames string
+ Title string
+ Desc string
+ Anchor string
+ Cover string
+ TagIds string
+ TagNames string
+ TotalEpisode string
+ UpdateStatus string
+ CountBrowse string
+ HeatValue string
+ CountCollect string
+ CountLike string
+ SeoInfo string
+ UpdatedAt string
+ CreatedAt string
+}
+type TagESActress struct {
+ Id string
+ Alias string
+ Avatar string
+ Meas string
+ Office string
+ DebutDate string
+ BornDate string
+ Region string
+ Score string
+ Works string
+ HotValue string
+ Real struct {
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ WeekWatchs string
+ MonthWatchs string
+ TodayWatchs string
+ }
+ Desc string
+ Status string
+ CreatedAt string
+ UpdatedAt string
+ Rank string
+}
+
+var FN struct {
+ ESRecord TagESRecord
+ ESVideo TagESVideo
+ ESPlaylet TagESPlaylet
+ ESActress TagESActress
+}
+
+var TN struct {
+ ESRecord string
+ ESVideo string
+ ESPlaylet string
+ ESActress string
+}
+
+func init() {
+ FN.ESRecord = TagESRecord{
+ CreatedAt: "createdAt",
+ EsIndex: "esIndex",
+ Id: "_id",
+ RecordTime: "recordTime",
+ Timestamp: struct {
+ I string
+ T string
+ }{
+ I: "i",
+ T: "t",
+ },
+ UpdatedAt: "updatedAt",
+ }
+ FN.ESVideo = TagESVideo{
+ Id: "_id",
+ Title: "title",
+ Desc: "desc",
+ Status: "status",
+ AddedTime: "addedTime",
+ MediaType: "mediaType",
+ CreatorName: "creatorName",
+ Keyworlds: "keyworlds",
+ Remark: "remark",
+ IsHot: "isHot",
+ Sort: "sort",
+ UpdatedAt: "updatedAt",
+ CreatedAt: "createdAt",
+ WatchTimes: "watchTimes",
+ ShowTimes: "showTimes",
+ Comments: "comments",
+ IssuedAt: "issuedAt",
+ IssuedBrand: "issuedBrand",
+ Bango: "bango",
+ Real: struct {
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ WeekWatchs string
+ MonthWatchs string
+ TodayWatchs string
+ }{WatchTimes: "watchTimes", ShowTimes: "showTimes", Comments: "comments", WeekWatchs: "weekWatchs", MonthWatchs: "monthWatchs", TodayWatchs: "todayWatchs"},
+ }
+
+ FN.ESPlaylet = TagESPlaylet{
+ Id: "id",
+ Status: "status",
+ IsFeatured: "isFeatured",
+ IsRecommend: "isRecommend",
+ ShowOnBanner: "showOnBanner",
+ Banner: "banner",
+ OnlineAt: "onlineAt",
+ CategoryIds: "categoryIds",
+ CategoryNames: "categoryNames",
+ ThemeIds: "themeIds",
+ ThemeNames: "themeNames",
+ Title: "title",
+ Desc: "desc",
+ Anchor: "anchor",
+ Cover: "cover",
+ TagIds: "tagIds",
+ TagNames: "tagNames",
+ TotalEpisode: "totalEpisode",
+ UpdateStatus: "updateStatus",
+ CountBrowse: "countBrowse",
+ HeatValue: "heatValue",
+ CountCollect: "countCollect",
+ CountLike: "countLike",
+ SeoInfo: "seoInfo",
+ UpdatedAt: "updatedAt",
+ CreatedAt: "createdAt",
+ }
+
+ FN.ESActress = TagESActress{
+ Id: "_id",
+ Alias: "alias",
+ Avatar: "avatar",
+ Meas: "meas",
+ Office: "office",
+ DebutDate: "debutDate",
+ BornDate: "bornDate",
+ Region: "region",
+ Score: "score",
+ Works: "works",
+ HotValue: "hotValue",
+ Real: struct {
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ WeekWatchs string
+ MonthWatchs string
+ TodayWatchs string
+ }{WatchTimes: "watchTimes", ShowTimes: "showTimes", Comments: "comments", WeekWatchs: "weekWatchs", MonthWatchs: "monthWatchs", TodayWatchs: "todayWatchs"},
+ Desc: "desc",
+ Status: "status",
+ CreatedAt: "createdAt",
+ UpdatedAt: "updatedAt",
+ Rank: "rank",
+ }
+
+ TN.ESRecord = "es_record"
+ TN.ESVideo = "es_video"
+ TN.ESPlaylet = "es_playlet"
+ TN.ESActress = "es_actress"
+}
diff --git a/pub/models/esmod/type.go b/pub/models/esmod/type.go
new file mode 100644
index 0000000..ef95c0b
--- /dev/null
+++ b/pub/models/esmod/type.go
@@ -0,0 +1,172 @@
+//go:generate gen_bson_tag -m esmod
+//esmod essearch数据结构定义
+
+package esmod
+
+import (
+ "time"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ESTimestamp struct {
+ T uint32 `json:"t" bson:"t"`
+ I uint32 `json:"i" bson:"i"`
+}
+
+func (v *ESTimestamp) IsZero() bool {
+ if v == nil {
+ return true
+ }
+ return v.T == 0 && v.I == 0
+}
+
+// ESOperator es操作记录
+// @table:es_record
+type ESRecord struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"`
+ EsIndex string `json:"esIndex" bson:"esIndex"` //es索引名称
+ Timestamp ESTimestamp `json:"timestamp" bson:"timestamp"` //操作时间
+ RecordTime time.Time `json:"recordTime" bson:"recordTime"` // 操作记录时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间
+}
+
+type ESRecordList []*ESRecord
+
+// ESVideo es中存储的media数据
+// @table:es_video
+type ESVideo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ MediaType videoinformationmod.MediaType `json:"mediaType" bson:"mediaType"` // 资源类型
+ AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 帖子上架时间
+ CreatorName string `json:"creatorName" bson:"creatorName"` // 作者名称
+ Keyworlds string `bson:"keyworlds,omitempty" json:"keyworlds"` // 关键字
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Status pubdef.InformationStatus `json:"status" bson:"status"` // 帖子上架状态
+ IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Comments int64 `json:"comments" bson:"comments"` // 评论条数
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+ CreatorId int64 `json:"creatorId" bson:"creatorId"` // 作者Id
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ Nodes []videoinformationmod.Node `json:"nodes" bson:"nodes"` // 节点
+ Video videoinformationmod.VideoInfo `json:"video" bson:"video"` // 视频
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ Real pubdef.Flow `json:"real" bson:"real"` // 真实流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+ IssuedAt time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
+ IssuedBrand string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
+ Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+}
+
+type ESVideoList []*ESVideo
+type ESTotal struct {
+ Value int64 `json:"value"`
+}
+
+type ESVideoSource struct {
+ ID string `json:"_id"`
+ Score float32 `json:"_score"`
+ Source *ESVideo `json:"_source"`
+}
+
+// ESVideoHits search media hits
+type ESVideoHits struct {
+ Total ESTotal `json:"total"`
+ Hits []*ESVideoSource `json:"hits"`
+}
+
+// ESPlaylet es中存储的短剧搜索数据
+// @table:es_playlet
+type ESPlaylet struct {
+ Id int64 `json:"id" bson:"id"` // 短剧ID
+ Status playletmod.Status `json:"status" bson:"status"` // 状态: 0下架 1上架
+ IsFeatured bool `json:"isFeatured" bson:"isFeatured"` // 是否爆剧
+ IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 是否推荐
+ ShowOnBanner bool `json:"showOnBanner" bson:"showOnBanner"` // 是否显示在首页banner
+ Banner string `json:"banner" bson:"banner"` // banner图
+ OnlineAt time.Time `json:"onlineAt" bson:"onlineAt"` // 上架时间
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类ID集合
+ CategoryNames []string `json:"categoryNames" bson:"categoryNames"` // 分类名称集合
+ ThemeIds []int64 `json:"themeIds" bson:"themeIds"` // 题材ID集合
+ ThemeNames []string `json:"themeNames" bson:"themeNames"` // 题材名称集合
+ Title string `json:"title" bson:"title"` // 剧名
+ Desc string `json:"desc" bson:"desc"` // 简介
+ Anchor string `json:"anchor" bson:"anchor"` // 作者/演员
+ Cover string `json:"cover" bson:"cover"` // 封面
+ TagIds []int64 `json:"tagIds" bson:"tagIds"` // 标签ID集合
+ TagNames []string `json:"tagNames" bson:"tagNames"` // 标签名称集合
+ TotalEpisode int64 `json:"totalEpisode" bson:"totalEpisode"` // 总集数
+ UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 1-连载中 2-已完结
+ CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 观看量
+ HeatValue int64 `json:"heatValue" bson:"heatValue"` // 热度值
+ CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数
+ CountLike int64 `json:"countLike" bson:"countLike"` // 展示点赞数
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type ESPlayletList []*ESPlaylet
+
+type ESPlayletSource struct {
+ ID string `json:"_id"`
+ Score float32 `json:"_score"`
+ Source *ESPlaylet `json:"_source"`
+}
+
+// ESPlayletHits search playlet hits
+type ESPlayletHits struct {
+ Total ESTotal `json:"total"`
+ Hits []*ESPlayletSource `json:"hits"`
+}
+
+// ESActress es中存储的actress数据
+// @table:es_actress
+type ESActress struct {
+ Id int64 `json:"id" bson:"_id"` // Id
+ Alias string `bson:"alias" json:"alias"` // 别名
+ Avatar string `bson:"avatar" json:"avatar"` // 头像
+ Meas actress.Meas `bson:"meas" json:"meas"` // 三围
+ Desc string `bson:"desc" json:"desc"` // 详细描述
+ Office string `json:"office" bson:"office"` // 事务所
+ DebutDate time.Time `bson:"debutDate" json:"debutDate" ` // 出道日期
+ BornDate time.Time `bson:"bornDate" json:"bornDate"` // 出生日期
+ Region string `json:"region" bson:"region"` // 地区/国家
+ Score string `json:"score" bson:"score"` // 评分
+ Works int64 `json:"works" bson:"works"` // 作品数
+ HotValue int64 `json:"hotValue" bson:"hotValue"` // 热度值
+ Real pubdef.Flow `json:"real" bson:"real"` // 真实流量
+ Status bool `json:"status" bson:"status"` // 状态
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ TranslationCategoryId int64 `bson:"translationCategoryId" json:"translationCategoryId"` // 关联导航分类id
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+}
+
+type ESActressList []*ESActress
+
+type ESActressSource struct {
+ ID string `json:"_id"`
+ Score float32 `json:"_score"`
+ Source *ESActress `json:"_source"`
+}
+
+// ESActressHits search media hits
+type ESActressHits struct {
+ Total ESTotal `json:"total"`
+ Hits []*ESActressSource `json:"hits"`
+}
diff --git a/pub/models/esmod/video.go b/pub/models/esmod/video.go
new file mode 100644
index 0000000..2a154cb
--- /dev/null
+++ b/pub/models/esmod/video.go
@@ -0,0 +1,153 @@
+package esmod
+
+import (
+ "fmt"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+// SearchVideo 根据关键字搜索媒体
+func SearchVideo(content string, from int64, size int64, sort string, isAv bool) (int64, []*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
+ fields := []string{FN.ESVideo.Title, FN.ESVideo.Bango, FN.ESVideo.IssuedBrand, FN.ESVideo.Keyworlds, FN.ESVideo.Desc}
+ mediaType := videoinformationmod.MediaType_Video
+ if isAv {
+ mediaType = videoinformationmod.MediaType_AV
+ }
+ must := esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields},
+ },
+ {"term": esmanager.M{FN.ESVideo.Status: pubdef.InformationStatusVerifyOK}},
+ {"term": esmanager.M{FN.ESVideo.MediaType: mediaType}},
+ },
+ },
+ }
+ switch sort {
+ case string(proto.SearchSortTypeHot):
+ must = esmanager.M{
+ "function_score": esmanager.M{
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields,
+ }},
+ {"term": esmanager.M{FN.ESVideo.Status: pubdef.InformationStatusVerifyOK}},
+ {"term": esmanager.M{FN.ESVideo.MediaType: mediaType}},
+ },
+ },
+ },
+ "functions": esmanager.A{
+ esmanager.M{
+ "field_value_factor": esmanager.M{
+ "field": FN.ESVideo.WatchTimes,
+ "factor": 1.0,
+ "modifier": "none",
+ },
+ },
+ },
+ "score_mode": "sum",
+ "boost_mode": "multiply",
+ },
+ }
+ case string(proto.SearchSortTypeNew):
+ must = esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields},
+ },
+ {"term": esmanager.M{FN.ESVideo.Status: pubdef.InformationStatusVerifyOK}},
+ {"term": esmanager.M{FN.ESVideo.MediaType: mediaType}},
+ },
+ "should": esmanager.A{
+ {"range": esmanager.M{FN.ESVideo.AddedTime: esmanager.M{"gte": "now-30d/d", "boost": 2.0}}}, //7天内提升4倍权重
+ {"range": esmanager.M{FN.ESVideo.AddedTime: esmanager.M{"gte": "now-180d/d", "boost": 1.5}}}, // 30天内提升3倍权重
+ {"range": esmanager.M{FN.ESVideo.AddedTime: esmanager.M{"gte": "now-365d/d", "boost": 1.1}}}, // 90天内提升1.5倍权重
+ },
+ "minimum_should_match": 0, // 允许不匹配任何should条件
+ },
+ }
+ }
+
+ query := esmanager.M{
+ "query": must,
+ "sort": esmanager.A{
+ esmanager.M{"_score": esmanager.M{"order": "desc"}},
+ },
+ "from": from,
+ "size": size,
+ }
+
+ log.Info("(esmod:SearchVideo) 执行搜索",
+ log.Any("index", videoIndex),
+ log.Any("keywords", content),
+ log.Any("sort", sort),
+ log.Any("from", from),
+ log.Any("size", size))
+ videos := &ESVideoHits{}
+ err := esmanager.Instance.Search(videoIndex, videos, query)
+ if err != nil {
+ info := fmt.Sprintf(
+ "(esmod:SearchVideo) Search failed! err:%+v:", err)
+ log.Warn(info,
+ log.Any("keywords", content),
+ log.Any("from", from),
+ log.Any("size", size))
+ return 0, nil, srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ total := int64(0)
+ rslData := make([]*videoinformationmod.VideoInformation, 0)
+ if videos != nil && videos.Hits != nil && len(videos.Hits) > 0 {
+ total = videos.Total.Value
+ for _, video := range videos.Hits {
+ rslData = append(rslData, EsVideoInfoToData(video.Source))
+ }
+ }
+ log.Info("(esmod:SearchVideo) 搜索完成",
+ log.Any("index", videoIndex),
+ log.Any("keywords", content),
+ log.Any("total", total),
+ log.Any("hits", len(rslData)))
+ return total, rslData, nil
+}
+
+func EsVideoInfoToData(esInfo *ESVideo) *videoinformationmod.VideoInformation {
+ info := &videoinformationmod.VideoInformation{
+ Id: esInfo.Id,
+ Title: esInfo.Title,
+ MediaType: esInfo.MediaType,
+ AddedTime: esInfo.AddedTime,
+ CreatorId: esInfo.CreatorId,
+ CreatorName: esInfo.CreatorName,
+ Keyworld: esInfo.Keyworld,
+ FirstTag: esInfo.FirstTag,
+ SecondTags: esInfo.SecondTags,
+ CoverImg: esInfo.CoverImg,
+ Desc: esInfo.Desc,
+ Remark: esInfo.Remark,
+ Nodes: esInfo.Nodes,
+ Status: esInfo.Status,
+ Video: esInfo.Video,
+ Tags: esInfo.Tags,
+ IsHot: esInfo.IsHot,
+ Sort: esInfo.Sort,
+ Real: esInfo.Real,
+ VideoCount: esInfo.VideoCount,
+ UpdatedAt: esInfo.UpdatedAt,
+ CreatedAt: esInfo.CreatedAt,
+ }
+ return info
+}
diff --git a/pub/models/eventmod/index.go b/pub/models/eventmod/index.go
new file mode 100644
index 0000000..5e34736
--- /dev/null
+++ b/pub/models/eventmod/index.go
@@ -0,0 +1,47 @@
+package eventmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.Event)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Event.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Event.EventType, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Event.EventId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Event.Status, Value: 1},
+ {Key: FN.Event.RetryCount, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Event.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(eventmod:Init) Create Event Index failed! err:%+v:", err))
+ }
+}
diff --git a/pub/models/eventmod/model.go b/pub/models/eventmod/model.go
new file mode 100644
index 0000000..fa91bf7
--- /dev/null
+++ b/pub/models/eventmod/model.go
@@ -0,0 +1,130 @@
+package eventmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// InsertOneEvent 插入一个事件
+func InsertOneEvent(ctx context.Context, event *Event) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, event); err != nil {
+ log.Warn("eventmod:InsertOneEvent db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// InsertManyEvent 批量插入事件
+func InsertManyEvent(ctx context.Context, events *EventList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, events); err != nil {
+ log.Warn("eventmod:InsertManyEvent db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneEvent 删除一个事件
+func DelOneEvent(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("eventmod:DelOneEvent db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyEvent 批量删除事件
+func DelManyEvent(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("eventmod:DelManyEvent db.DeleteMany failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneEvent 查找一个事件
+func FindOneEvent(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*Event, *srvdef.SrvErr) {
+ var event Event
+ if err := db.FindOneEx(ctx, &event, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("eventmod:FindOneEvent db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &event, nil
+}
+
+// FindManyEvent 查找多个事件
+func FindManyEvent(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (EventList, *srvdef.SrvErr) {
+ data := make(EventList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("eventmod:FindManyEvent db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountEvent 统计事件数量
+func CountEvent(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("eventmod:CountEvent db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneEvent 更新一个事件
+func UpdateOneEvent(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("eventmod:UpdateOneEvent db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("eventmod:UpdateOneEvent db.UpdateOne not matched",
+ log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyEvent 批量更新事件
+func UpdateManyEvent(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("eventmod:UpdateManyEvent db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("eventmod:UpdateManyEvent db.UpdateMany not matched",
+ log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpsertOneEvent 插入或更新一个事件
+func UpsertOneEvent(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ opts := options.Update().SetUpsert(true)
+ if _, err := db.UpdateOne(ctx, filter, update, opts); err != nil {
+ log.Warn("eventmod:UpsertOneEvent db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/eventmod/tag.go b/pub/models/eventmod/tag.go
new file mode 100644
index 0000000..12047d7
--- /dev/null
+++ b/pub/models/eventmod/tag.go
@@ -0,0 +1,31 @@
+package eventmod
+
+type TagEvent struct {
+ ID string //
+ CreatedAt string // 创建事件
+ RetryCount string // 重试次数
+ Status string //
+ Data string //
+ EventType string //
+ EventId string //
+ UpdatedAt string // 修改时间
+}
+
+var FN struct {
+ Event TagEvent
+}
+var TN struct {
+ Event string
+}
+
+func init() {
+ FN.Event.ID = "_id"
+ FN.Event.CreatedAt = "createdAt"
+ FN.Event.RetryCount = "retryCount"
+ FN.Event.Status = "status"
+ FN.Event.Data = "data"
+ FN.Event.EventType = "eventType"
+ FN.Event.EventId = "eventId"
+ FN.Event.UpdatedAt = "updatedAt"
+ TN.Event = "event"
+}
diff --git a/pub/models/eventmod/type.go b/pub/models/eventmod/type.go
new file mode 100644
index 0000000..7ddfa74
--- /dev/null
+++ b/pub/models/eventmod/type.go
@@ -0,0 +1,32 @@
+package eventmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type EventStatus int
+
+const (
+ Default EventStatus = 0 // 默认状态,等待上报
+ Success EventStatus = 1 // 成功
+ Failure EventStatus = 2 // 失败
+)
+
+//go:generate fox mongo --model
+//go:generate fox mongo --tag
+//go:generate fox mongo --update
+
+// @table:event
+// @title:事件表
+type Event struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ EventId string `json:"eventId" bson:"eventId"`
+ EventType string `json:"eventType" bson:"eventType"`
+ Data string `json:"data" bson:"data"`
+ Status EventStatus `json:"status" bson:"status"` //
+ RetryCount int `json:"retryCount" bson:"retryCount"` // 重试次数
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建事件
+ UpdatedAt time.Time `bson:"updatedAt"` // 修改时间
+}
diff --git a/pub/models/eventmod/update.go b/pub/models/eventmod/update.go
new file mode 100644
index 0000000..f52d492
--- /dev/null
+++ b/pub/models/eventmod/update.go
@@ -0,0 +1,53 @@
+package eventmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type EventList []*Event
+type EventUpdate struct {
+ RetryCount *int `json:"retryCount" bson:"retryCount,omitempty"` // 重试次数
+ Status *EventStatus `json:"status" bson:"status,omitempty"` //
+ Data *string `json:"data" bson:"data,omitempty"` //
+ EventType *string `json:"eventType" bson:"eventType,omitempty"` //
+ EventId *string `json:"eventId" bson:"eventId,omitempty"` //
+ UpdatedAt *time.Time `json:"" bson:"updatedAt,omitempty"` // 修改时间
+}
+type EventQuery struct {
+ ID *primitive.ObjectID `json:"" bson:"_id,omitempty"` //
+ RetryCount *int `json:"retryCount" bson:"retryCount,omitempty"` // 重试次数
+ Status *EventStatus `json:"status" bson:"status,omitempty"` //
+ Data *string `json:"data" bson:"data,omitempty"` //
+ EventType *string `json:"eventType" bson:"eventType,omitempty"` //
+ EventId *string `json:"eventId" bson:"eventId,omitempty"` //
+ UpdatedAt *time.Time `json:"" bson:"updatedAt,omitempty"` // 修改时间
+}
+
+func (query *EventQuery) GetQueryFilter() bson.M {
+ filter := bson.M{}
+ if query.ID != nil {
+ filter[FN.Event.ID] = *query.ID
+ }
+ if query.RetryCount != nil {
+ filter[FN.Event.RetryCount] = *query.RetryCount
+ }
+ if query.Status != nil {
+ filter[FN.Event.Status] = *query.Status
+ }
+ if query.Data != nil {
+ filter[FN.Event.Data] = bson.M{"$regex": primitive.Regex{Pattern: *query.Data, Options: "i"}}
+ }
+ if query.EventType != nil {
+ filter[FN.Event.EventType] = bson.M{"$regex": primitive.Regex{Pattern: *query.EventType, Options: "i"}}
+ }
+ if query.EventId != nil {
+ filter[FN.Event.EventId] = bson.M{"$regex": primitive.Regex{Pattern: *query.EventId, Options: "i"}}
+ }
+ if query.UpdatedAt != nil {
+ filter[FN.Event.UpdatedAt] = *query.UpdatedAt
+ }
+ return filter
+}
diff --git a/pub/models/homebannermod/banner.go b/pub/models/homebannermod/banner.go
new file mode 100644
index 0000000..a7d141f
--- /dev/null
+++ b/pub/models/homebannermod/banner.go
@@ -0,0 +1,135 @@
+package homebannermod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 home_banner 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.HomeBanner)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.HomeBanner.Status, Value: 1},
+ {Key: FN.HomeBanner.UpdatedAt, Value: -1},
+ {Key: FN.HomeBanner.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.HomeBanner.Name, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.HomeBanner.CreatedAt, Value: -1},
+ {Key: FN.HomeBanner.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.HomeBanner.Status, Value: 1},
+ {Key: FN.HomeBanner.CreatedAt, Value: -1},
+ {Key: FN.HomeBanner.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.HomeBanner.Status, Value: 1},
+ {Key: FN.HomeBanner.Rank, Value: -1},
+ {Key: FN.HomeBanner.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(homebannermod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneHomeBanner 插入一条首页 banner 数据
+func InsertOneHomeBanner(ctx context.Context, doc *HomeBanner) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.HomeBanner.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("homebannermod:InsertOneHomeBanner failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneHomeBanner 删除一条首页 banner 数据
+func DelOneHomeBanner(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("homebannermod:DelOneHomeBanner failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneHomeBanner 查询一条首页 banner 数据
+func FindOneHomeBanner(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*HomeBanner, *srvdef.SrvErr) {
+ var doc HomeBanner
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("homebannermod:FindOneHomeBanner failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyHomeBanner 按条件查询多条首页 banner 数据
+func FindManyHomeBanner(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (HomeBannerList, *srvdef.SrvErr) {
+ data := make(HomeBannerList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("homebannermod:FindManyHomeBanner failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountHomeBanner 统计满足条件的首页 banner 数量
+func CountHomeBanner(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("homebannermod:CountHomeBanner failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneHomeBanner 更新一条首页 banner 数据
+func UpdateOneHomeBanner(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("homebannermod:UpdateOneHomeBanner failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/homebannermod/tag.go b/pub/models/homebannermod/tag.go
new file mode 100644
index 0000000..caa5413
--- /dev/null
+++ b/pub/models/homebannermod/tag.go
@@ -0,0 +1,41 @@
+package homebannermod
+
+// TagHomeBanner 保存 home_banner 集合字段名
+type TagHomeBanner struct {
+ Id string
+ Name string
+ Image string
+ VerticalImage string
+ Url string
+ Rank string
+ Status string
+ CreatedById string
+ CreatedByName string
+ UpdatedByName string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ HomeBanner TagHomeBanner
+}
+
+var TN struct {
+ HomeBanner string
+}
+
+func init() {
+ FN.HomeBanner.Id = "_id"
+ FN.HomeBanner.Name = "name"
+ FN.HomeBanner.Image = "image"
+ FN.HomeBanner.VerticalImage = "verticalImage"
+ FN.HomeBanner.Url = "url"
+ FN.HomeBanner.Rank = "rank"
+ FN.HomeBanner.Status = "status"
+ FN.HomeBanner.CreatedById = "createdById"
+ FN.HomeBanner.CreatedByName = "createdByName"
+ FN.HomeBanner.UpdatedByName = "updatedByName"
+ FN.HomeBanner.CreatedAt = "createdAt"
+ FN.HomeBanner.UpdatedAt = "updatedAt"
+ TN.HomeBanner = "home_banner"
+}
diff --git a/pub/models/homebannermod/type.go b/pub/models/homebannermod/type.go
new file mode 100644
index 0000000..4d9fe3b
--- /dev/null
+++ b/pub/models/homebannermod/type.go
@@ -0,0 +1,24 @@
+package homebannermod
+
+import "time"
+
+// HomeBanner @table: home_banner
+//
+// HomeBanner 首页 banner 配置集合模型
+type HomeBanner struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // ID
+ Name string `json:"name" bson:"name"` // 名称
+ Image string `json:"image" bson:"image"` // 图片
+ VerticalImage string `json:"verticalImage" bson:"verticalImage"` // 竖版图片
+ Url string `json:"url" bson:"url"` // URL
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ Status bool `json:"status" bson:"status"` // 状态 true-启用 false-停用
+ CreatedById int64 `json:"createdById" bson:"createdById"` // 创建人ID
+ CreatedByName string `json:"createdByName" bson:"createdByName"` // 创建人名称
+ UpdatedByName string `json:"updatedByName" bson:"updatedByName"` // 修改人名称
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// HomeBannerList 首页 banner 列表
+type HomeBannerList []*HomeBanner
diff --git a/pub/models/hotnavmod/hotnav.go b/pub/models/hotnavmod/hotnav.go
new file mode 100644
index 0000000..6adc314
--- /dev/null
+++ b/pub/models/hotnavmod/hotnav.go
@@ -0,0 +1,101 @@
+package hotnavmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.HotNav)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.HotNav.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.HotNav.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(hotnavmod:Init) Create HotNav Index failed! err:%+v:", err))
+ }
+}
+
+func InsertOneHotNav(ctx context.Context, doc *HotNav) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, doc); err != nil {
+ log.Warn("hotnavmod:InsertOneHotNav db.InsertOne failed",
+ log.E(err), log.Any("doc", doc))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+func DelOneHotNav(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("hotnavmod:DelOneHotNav db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+func FindOneHotNav(ctx context.Context, filter bson.M) (*HotNav, *srvdef.SrvErr) {
+ var doc HotNav
+ if err := db.FindOneEx(ctx, &doc, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("hotnavmod:FindOneHotNav db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+func FindManyHotNav(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (HotNavList, *srvdef.SrvErr) {
+ data := make(HotNavList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("hotnavmod:FindManyHotNav db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+func CountHotNav(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("hotnavmod:CountHotNav db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+func UpdateOneHotNav(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("hotnavmod:UpdateOneHotNav db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("hotnavmod:UpdateOneHotNav not matched",
+ log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/hotnavmod/tag.go b/pub/models/hotnavmod/tag.go
new file mode 100644
index 0000000..f8c821a
--- /dev/null
+++ b/pub/models/hotnavmod/tag.go
@@ -0,0 +1,34 @@
+package hotnavmod
+
+type TagHotNav struct {
+ CreatedAt string
+ Icon string
+ Id string
+ Intro string
+ Sort string
+ Status string
+ Title string
+ UpdatedAt string
+ Url string
+}
+
+var FN struct {
+ HotNav TagHotNav
+}
+
+var TN struct {
+ HotNav string
+}
+
+func init() {
+ FN.HotNav.CreatedAt = "createdAt"
+ FN.HotNav.Icon = "icon"
+ FN.HotNav.Id = "_id"
+ FN.HotNav.Intro = "intro"
+ FN.HotNav.Sort = "sort"
+ FN.HotNav.Status = "status"
+ FN.HotNav.Title = "title"
+ FN.HotNav.UpdatedAt = "updatedAt"
+ FN.HotNav.Url = "url"
+ TN.HotNav = "hot_nav"
+}
diff --git a/pub/models/hotnavmod/type.go b/pub/models/hotnavmod/type.go
new file mode 100644
index 0000000..20c2357
--- /dev/null
+++ b/pub/models/hotnavmod/type.go
@@ -0,0 +1,19 @@
+package hotnavmod
+
+import "time"
+
+// HotNav 热门导航配置
+// @table:hot_nav
+type HotNav struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // ID
+ Title string `json:"title" bson:"title"` // 标题
+ Intro string `json:"intro" bson:"intro"` // 介绍
+ Url string `json:"url" bson:"url"` // 链接
+ Icon string `json:"icon" bson:"icon"` // 图标
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ Status bool `json:"status" bson:"status"` // 是否启用
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+
+type HotNavList []*HotNav
diff --git a/pub/models/ipblackmod/ipblack.go b/pub/models/ipblackmod/ipblack.go
new file mode 100644
index 0000000..c60960d
--- /dev/null
+++ b/pub/models/ipblackmod/ipblack.go
@@ -0,0 +1,109 @@
+package ipblackmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.IPBlackModel)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.IPBlackModel.Ip, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(IPBlackModel:InitIPBlackModel) Create IPBlackModel Index failed! err:%+v:", err))
+ }
+}
+
+// CountIPBlackModel 统计活动数量
+func CountIPBlackModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("IPBlackModelmod:CountIPBlackModel db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// InsertOneIPBlackModel 插入一条活动
+func InsertOneIPBlackModel(ctx context.Context, IPBlackModel *IPBlackModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, IPBlackModel); err != nil {
+ log.Info("IPBlackModelmod:InsertOneIPBlackModel db.InsertOne failed",
+ log.E(err), log.Any("IPBlackModel", IPBlackModel))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneIPBlackModel 删除一个活动
+func DelOneIPBlackModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("IPBlackModelmod:DelOneIPBlackModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneIPBlackModel 更新一个IPBlackModel
+func UpdateOneIPBlackModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("IPBlackModelmod:UpdateOneIPBlackModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("IPBlackModelmod:UpdateOneIPBlackModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ObjectNotExist, "", "")
+ }
+ return nil
+}
+
+// FindManyIPBlackModel 查找一批活动数据
+func FindManyIPBlackModel(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (IPBlackModelList, *srvdef.SrvErr) {
+ data := make(IPBlackModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("IPBlackModelmod:FindManyAcivities db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindOneIPBlackModel 查找一个媒体
+func FindOneIPBlackModel(ctx context.Context, filter bson.M) (
+ *IPBlackModel, *srvdef.SrvErr) {
+ var IPBlackModel IPBlackModel
+ if err := db.FindOneEx(ctx, &IPBlackModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("IPBlackModel:FindOneIPBlackModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ObjectDayStatDoesNotExist, "", "")
+ }
+ return &IPBlackModel, nil
+}
\ No newline at end of file
diff --git a/pub/models/ipblackmod/tag.go b/pub/models/ipblackmod/tag.go
new file mode 100755
index 0000000..ff235ed
--- /dev/null
+++ b/pub/models/ipblackmod/tag.go
@@ -0,0 +1,26 @@
+package ipblackmod
+
+type TagIPBlackModel struct {
+ CreatedAt string
+ ID string
+ Ip string
+ Desc string
+ UpdatedAt string
+}
+
+var FN struct {
+ IPBlackModel TagIPBlackModel
+}
+
+var TN struct {
+ IPBlackModel string
+}
+
+func init() {
+ FN.IPBlackModel.CreatedAt = "createdAt"
+ FN.IPBlackModel.ID = "_id"
+ FN.IPBlackModel.Ip = "ip"
+ FN.IPBlackModel.UpdatedAt = "updatedAt"
+ FN.IPBlackModel.Desc = "desc"
+ TN.IPBlackModel = "ip_black"
+}
\ No newline at end of file
diff --git a/pub/models/ipblackmod/type.go b/pub/models/ipblackmod/type.go
new file mode 100644
index 0000000..be9f72f
--- /dev/null
+++ b/pub/models/ipblackmod/type.go
@@ -0,0 +1,18 @@
+package ipblackmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 资源同步
+// @table:ip_black
+type IPBlackModel struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Ip string `json:"ip" bson:"ip"`
+ Desc string `json:"desc" bson:"desc"` // 备注
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+type IPBlackModelList []*IPBlackModel
\ No newline at end of file
diff --git a/pub/models/ipdailystatmod/ipdailystat.go b/pub/models/ipdailystatmod/ipdailystat.go
new file mode 100644
index 0000000..c9678d0
--- /dev/null
+++ b/pub/models/ipdailystatmod/ipdailystat.go
@@ -0,0 +1,62 @@
+package ipdailystatmod
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.IpDailyStat)
+
+ // 唯一索引:(appId, date)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.IpDailyStat.AppId, Value: 1},
+ {Key: FN.IpDailyStat.Date, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ _, _ = db.CreateIndex(ctx, indexes)
+}
+
+// UpsertCount 写入或更新指定 appId 当天的 IP 去重数
+func UpsertCount(ctx context.Context, appId int64, date string, count int64) error {
+ filter := bson.M{
+ FN.IpDailyStat.AppId: appId,
+ FN.IpDailyStat.Date: date,
+ }
+ update := bson.M{
+ "$set": bson.M{
+ FN.IpDailyStat.Count: count,
+ FN.IpDailyStat.UpdatedAt: time.Now(),
+ },
+ }
+ _, err := db.UpsertOne(ctx, filter, update)
+ return err
+}
+
+// FindCount 查询指定 appId 在某天的 IP 去重数,未找到返回 0
+func FindCount(ctx context.Context, appId int64, date string) (int64, error) {
+ filter := bson.M{
+ FN.IpDailyStat.AppId: appId,
+ FN.IpDailyStat.Date: date,
+ }
+ var stat IpDailyStat
+ err := db.FindOneEx(ctx, &stat, filter)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return 0, nil
+ }
+ return 0, err
+ }
+ return stat.Count, nil
+}
diff --git a/pub/models/ipdailystatmod/tag.go b/pub/models/ipdailystatmod/tag.go
new file mode 100644
index 0000000..f85fbc6
--- /dev/null
+++ b/pub/models/ipdailystatmod/tag.go
@@ -0,0 +1,25 @@
+package ipdailystatmod
+
+type tagIpDailyStat struct {
+ AppId string
+ Date string
+ Count string
+ UpdatedAt string
+}
+
+var FN struct {
+ IpDailyStat tagIpDailyStat
+}
+
+var TN struct {
+ IpDailyStat string
+}
+
+func init() {
+ FN.IpDailyStat.AppId = "appId"
+ FN.IpDailyStat.Date = "date"
+ FN.IpDailyStat.Count = "count"
+ FN.IpDailyStat.UpdatedAt = "updatedAt"
+
+ TN.IpDailyStat = "ip_daily_stat"
+}
diff --git a/pub/models/ipdailystatmod/type.go b/pub/models/ipdailystatmod/type.go
new file mode 100644
index 0000000..54bd3c5
--- /dev/null
+++ b/pub/models/ipdailystatmod/type.go
@@ -0,0 +1,13 @@
+package ipdailystatmod
+
+import "time"
+
+// IpDailyStat 每日访客 IP 去重统计
+// app 端写入,web 端读取,通过共享 MongoDB 跨进程共享数据。
+// @table:ip_daily_stat
+type IpDailyStat struct {
+ AppId int64 `bson:"appId"` // 站点 ID
+ Date string `bson:"date"` // 日期,格式 "20060102"
+ Count int64 `bson:"count"` // 当日去重 IP 数
+ UpdatedAt time.Time `bson:"updatedAt"` // 最后同步时间
+}
diff --git a/pub/models/m3u8requestlogmod/m3u8_request_log.go b/pub/models/m3u8requestlogmod/m3u8_request_log.go
new file mode 100644
index 0000000..bec15ce
--- /dev/null
+++ b/pub/models/m3u8requestlogmod/m3u8_request_log.go
@@ -0,0 +1,73 @@
+package m3u8requestlogmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.M3U8RequestLog)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.VideoId, Value: 1},
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.M3U8Path, Value: 1},
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.ClientIP, Value: 1},
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.Host, Value: 1},
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.Status, Value: 1},
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.M3U8RequestLog.TokenOK, Value: 1},
+ {Key: FN.M3U8RequestLog.PathOK, Value: 1},
+ {Key: FN.M3U8RequestLog.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(m3u8requestlogmod:Init) Create M3U8RequestLog Index failed! err:%+v:", err))
+ }
+}
+
+func InsertOneM3U8RequestLog(ctx context.Context, doc *M3U8RequestLog) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, doc); err != nil {
+ log.Warn("m3u8requestlogmod:InsertOneM3U8RequestLog db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/m3u8requestlogmod/tag.go b/pub/models/m3u8requestlogmod/tag.go
new file mode 100644
index 0000000..3aa5462
--- /dev/null
+++ b/pub/models/m3u8requestlogmod/tag.go
@@ -0,0 +1,66 @@
+package m3u8requestlogmod
+
+type TagM3U8RequestLog struct {
+ CDNDomain string
+ CFConnectingIP string
+ ClientIP string
+ CostMs string
+ CreatedAt string
+ Error string
+ Headers string
+ Host string
+ Id string
+ IsOverseas string
+ M3U8Path string
+ PathOK string
+ Query string
+ RawQuery string
+ Referer string
+ RemoteAddr string
+ RequestURI string
+ Status string
+ T string
+ Token string
+ TokenOK string
+ UserAgent string
+ VideoId string
+ XForwardedFor string
+ XRealIP string
+}
+
+var FN struct {
+ M3U8RequestLog TagM3U8RequestLog
+}
+
+var TN struct {
+ M3U8RequestLog string
+}
+
+func init() {
+ FN.M3U8RequestLog.CDNDomain = "cdnDomain"
+ FN.M3U8RequestLog.CFConnectingIP = "cfConnectingIP"
+ FN.M3U8RequestLog.ClientIP = "clientIP"
+ FN.M3U8RequestLog.CostMs = "costMs"
+ FN.M3U8RequestLog.CreatedAt = "createdAt"
+ FN.M3U8RequestLog.Error = "error"
+ FN.M3U8RequestLog.Headers = "headers"
+ FN.M3U8RequestLog.Host = "host"
+ FN.M3U8RequestLog.Id = "_id"
+ FN.M3U8RequestLog.IsOverseas = "isOverseas"
+ FN.M3U8RequestLog.M3U8Path = "m3u8Path"
+ FN.M3U8RequestLog.PathOK = "pathOK"
+ FN.M3U8RequestLog.Query = "query"
+ FN.M3U8RequestLog.RawQuery = "rawQuery"
+ FN.M3U8RequestLog.Referer = "referer"
+ FN.M3U8RequestLog.RemoteAddr = "remoteAddr"
+ FN.M3U8RequestLog.RequestURI = "requestURI"
+ FN.M3U8RequestLog.Status = "status"
+ FN.M3U8RequestLog.T = "t"
+ FN.M3U8RequestLog.Token = "token"
+ FN.M3U8RequestLog.TokenOK = "tokenOK"
+ FN.M3U8RequestLog.UserAgent = "userAgent"
+ FN.M3U8RequestLog.VideoId = "videoId"
+ FN.M3U8RequestLog.XForwardedFor = "xForwardedFor"
+ FN.M3U8RequestLog.XRealIP = "xRealIP"
+ TN.M3U8RequestLog = "m3u8_request_log"
+}
diff --git a/pub/models/m3u8requestlogmod/type.go b/pub/models/m3u8requestlogmod/type.go
new file mode 100644
index 0000000..338ebb6
--- /dev/null
+++ b/pub/models/m3u8requestlogmod/type.go
@@ -0,0 +1,37 @@
+package m3u8requestlogmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// M3U8RequestLog records every HLS playlist request for hotlink analysis.
+// @table:m3u8_request_log
+type M3U8RequestLog struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ CreatedAt time.Time `bson:"createdAt"`
+ Host string `bson:"host"`
+ RequestURI string `bson:"requestURI"`
+ M3U8Path string `bson:"m3u8Path"`
+ RawQuery string `bson:"rawQuery"`
+ Query map[string][]string `bson:"query"`
+ Headers map[string][]string `bson:"headers"`
+ VideoId int64 `bson:"videoId"`
+ T int64 `bson:"t"`
+ Token string `bson:"token"`
+ ClientIP string `bson:"clientIP"`
+ RemoteAddr string `bson:"remoteAddr"`
+ XForwardedFor string `bson:"xForwardedFor"`
+ XRealIP string `bson:"xRealIP"`
+ CFConnectingIP string `bson:"cfConnectingIP"`
+ UserAgent string `bson:"userAgent"`
+ Referer string `bson:"referer"`
+ IsOverseas bool `bson:"isOverseas"`
+ CDNDomain string `bson:"cdnDomain"`
+ TokenOK bool `bson:"tokenOK"`
+ PathOK bool `bson:"pathOK"`
+ Status string `bson:"status"`
+ Error string `bson:"error"`
+ CostMs int64 `bson:"costMs"`
+}
diff --git a/pub/models/models.go b/pub/models/models.go
new file mode 100644
index 0000000..321b2f8
--- /dev/null
+++ b/pub/models/models.go
@@ -0,0 +1,202 @@
+package models
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/announcementmod"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/contactusmod"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/detailnavmod"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/homebannermod"
+ "videoweb/pub/models/hotnavmod"
+ "videoweb/pub/models/ipblackmod"
+ "videoweb/pub/models/ipdailystatmod"
+ "videoweb/pub/models/m3u8requestlogmod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/oplog"
+ "videoweb/pub/models/playletbackflowstatmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletfavoritemod"
+ "videoweb/pub/models/playlethomeconfigmod"
+ "videoweb/pub/models/playletlikemod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/questionnairemod"
+ "videoweb/pub/models/richtextcontentmod"
+ "videoweb/pub/models/seopushmod"
+ "videoweb/pub/models/syncfilemod"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/models/userlogin"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videocollectionmediamod"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videofavoritemod"
+ "videoweb/pub/mongos"
+
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videolikemod"
+ "videoweb/pub/models/videotagmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func Init() {
+ log.Info("models init start..")
+ ctx := context.Background()
+ cfg.Init(ctx)
+ mwacc.Init(ctx)
+ oplog.Init(ctx)
+ m3u8requestlogmod.Init(ctx)
+
+ videocategorymod.Init(ctx)
+ videoinformationmod.Init(ctx)
+ videofavoritemod.Init(ctx)
+ videolikemod.Init(ctx)
+ videotagmod.Init(ctx)
+
+ translationcategorymod.Init(ctx)
+ translationmod.Init(ctx)
+ contenttagmod.Init(ctx)
+ playletcategorymod.Init(ctx)
+ playlethomeconfigmod.Init(ctx)
+ playlettagmod.Init(ctx)
+ playletthememod.Init(ctx)
+ playletmod.Init(ctx)
+ playletbackflowstatmod.Init(ctx)
+ playletchaptermod.Init(ctx)
+ playletlikemod.Init(ctx)
+ playletfavoritemod.Init(ctx)
+ initWatchHistory(ctx)
+ videocategorymod.Init(ctx)
+ videocreatermod.Init(ctx)
+ videotagmod.Init(ctx)
+ videoinformationmod.Init(ctx)
+ videocreatermod.Init(ctx)
+ syncfilemod.Init(ctx)
+ seopushmod.Init(ctx)
+ richtextcontentmod.Init(ctx)
+ announcementmod.Init(ctx)
+ commentmod.Init(ctx)
+ commentlikemod.Init(ctx)
+ videocollectionmediamod.Init(ctx)
+ ipblackmod.Init(ctx)
+ esmod.Init(ctx)
+ actress.Init(ctx)
+ contactusmod.Init(ctx)
+ detailnavmod.Init(ctx)
+ hotnavmod.Init(ctx)
+ homebannermod.Init(ctx)
+ eventmod.Init(ctx)
+ usermod.Init(ctx)
+ userlogin.Init(ctx)
+ questionnairemod.Init(ctx)
+ ipdailystatmod.Init(ctx)
+ log.Info("models init end")
+}
+
+const (
+ WatchHistoryTypePlaylet int64 = 1
+ WatchHistoryTypeVideo int64 = 2
+ WatchHistoryRetentionDays = 60
+ watchHistoryCollectionName = "watch_history"
+ WatchHistoryCollectionName = watchHistoryCollectionName
+ WatchHistoryUserID = "userId"
+ WatchHistoryDataType = "dataType"
+ WatchHistoryContentID = "contentId"
+ WatchHistoryChapterID = "chapterId"
+ WatchHistoryEpisode = "episode"
+ WatchHistoryCreatedAt = "createdAt"
+ WatchHistoryUpdatedAt = "updatedAt"
+)
+
+// WatchHistory 保存当前用户最近观看的内容
+type WatchHistory struct {
+ UserID int64 `bson:"userId"`
+ DataType int64 `bson:"dataType"`
+ ContentID int64 `bson:"contentId"`
+ ChapterID int64 `bson:"chapterId"`
+ Episode int64 `bson:"episode"`
+ CreatedAt time.Time `bson:"createdAt"`
+ UpdatedAt time.Time `bson:"updatedAt"`
+}
+
+var watchHistoryColl *mongos.Collection
+
+// initWatchHistory 初始化观看历史及 60 天 TTL 索引
+func initWatchHistory(ctx context.Context) {
+ watchHistoryColl = mongos.Shop.Coll(watchHistoryCollectionName)
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: WatchHistoryUserID, Value: 1}, {Key: WatchHistoryDataType, Value: 1}, {Key: WatchHistoryContentID, Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: WatchHistoryUserID, Value: 1}, {Key: WatchHistoryDataType, Value: 1}, {Key: WatchHistoryUpdatedAt, Value: -1}, {Key: "_id", Value: -1}}},
+ {Keys: bson.D{{Key: WatchHistoryUpdatedAt, Value: 1}}, Options: options.Index().SetExpireAfterSeconds(WatchHistoryRetentionDays * 24 * 60 * 60)},
+ }
+ if _, err := watchHistoryColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(models:initWatchHistory) Create Index failed! err:%+v:", err))
+ }
+}
+
+// TouchWatchHistory 从已验证的播放清单刷新历史
+func TouchWatchHistory(ctx context.Context, item WatchHistory) *srvdef.SrvErr {
+ filter := bson.M{WatchHistoryUserID: item.UserID, WatchHistoryDataType: item.DataType, WatchHistoryContentID: item.ContentID}
+ if item.DataType == WatchHistoryTypePlaylet {
+ sameChapter := bson.M{WatchHistoryUserID: item.UserID, WatchHistoryDataType: item.DataType, WatchHistoryContentID: item.ContentID, WatchHistoryChapterID: item.ChapterID}
+ result, err := watchHistoryColl.UpdateOne(ctx, sameChapter, bson.M{"$set": bson.M{WatchHistoryUpdatedAt: item.UpdatedAt, WatchHistoryEpisode: item.Episode}})
+ if err != nil {
+ log.Warn("TouchWatchHistory failed", log.Any("filter", sameChapter), log.E(err))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount > 0 {
+ return nil
+ }
+ }
+ update := bson.M{
+ "$set": bson.M{WatchHistoryChapterID: item.ChapterID, WatchHistoryEpisode: item.Episode, WatchHistoryUpdatedAt: item.UpdatedAt},
+ "$setOnInsert": bson.M{WatchHistoryUserID: item.UserID, WatchHistoryDataType: item.DataType, WatchHistoryContentID: item.ContentID, WatchHistoryCreatedAt: item.CreatedAt},
+ }
+ if _, err := watchHistoryColl.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true)); err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ _, err = watchHistoryColl.UpdateOne(ctx, filter, bson.M{"$set": update["$set"]})
+ if err == nil {
+ return nil
+ }
+ }
+ log.Warn("TouchWatchHistory failed", log.Any("filter", filter), log.E(err))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// AggregateWatchHistory 聚合查询观看历史
+func AggregateWatchHistory(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := watchHistoryColl.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AggregateWatchHistory failed", log.Any("pipeline", pipeline), log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// DeleteWatchHistory 删除当前用户指定类型的观看历史
+func DeleteWatchHistory(ctx context.Context, userID, dataType int64) (int64, *srvdef.SrvErr) {
+ filter := bson.M{WatchHistoryUserID: userID, WatchHistoryDataType: dataType}
+ count, err := watchHistoryColl.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("DeleteWatchHistory failed", log.Any("filter", filter), log.E(err))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
diff --git a/pub/models/mwacc/acc.go b/pub/models/mwacc/acc.go
new file mode 100644
index 0000000..0832450
--- /dev/null
+++ b/pub/models/mwacc/acc.go
@@ -0,0 +1,170 @@
+package mwacc
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/errors"
+ sErr "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 过滤查询数量
+func AccCountByFilter(ctx context.Context, filters primitive.M, opts ...*options.CountOptions) (c int64, err error) {
+ c, err = mwAccColl.Count(ctx, filters)
+ return
+}
+
+// 过滤查询账号
+func FindOneAccByFilter(ctx context.Context, filters primitive.M, opts ...*options.FindOneOptions) (*MwAcc, error) {
+ var res MwAcc
+ err := mwAccColl.FindOne(ctx, &res, filters, opts...)
+ if err != nil {
+ log.Error("mwacc FindOneAccByFilter err", log.Any("filters", filters), log.E(err))
+ }
+ return &res, err
+}
+
+// 过滤更新
+func UpOneAccByFilter(ctx context.Context, query primitive.M, update primitive.M) (err error) {
+ _, err = mwAccColl.UpdateOne(ctx, query, primitive.M{"$set": update})
+ return
+}
+
+// 过滤删除角色
+func DelAccByFilter(ctx context.Context, filters primitive.M) (err error) {
+ _, err = mwAccColl.DeleteOne(ctx, filters)
+ if err != nil {
+ log.Error("mwacc DelAccByFilter err", log.Any("filters", filters), log.E(err))
+ }
+ return
+}
+
+// // 更新最后登录时间
+// func UpAccLastTimeById(ctx context.Context, id int64) (*mongo.UpdateResult, error) {
+// ret, err := mwAccColl.UpdateOne(ctx, primitive.M{FN.MwAcc.Id: id}, primitive.M{"$set": primitive.M{FN.MwAcc.LastTime: time.Now()}})
+// if err != nil {
+// log.Error("mvacc UpLastTimeById err", log.Any(FN.MwAcc.Id, id), log.E(err))
+// }
+// return ret, err
+// }
+
+// 绑定二维码
+func BindAccQurl(ctx context.Context, acc string) (ok bool, err error) {
+
+ res, dataErr := FindOneAccByFilter(ctx, primitive.M{
+ FN.MwAcc.Acc: acc,
+ })
+
+ if dataErr != nil || res.Id == 0 {
+ err = sErr.MakeWithMsg(sErr.Logo, "用户不存在!")
+ return false, err
+ }
+
+ if res.IsEnb == false {
+ err = sErr.MakeWithMsg(sErr.UserPermission, "用户被禁用!")
+ return false, err
+ }
+ err = UpOneAccByFilter(ctx, primitive.M{
+ FN.MwAcc.Acc: acc,
+ }, primitive.M{
+ FN.MwAcc.IsBase32: true,
+ })
+
+ if err != nil {
+ return false, err
+ }
+ return true, err
+}
+
+// 插入角色
+func AccAdd(ctx context.Context, insertReq *AccInsertDoc) (id int64, ok bool, err error) {
+
+ insertReq.Id = int64(mongos.Lid.MwAcc.Get())
+ insertReq.CreatedAt = time.Now()
+
+ _, err = mwAccColl.InsertOne(ctx, insertReq)
+ if err != nil {
+ log.ZapLog.Error(fmt.Sprintf("InsertRole mongo error %v", err))
+ err = errors.MakeWithErr(errors.DBInsert, err)
+ return 0, false, err
+ }
+ return insertReq.Id, true, nil
+}
+
+// 获取角色列表
+func GetAccList(ctx context.Context, search primitive.M, sort []primitive.M) (
+ total int64, data []*AccGetListRec, err error) {
+
+ var countPipe []primitive.M
+ countPipe = append(countPipe, search, bson.M{"$count": "total"})
+ var aggCount AggStatCount
+ err = mwAccColl.ObjectAggregate(ctx, &aggCount, countPipe)
+ if err != nil {
+ return 0, nil, err
+ }
+ total = aggCount.Total
+
+ var pip []primitive.M
+ pip = append(pip, search)
+ pip = append(pip, sort...)
+ pip = append(pip,
+ primitive.M{
+ "$lookup": primitive.M{
+ "localField": FN.MwAcc.RoleId,
+ "from": TN.MwRole,
+ "foreignField": FN.MwRole.Id,
+ "as": "roles",
+ },
+ },
+ primitive.M{"$unwind": primitive.M{
+ "path": "$roles",
+ "preserveNullAndEmptyArrays": true,
+ },
+ },
+ primitive.M{
+ "$project": primitive.M{
+ FN.MwAcc.Id: 1,
+ FN.MwAcc.UserName: 1,
+ FN.MwAcc.Acc: 1,
+ FN.MwAcc.Pwd: 1,
+ FN.MwAcc.Email: 1,
+ FN.MwAcc.RoleId: 1,
+ FN.MwAcc.RoleName: "$roles.name",
+ FN.MwAcc.Base32: 1,
+ FN.MwAcc.IsBase32: 1,
+ // mwacc.FN.MwAcc.LastTime: 1,
+ FN.MwAcc.IsEnb: 1,
+ FN.MwAcc.Feedback: 1,
+ FN.MwAcc.CreatedAt: 1,
+ FN.MwAcc.UpdatedAt: 1,
+ FN.MwAcc.LimitIP: 1,
+ },
+ })
+
+ err = mwAccColl.Aggregate(ctx, &data, pip)
+ if err != nil {
+ err = errors.MakeWithErr(errors.DBQuery, err)
+ return
+ }
+ return
+}
+
+// 过滤查询账号
+func FindManyAccByFilter(ctx context.Context, filters primitive.M, opts ...*options.FindOptions) ([]*MwAcc, *srvdef.SrvErr) {
+ var res []*MwAcc
+ err := mwAccColl.Find(ctx, &res, filters, opts...)
+ if err != nil {
+ log.Error("mwacc FindOneAccByFilter err", log.Any("filters", filters), log.E(err))
+ return res, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ log.Info("success")
+
+ return res, nil
+}
diff --git a/pub/models/mwacc/dbhelper.go b/pub/models/mwacc/dbhelper.go
new file mode 100644
index 0000000..48d7d4a
--- /dev/null
+++ b/pub/models/mwacc/dbhelper.go
@@ -0,0 +1,13 @@
+package mwacc
+
+import "videoweb/pub/mongos"
+
+var (
+ mwAccColl *mongos.Collection = nil
+ mwRoleColl *mongos.Collection = nil
+ mwAccTokenColl *mongos.Collection = nil
+)
+
+type AggStatCount struct {
+ Total int64 `bson:"total"`
+}
diff --git a/pub/models/mwacc/drop.go b/pub/models/mwacc/drop.go
new file mode 100644
index 0000000..352a218
--- /dev/null
+++ b/pub/models/mwacc/drop.go
@@ -0,0 +1,66 @@
+package mwacc
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ mwAccColl = mongos.Shop.Coll(TN.MwAcc)
+ mwRoleColl = mongos.Shop.Coll(TN.MwRole)
+ mwAccTokenColl = mongos.Shop.Coll(TN.MwAccToken)
+ initIndex(ctx)
+ return
+}
+
+// initIndex 初始化索引
+func initIndex(ctx context.Context) {
+ initAccIndex(ctx)
+ initRoleIndex(ctx)
+ initAccTokenIndex(ctx)
+}
+
+func initAccIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.MwAcc.Id, 1},
+ },
+ },
+ }
+ if _, err := mwAccColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(mwacc:initAccIndex) Create mwacc Index failed! err:%+v:", err))
+ }
+}
+
+func initRoleIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.MwRole.Id, 1},
+ },
+ // Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := mwRoleColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(mwacc:initRoleIndex) Create mwacc Index failed! err:%+v:", err))
+ }
+}
+func initAccTokenIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.MwAccToken.Id, 1},
+ },
+ // Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := mwAccTokenColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(mwacc:initAccTokenIndex) Create mwacc Index failed! err:%+v:", err))
+ }
+}
diff --git a/pub/models/mwacc/fun.go b/pub/models/mwacc/fun.go
new file mode 100644
index 0000000..5b415da
--- /dev/null
+++ b/pub/models/mwacc/fun.go
@@ -0,0 +1,61 @@
+package mwacc
+
+import (
+ "strconv"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func MakeSearch(keyWords string, status int64, page pubdef.Page) (searchs bson.M) {
+ var search []bson.M
+ if len(keyWords) > 0 {
+ userId, _ := strconv.ParseInt(keyWords, 10, 64)
+ search = append(search, bson.M{FN.MwAcc.Id: userId})
+ search = append(search, bson.M{FN.MwAcc.UserName: bson.M{"$regex": keyWords}})
+ search = append(search, bson.M{FN.MwAcc.Acc: bson.M{"$regex": keyWords}})
+ search = append(search, bson.M{FN.MwAcc.Email: bson.M{"$regex": keyWords}})
+ }
+
+ match := bson.M{}
+ if len(search) > 0 {
+ match["$or"] = search
+ }
+
+ if status > 0 {
+ if status == 1 {
+ match[FN.MwAcc.IsEnb] = true
+ } else {
+ match[FN.MwAcc.IsEnb] = false
+ }
+
+ }
+
+ searchs = bson.M{}
+ searchs["$match"] = match
+ return
+}
+
+func MakeSort(pageNum int64, pageSize int64) (sort []bson.M) {
+ sort = append(sort, bson.M{
+ "$sort": bson.M{
+ FN.MwAcc.CreatedAt: -1,
+ },
+ })
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 10
+ }
+ skip := (pageNum - 1) * pageSize
+ sort = append(sort, bson.M{
+ "$skip": skip,
+ })
+
+ sort = append(sort, bson.M{
+ "$limit": pageSize,
+ })
+
+ return
+}
diff --git a/pub/models/mwacc/mwacc_gen.go b/pub/models/mwacc/mwacc_gen.go
new file mode 100644
index 0000000..1c4987b
--- /dev/null
+++ b/pub/models/mwacc/mwacc_gen.go
@@ -0,0 +1,130 @@
+// Code generated by gen_bson_tag. DO NOT EDIT.
+package mwacc
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// CreateOneMwAcc 创建一个MwAcc
+func CreateOneMwAcc(ctx context.Context, data *MwAcc) *srvdef.SrvErr {
+ _, err := mwAccColl.InsertOne(ctx, data)
+ if err != nil {
+ log.Error("mwacc:CreateOneMwAcc db.InsertOne failed",
+ log.Any("doc", data),
+ log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneMwAcc 查找一个MwAcc
+func FindOneMwAcc(ctx context.Context, filter bson.M) (
+ *MwAcc, *srvdef.SrvErr) {
+ var data *MwAcc
+ if err := mwAccColl.FindOneEx(ctx, &data, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("mwacc:FindOneMwAcc mwAccColl.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return data, nil
+}
+
+// DelOneOneMwAcc 删除一个MwAcc
+func DelOneOneMwAcc(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := mwAccColl.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("mwacc:DelOneOneMwAcc mwAccColl.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneMwAcc 更新一个MwAcc
+func UpdateOneMwAcc(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := mwAccColl.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("mwacc:UpdateOneMwAcc mwAccColl.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// CountMwAccs 统计MwAcc数量
+func CountMwAccs(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := mwAccColl.Count(ctx, filter)
+ if err != nil {
+ log.Warn("mwacc:CountMwAccs mwAccColl.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// FindManyMwAccs 查找一批MwAcc
+func FindManyMwAccs(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (MwAccList, *srvdef.SrvErr) {
+ data := make(MwAccList, 0)
+ if err := mwAccColl.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("mwacc:FindManyMwAccs mwAccColl.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManyMwAccsWithBind 用自己定义结构查找一批MwAcc数据
+func FindManyMwAccsWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := mwAccColl.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("mwacc:FindManyMwAccsWithBind mwAccColl.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateMwAccs 聚合查找一批MwAcc
+func AggregateMwAccs(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := mwAccColl.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("mwacc:AggregateMwAccs mwAccColl.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateMwAccs 聚合查找一批MwAcc数据
+func ObjectAggregateMwAccs(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := mwAccColl.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("mwacc:AggregateMwAccs mwAccColl.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateMwAccs 批量写入或者更新MwAcc数据
+func BulkWriteMwAccs(ctx context.Context,
+ docs []mongo.WriteModel, opts ...*options.BulkWriteOptions) *srvdef.SrvErr {
+ _, err := mwAccColl.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/mwacc/role.go b/pub/models/mwacc/role.go
new file mode 100644
index 0000000..92cd8a7
--- /dev/null
+++ b/pub/models/mwacc/role.go
@@ -0,0 +1,158 @@
+package mwacc
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/pageopt"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 角色聚集处理
+func RoleAggregate(ctx context.Context, filters []primitive.M, opts ...*options.AggregateOptions) (roles []*MwRole, err error) {
+ roles = make([]*MwRole, 0)
+ err = mwRoleColl.Aggregate(ctx, &roles, filters)
+ return
+}
+
+// 过滤查询数量
+func RoleCountByFilter(ctx context.Context, filters primitive.M, opts ...*options.CountOptions) (c int64, err error) {
+ c, err = mwRoleColl.Count(ctx, filters)
+ return
+}
+
+// 过滤查询角色
+func FindOneRoleByFilter(ctx context.Context, filters primitive.M, opts ...*options.FindOneOptions) (*MwRole, *srvdef.SrvErr) {
+ var res MwRole
+ err := mwRoleColl.FindOne(ctx, &res, filters, opts...)
+ if err != nil {
+ log.Error("mwacc FindOneRoleByFilter err", log.Any("filters", filters), log.E(err))
+ return &res, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &res, nil
+}
+
+// 过滤更新
+func UpOneRoleByFilter(ctx context.Context, query primitive.M, update primitive.M) *srvdef.SrvErr {
+ _, err := mwRoleColl.UpdateOne(ctx, query, update)
+ if err != nil {
+ log.Error("mwacc UpOneRoleByFilter: db.UpdateOne err", log.Any("update", update), log.E(err))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// 过滤删除角色
+func DelRoleByFilter(ctx context.Context, filters primitive.M) *srvdef.SrvErr {
+ _, err := mwRoleColl.DeleteOne(ctx, filters)
+ if err != nil {
+ log.Error("mwacc DelRoleByFilter err", log.Any("filters", filters), log.E(err))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return nil
+}
+
+// 更新角色权限
+func UpRoleAuth(ctx context.Context, id int64, keys []string, dataRouter []*Router) (ok bool, err error) {
+
+ _, err = mwRoleColl.UpdateOne(ctx, primitive.M{
+ FN.MwRole.Id: id,
+ }, primitive.M{
+ "$set": primitive.M{
+ FN.MwRole.ApiMenus: keys,
+ FN.MwRole.Router: dataRouter,
+ },
+ })
+
+ if err != nil {
+ log.Error("mvacc UpRoleAuth err", log.Any(FN.MwRole.Id, id), log.E(err))
+ return false, err
+ }
+ return true, nil
+}
+
+// 获取角色列表
+func GetRoleList(ctx context.Context, skip, limit int64, matchs ...pageopt.Matcher) (data []*RoleGetListRec, err error) {
+ data = make([]*RoleGetListRec, 0)
+
+ err = mwRoleColl.Aggregate(ctx, &data,
+ []primitive.M{
+ {"$match": pageopt.MergeM(matchs)},
+ {"$sort": primitive.M{FN.MwRole.Id: -1}},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$lookup": primitive.M{
+ "localField": FN.MwRole.Id,
+ "from": TN.MwAcc,
+ "foreignField": FN.MwAcc.RoleId,
+ "as": "onlyRole",
+ }},
+ {"$project": primitive.M{
+ FN.MwRole.Id: 1,
+ FN.MwRole.Name: 1,
+ FN.MwRole.Desc: 1,
+ FN.MwRole.IsEnb: 1,
+ FN.MwRole.ApiMenus: 1,
+ FN.MwRole.CreatedAt: 1,
+ FN.MwRole.UpdatedAt: 1,
+ "account": primitive.M{
+ "$size": "$onlyRole",
+ },
+ }},
+ })
+ if err != nil && err.Error() != errors.NotFind {
+ log.ZapLog.Error(fmt.Sprintf("mwacc GetRoleList error %v", err))
+ err = errors.MakeWithErr(errors.DBQuery, err)
+ return
+ }
+ return
+}
+
+// 下拉列表
+func RoleDrop(ctx context.Context) (data []*RoleDropDoc, err error) {
+
+ query := primitive.M{FN.MwRole.IsEnb: true}
+
+ opt := options.Find().SetProjection(primitive.M{
+ FN.MwRole.Id: 1,
+ FN.MwRole.Name: 1,
+ })
+
+ err = mwRoleColl.Find(ctx, &data, query, opt)
+ if err != nil {
+ err = errors.MakeWithErr(errors.DBQuery, err)
+ return
+ }
+ return
+}
+
+// 插入角色
+func RoleAdd(ctx context.Context, name string, desc string, isEnb bool) (ok bool, err error) {
+ c, err := mwRoleColl.Count(ctx, primitive.M{FN.MwRole.Name: name})
+ if c > 0 {
+ err = errors.MakeWithMsg(errors.DBInsert, "角色名称")
+ return false, err
+ }
+
+ _, err = mwRoleColl.InsertOne(ctx, primitive.M{
+ FN.MwRole.Id: mongos.Lid.MwRole.Get(),
+ FN.MwRole.Name: name,
+ FN.MwRole.Desc: desc,
+ FN.MwRole.IsEnb: isEnb,
+ FN.MwRole.CreatedAt: time.Now(),
+ })
+
+ if err != nil {
+ log.ZapLog.Error(fmt.Sprintf("InsertRole mongo error %v", err))
+ err = errors.MakeWithErr(errors.DBInsert, err)
+ return false, err
+ }
+
+ return true, nil
+}
diff --git a/pub/models/mwacc/tag.go b/pub/models/mwacc/tag.go
new file mode 100755
index 0000000..28b8406
--- /dev/null
+++ b/pub/models/mwacc/tag.go
@@ -0,0 +1,88 @@
+package mwacc
+
+type TagMwRole struct {
+ ApiMenus string
+ CreatedAt string
+ Desc string
+ Id string
+ IsEnb string
+ Name string
+ Router string
+ UpdatedAt string
+}
+type TagMwAcc struct {
+ Acc string
+ Base32 string
+ Code string
+ CreatedAt string
+ Email string
+ Feedback string
+ Id string
+ IsBase32 string
+ IsEnb string
+ LastTime string
+ LimitIP string
+ Pwd string
+ Qurl string
+ RoleId string
+ RoleName string
+ Token string
+ UpdatedAt string
+ UserName string
+}
+type TagMwAccToken struct {
+ AccId string
+ CreatedAt string
+ Id string
+ Token string
+ UpdatedAt string
+}
+
+var FN struct {
+ MwRole TagMwRole
+ MwAcc TagMwAcc
+ MwAccToken TagMwAccToken
+}
+
+var TN struct {
+ MwRole string
+ MwAcc string
+ MwAccToken string
+}
+
+func init() {
+ FN.MwRole.ApiMenus = "apiMenus"
+ FN.MwRole.CreatedAt = "createdAt"
+ FN.MwRole.Desc = "desc"
+ FN.MwRole.Id = "_id"
+ FN.MwRole.IsEnb = "isEnb"
+ FN.MwRole.Name = "name"
+ FN.MwRole.Router = "router"
+ FN.MwRole.UpdatedAt = "updatedAt"
+ FN.MwAcc.Acc = "acc"
+ FN.MwAcc.Base32 = "base32"
+ FN.MwAcc.Code = "code"
+ FN.MwAcc.CreatedAt = "createdAt"
+ FN.MwAcc.Email = "email"
+ FN.MwAcc.Feedback = "feedback"
+ FN.MwAcc.Id = "_id"
+ FN.MwAcc.IsBase32 = "isBase32"
+ FN.MwAcc.IsEnb = "isEnb"
+ FN.MwAcc.LastTime = "lastTime"
+ FN.MwAcc.LimitIP = "limitIP"
+ FN.MwAcc.Pwd = "pwd"
+ FN.MwAcc.Qurl = "qurl"
+ FN.MwAcc.RoleId = "roleId"
+ FN.MwAcc.RoleName = "roleName"
+ FN.MwAcc.Token = "token"
+ FN.MwAcc.UpdatedAt = "updatedAt"
+ FN.MwAcc.UserName = "userName"
+ FN.MwAccToken.AccId = "accId"
+ FN.MwAccToken.CreatedAt = "createdAt"
+ FN.MwAccToken.Id = "_id"
+ FN.MwAccToken.Token = "token"
+ FN.MwAccToken.UpdatedAt = "updatedAt"
+ TN.MwRole = "mw_role"
+ TN.MwAcc = "mw_acc"
+ TN.MwAccToken = "mw_acc_token"
+}
diff --git a/pub/models/mwacc/token.go b/pub/models/mwacc/token.go
new file mode 100644
index 0000000..4bdd821
--- /dev/null
+++ b/pub/models/mwacc/token.go
@@ -0,0 +1,84 @@
+package mwacc
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 过滤更新
+func UpOneTokenByFilter(ctx context.Context, query primitive.M, update primitive.M) (err error) {
+ _, err = mwAccTokenColl.UpdateOne(ctx, query, primitive.M{"$set": update})
+ return
+}
+
+func SaveToken(ctx context.Context, accId int64, token string) (ok bool, err error) {
+ var booToken MwAccToken
+
+ // 查找
+ err = mwAccTokenColl.FindOne(ctx, &booToken, primitive.M{FN.MwAccToken.AccId: accId})
+
+ if booToken.AccId > 0 {
+ // 更新
+ err = UpOneTokenByFilter(ctx, primitive.M{
+ FN.MwAccToken.AccId: accId,
+ }, primitive.M{
+ FN.MwAccToken.Token: token,
+ FN.MwAccToken.UpdatedAt: time.Now(),
+ })
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+ }
+
+ if err != nil || booToken.AccId == 0 {
+ if _, err := mwAccTokenColl.InsertOne(ctx,
+ primitive.M{
+ FN.MwAccToken.Id: mongos.Lid.MwAccToken.Get(),
+ FN.MwAccToken.AccId: accId,
+ FN.MwAccToken.Token: token,
+ FN.MwAccToken.CreatedAt: time.Now(),
+ }); err != nil {
+ return false, err
+ }
+ }
+
+ return true, nil
+}
+
+func CheckToken(ctx context.Context, token string) (ok bool, err error) {
+ var booToken MwAccToken
+ Terr := mwAccTokenColl.FindOne(ctx, &booToken, primitive.M{FN.MwAccToken.Token: token})
+ if Terr != nil || booToken.AccId == 0 {
+ err = errors.MakeWithMsg(errors.NoToken, "token 不存在或已过期")
+ return false, err
+ }
+ return true, nil
+}
+
+// // 检测Token
+// func CheckToken(ctx context.Context, token string) (ok bool, err error) {
+// var booToken WebToken
+// Terr := tokenColl.FindOne(ctx, &booToken, primitive.M{"token": token})
+// if Terr != nil || booToken.Id == 0 {
+// err = errors.MakeWithMsg(errors.NoToken, "token 不存在或已过期")
+// return
+// }
+// return true, nil
+// }
+
+// 删除token
+func DeleteOneToken(ctx context.Context, filter bson.M) *srvdef.SrvErr {
+ if _, err := mwAccTokenColl.DeleteOne(ctx, filter); err != nil {
+ log.Warn("mwacc:DeleteOneToken db.DeleteOne failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/mwacc/type.go b/pub/models/mwacc/type.go
new file mode 100644
index 0000000..1f5e471
--- /dev/null
+++ b/pub/models/mwacc/type.go
@@ -0,0 +1,78 @@
+package mwacc
+
+import (
+ "time"
+)
+
+type Meta struct {
+ Title string `bson:"title"`
+ Icon string `bson:"icon"`
+ NoCache bool `bson:"noCache"`
+}
+
+type Apiurl struct {
+ Url []string `bson:"url"`
+ Desc string `bson:"desc"`
+ Status bool `bson:"status"`
+ Method []string `bson:"method"`
+}
+
+// 编辑返回去的数据
+type Router struct {
+ Path string `bson:"path"`
+ Component string `bson:"component"`
+ Redirect string `bson:"redirect"`
+ Name string `bson:"name"`
+ IsShow bool `bson:"isShow"`
+ Hidden bool `bson:"hidden"`
+ Meta Meta `bson:"meta"`
+ Children []Router `bson:"children"`
+ ApiUrl []Apiurl `bson:"apiUrl"`
+}
+
+// 角色
+// @table:mw_role
+type MwRole struct {
+ Id int64 `bson:"_id"`
+ Name string `bson:"name"` // 角色名称
+ Desc string `bson:"desc"` // 描述
+ IsEnb bool `bson:"isEnb"` // 是否启用 false:禁用 true:启用
+ ApiMenus []string `bson:"apiMenus"` // 已有权限的api
+ Router []Router `bson:"router"`
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+}
+
+// 账号
+// @table:mw_acc
+type MwAcc struct {
+ Id int64 `bson:"_id"` // id
+ UserName string `bson:"userName"` // 用户名
+ Acc string `bson:"acc"` // 账号
+ Pwd string `bson:"pwd"` // 密码
+ Email string `bson:"email"` // 邮箱
+ RoleId int64 `bson:"roleId"` // 角色Id
+ RoleName string `bson:"roleName"` // 角色名
+ Base32 string `bson:"base32"` // 二步签名
+ IsBase32 bool `bson:"isBase32"` // 是否已签名
+ Code string `bson:"code"` // 验证码
+ Qurl []byte `bson:"qurl"` // 二维码链接
+ Token string `bson:"token"` // token
+ LastTime time.Time `bson:"lastTime"` // 登录时间
+ IsEnb bool `bson:"isEnb"` // 使能
+ Feedback string `bson:"feedback"` // 禁用原因
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+ LimitIP []string `bson:"limitIP"` // 限制IP
+}
+type MwAccList []*MwAcc
+
+// 账号Token
+// @table:mw_acc_token
+type MwAccToken struct {
+ Id int64 `bson:"_id"` // id
+ AccId int64 `bson:"accId"` // 账号id
+ Token string `bson:"token"` // token
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+}
diff --git a/pub/models/mwacc/web.go b/pub/models/mwacc/web.go
new file mode 100644
index 0000000..ec5f3fb
--- /dev/null
+++ b/pub/models/mwacc/web.go
@@ -0,0 +1,53 @@
+package mwacc
+
+import (
+ "time"
+)
+
+type AccGetListRec struct {
+ Id int64 `bson:"_id"` // id
+ Username string `bson:"username"` // 用户名
+ Acc string `bson:"acc"` // 用户名
+ Pwd string `bson:"pwd"` // 密码
+ Email string `bson:"email"` // 邮箱
+ RoleId int64 `bson:"roleId"` // 角色Id
+ RoleName string `bson:"roleName"` // 角色名
+ // Base32 string `bson:"base32"` // 二步签名
+ IsBase32 bool `bson:"isBase32"` // 是否已签名
+ IsEnb bool `bson:"isEnb"` // 使能
+ Feedback string `bson:"feedback"` // 禁用原因
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+ LimitIP []string `bson:"limitIP"` // 允许访问的IP
+}
+
+type RoleGetListRec struct {
+ Id int64 `bson:"_id"` // 角色Id
+ Name string `bson:"name"` // 角色名称
+ Desc string `bson:"desc"` // 描述
+ IsEnb bool `bson:"isEnb"` // 是否启用 false:禁用 true:启用
+ ApiMenus []string `bson:"apiMenus"` // 已有权限的api
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+ AccCount int64 `bson:"accCount"` // 账户数量
+}
+
+type AccInsertDoc struct {
+ Id int64 `bson:"_id"` // id
+ UserName string `bson:"userName"` // 用户名
+ Pwd string `bson:"pwd"` // 密码
+ Acc string `bson:"acc"` // 用户名
+ Email string `bson:"email"` // 邮箱
+ RoleId int64 `bson:"roleId"` // 角色Id
+ Base32 string `bson:"base32"` // 二步签名
+ IsBase32 bool `bson:"isBase32"` // 是否已签名
+ IsEnb bool `bson:"isEnb"` // 使能
+ LastTime time.Time `bson:"lastTime"` // 登录时间
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ LimitIP []string `bson:"limitIP"` // 允许访问的IP
+}
+
+type RoleDropDoc struct {
+ Id int64 `bson:"_id"` // 角色Id
+ Name string `bson:"name"` // 角色名称
+}
diff --git a/pub/models/oplog/dbhelper.go b/pub/models/oplog/dbhelper.go
new file mode 100644
index 0000000..d19a38e
--- /dev/null
+++ b/pub/models/oplog/dbhelper.go
@@ -0,0 +1,9 @@
+package oplog
+
+import (
+ "videoweb/pub/mongos"
+)
+
+var (
+ opLogColl *mongos.Collection
+)
diff --git a/pub/models/oplog/oplog.go b/pub/models/oplog/oplog.go
new file mode 100644
index 0000000..ecb33ec
--- /dev/null
+++ b/pub/models/oplog/oplog.go
@@ -0,0 +1,175 @@
+package oplog
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/mongos"
+
+ "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"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ opLogColl = mongos.ShopLog.Coll(TN.OpLog)
+ initIndex(ctx)
+ return
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.OpLog.ActId, 1},
+ {FN.OpLog.Action, 1},
+ {FN.OpLog.Path, 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {FN.OpLog.CreatedAt, -1},
+ },
+ },
+ }
+ if _, err := opLogColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(oplog:initIndex) Create adLog Index failed! err:%+v:", err))
+ }
+}
+
+// 添加日志
+func AddLog(ctx context.Context, path string, desc string, actId int64, rawData, ip string) (err error) {
+ _, err = opLogColl.InsertOne(ctx, primitive.M{
+ FN.OpLog.Id: mongos.Lid.MwOpLog.Get(),
+ FN.OpLog.Action: desc,
+ FN.OpLog.Path: path,
+ FN.OpLog.ActId: actId,
+ FN.OpLog.RawData: rawData,
+ FN.OpLog.CreatedAt: utils.Now(),
+ FN.OpLog.IP: ip,
+ })
+ return
+}
+
+type LogReq 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"` // 开始时间
+}
+
+// 操作日志信息
+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 OpPageRes struct {
+ Total int64 `json:"total" bson:"total"`
+ List []*OpLogInfo `json:"list" bson:"list"`
+}
+
+// 获取日志列表
+func PageLogList(ctx context.Context, req *LogReq) (res *OpPageRes, err error) {
+ filter := bson.M{}
+ if req.Path != nil && *req.Path != "" {
+ filter[FN.OpLog.Path] = bson.M{"$regex": req.Path}
+ }
+ if req.Action != nil && *req.Action != "" {
+ filter[FN.OpLog.Action] = req.Action
+ }
+ if req.ActId != nil && *req.ActId > 0 {
+ filter[FN.OpLog.ActId] = req.ActId
+ }
+ if req.StartTime != nil && req.EndTime != nil {
+ filter[FN.OpLog.CreatedAt] = bson.M{"$gte": req.StartTime, "$lt": req.EndTime}
+ }
+ res = new(OpPageRes)
+ res.Total, err = mongos.ShopLog.Coll(TN.OpLog).Count(ctx, filter)
+ if err != nil {
+ return
+ }
+
+ sort := bson.M{FN.OpLog.CreatedAt: -1}
+ opt := &options.FindOptions{}
+ opt.SetSort(sort)
+ skip := (req.PageNum - 1) * req.PageSize
+ opt.SetSkip(skip)
+ opt.SetLimit(req.PageSize)
+ list := make(OpLogList, 0)
+ err = mongos.ShopLog.Coll(TN.OpLog).Find(ctx, &list, filter, opt)
+ if err != nil {
+ return
+ }
+ actIds := make([]int64, 0)
+ for _, v := range list {
+ actIds = append(actIds, v.ActId)
+ }
+ // err = opLogColl.Aggregate(ctx, &res.List, []bson.M{
+ // {"$match": conf},
+ // {"$sort": bson.M{FN.OpLog.CreatedAt: -1}},
+ // {"$skip": (req.PageNum - 1) * req.PageSize},
+ // {"$limit": req.PageSize},
+ // {"$lookup": bson.M{
+ // "localField": FN.OpLog.ActId,
+ // "from": mwacc.TN.MwAcc,
+ // "foreignField": mwacc.FN.MwAcc.Id,
+ // "as": "admins",
+ // }},
+ // {"$unwind": "$admins"},
+ // {"$project": bson.M{
+ // FN.OpLog.Path: 1,
+ // FN.OpLog.Action: 1,
+ // FN.OpLog.ActId: 1,
+ // FN.OpLog.RawData: 1,
+ // FN.OpLog.CreatedAt: 1,
+ // "actName": "$admins.userName",
+ // }},
+ // })
+ return
+}
+
+// CountOpLogs 计算某个查询的数量
+func CountOpLogs(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ total, err := mongos.ShopLog.Coll(TN.OpLog).Count(ctx, filter)
+ if err != nil {
+ log.Info("oplog:CountOpLogs db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return total, nil
+}
+
+// InsertOneUser 插入一个用户
+func InsertOneUser(ctx context.Context, opLog *OpLog) *srvdef.SrvErr {
+ if _, err := mongos.ShopLog.Coll(TN.OpLog).InsertOne(ctx, opLog); err != nil {
+ log.Warn("oplog:InsertOneUser db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindManyOpLogs 查找一批数据
+func FindManyOpLogs(ctx context.Context,
+ filter bson.M, opts ...*options.FindOptions) (
+ OpLogList, *srvdef.SrvErr) {
+ data := make(OpLogList, 0)
+ if err := mongos.ShopLog.Coll(TN.OpLog).Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("oplog:FindManyOpLogs db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
diff --git a/pub/models/oplog/tag.go b/pub/models/oplog/tag.go
new file mode 100755
index 0000000..e2bc190
--- /dev/null
+++ b/pub/models/oplog/tag.go
@@ -0,0 +1,34 @@
+package oplog
+
+type TagOpLog struct {
+ ActId string
+ Action string
+ CreatedAt string
+ IP string
+ Id string
+ IsWhite string
+ OpType string
+ Path string
+ RawData string
+}
+
+var FN struct {
+ OpLog TagOpLog
+}
+
+var TN struct {
+ OpLog string
+}
+
+func init() {
+ FN.OpLog.ActId = "actId"
+ FN.OpLog.Action = "action"
+ FN.OpLog.CreatedAt = "createdAt"
+ FN.OpLog.IP = "ip"
+ FN.OpLog.Id = "_id"
+ FN.OpLog.IsWhite = "isWhite"
+ FN.OpLog.OpType = "opType"
+ FN.OpLog.Path = "path"
+ FN.OpLog.RawData = "rawData"
+ TN.OpLog = "mw_op_log"
+}
diff --git a/pub/models/oplog/type.go b/pub/models/oplog/type.go
new file mode 100644
index 0000000..c3348ef
--- /dev/null
+++ b/pub/models/oplog/type.go
@@ -0,0 +1,22 @@
+package oplog
+
+import (
+ "time"
+ "videoweb/pub/common"
+)
+
+// 操作日志
+// @table:mw_op_log
+type OpLog struct {
+ Id int64 `bson:"_id"` // id
+ Path string `bson:"path"` // 请求接口
+ OpType common.ApiOpType `bson:"opType"` // 操作类型
+ IsWhite bool `bson:"isWhite"` // 是否白名单
+ ActId int64 `bson:"actId"` // 操作人Id
+ Action string `bson:"action"` // 操作目的
+ RawData string `bson:"rawData"` // 原始数据
+ CreatedAt time.Time `bson:"createdAt"` // 操作时间
+ IP string `bson:"ip"` // 操作人IP
+}
+
+type OpLogList []*OpLog
diff --git a/pub/models/playletbackflowstatmod/stat.go b/pub/models/playletbackflowstatmod/stat.go
new file mode 100644
index 0000000..58e4916
--- /dev/null
+++ b/pub/models/playletbackflowstatmod/stat.go
@@ -0,0 +1,172 @@
+package playletbackflowstatmod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "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"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化原创短剧 AI 内容回流日统计表和查询索引。
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletBackflowDailyStat)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletBackflowDailyStat.DataDate, Value: 1},
+ {Key: FN.PlayletBackflowDailyStat.ReportStatus, Value: 1},
+ {Key: FN.PlayletBackflowDailyStat.Id, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletBackflowDailyStat.DataDate, Value: 1},
+ {Key: FN.PlayletBackflowDailyStat.PlayletId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true).SetName("uniq_dataDate_playletId"),
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletbackflowstatmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// UpsertManyPlayletBackflowDailyStats 批量保存每天原创短剧回流数据快照。
+func UpsertManyPlayletBackflowDailyStats(ctx context.Context, docs PlayletBackflowDailyStatList) *srvdef.SrvErr {
+ if len(docs) == 0 {
+ return nil
+ }
+ now := time.Now()
+ writes := make([]mongo.WriteModel, 0, len(docs))
+ for _, doc := range docs {
+ if doc == nil || doc.DataDate == "" || doc.PlayletId <= 0 {
+ continue
+ }
+ doc.UpdatedAt = now
+ doc.LastStatAt = now
+ update := bson.M{
+ "$set": bson.M{
+ FN.PlayletBackflowDailyStat.DataDate: doc.DataDate,
+ FN.PlayletBackflowDailyStat.PlayletId: doc.PlayletId,
+ FN.PlayletBackflowDailyStat.ChapterId: doc.ChapterId,
+ FN.PlayletBackflowDailyStat.CurrentEpisode: doc.CurrentEpisode,
+ FN.PlayletBackflowDailyStat.MediaCenterId: doc.MediaCenterId,
+ FN.PlayletBackflowDailyStat.MediaContentId: doc.MediaContentId,
+ FN.PlayletBackflowDailyStat.Title: doc.Title,
+ FN.PlayletBackflowDailyStat.PublishedData: doc.PublishedData,
+ FN.PlayletBackflowDailyStat.ScriptName: doc.ScriptName,
+ FN.PlayletBackflowDailyStat.DramaName: doc.DramaName,
+ FN.PlayletBackflowDailyStat.DramaNo: doc.DramaNo,
+ FN.PlayletBackflowDailyStat.VideoURL: doc.VideoURL,
+ FN.PlayletBackflowDailyStat.PlatformURL: doc.PlatformURL,
+ FN.PlayletBackflowDailyStat.Views: doc.Views,
+ FN.PlayletBackflowDailyStat.Clicks: doc.Clicks,
+ FN.PlayletBackflowDailyStat.Likes: doc.Likes,
+ FN.PlayletBackflowDailyStat.LikeRate: doc.LikeRate,
+ FN.PlayletBackflowDailyStat.CompletionRate: doc.CompletionRate,
+ FN.PlayletBackflowDailyStat.PaidRate: doc.PaidRate,
+ FN.PlayletBackflowDailyStat.Comments: doc.Comments,
+ FN.PlayletBackflowDailyStat.Shares: doc.Shares,
+ FN.PlayletBackflowDailyStat.CompletionRateScope: doc.CompletionRateScope,
+ FN.PlayletBackflowDailyStat.ReportStatus: doc.ReportStatus,
+ FN.PlayletBackflowDailyStat.ReportError: "",
+ FN.PlayletBackflowDailyStat.LastStatAt: doc.LastStatAt,
+ FN.PlayletBackflowDailyStat.UpdatedAt: doc.UpdatedAt,
+ },
+ "$setOnInsert": bson.M{
+ FN.PlayletBackflowDailyStat.Id: primitive.NewObjectID(),
+ FN.PlayletBackflowDailyStat.ReportRetryCount: int64(0),
+ FN.PlayletBackflowDailyStat.CreatedAt: now,
+ },
+ }
+ model := mongo.NewUpdateOneModel().
+ SetFilter(bson.M{
+ FN.PlayletBackflowDailyStat.DataDate: doc.DataDate,
+ FN.PlayletBackflowDailyStat.PlayletId: doc.PlayletId,
+ }).
+ SetUpdate(update).
+ SetUpsert(true)
+ writes = append(writes, model)
+ }
+ if len(writes) == 0 {
+ return nil
+ }
+ if _, err := coll.BulkWrite(ctx, writes, options.BulkWrite().SetOrdered(false)); err != nil {
+ log.Warn("playletbackflowstatmod:UpsertManyPlayletBackflowDailyStats failed", log.E(err), log.Any("count", len(writes)))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManyPlayletBackflowDailyStats 查询原创短剧回流日统计。
+func FindManyPlayletBackflowDailyStats(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletBackflowDailyStatList, *srvdef.SrvErr) {
+ data := make(PlayletBackflowDailyStatList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("playletbackflowstatmod:FindManyPlayletBackflowDailyStats failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletBackflowDailyStats 统计原创短剧回流日统计条数。
+func CountPlayletBackflowDailyStats(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("playletbackflowstatmod:CountPlayletBackflowDailyStats failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// MarkPlayletBackflowDailyStatsReported 把一批统计记录标记为上报成功。
+func MarkPlayletBackflowDailyStatsReported(ctx context.Context, ids []primitive.ObjectID, reportAt time.Time) *srvdef.SrvErr {
+ if len(ids) == 0 {
+ return nil
+ }
+ _, err := coll.UpdateMany(ctx, bson.M{FN.PlayletBackflowDailyStat.Id: bson.M{"$in": ids}}, bson.M{"$set": bson.M{
+ FN.PlayletBackflowDailyStat.ReportStatus: ReportStatusSuccess,
+ FN.PlayletBackflowDailyStat.ReportAt: reportAt,
+ FN.PlayletBackflowDailyStat.ReportError: "",
+ FN.PlayletBackflowDailyStat.UpdatedAt: time.Now(),
+ }})
+ if err != nil {
+ log.Warn("playletbackflowstatmod:MarkPlayletBackflowDailyStatsReported failed", log.E(err), log.Any("count", len(ids)))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// MarkPlayletBackflowDailyStatsFailed 把一批统计记录标记为上报失败。
+func MarkPlayletBackflowDailyStatsFailed(ctx context.Context, ids []primitive.ObjectID, reportErr string) *srvdef.SrvErr {
+ if len(ids) == 0 {
+ return nil
+ }
+ _, err := coll.UpdateMany(ctx, bson.M{FN.PlayletBackflowDailyStat.Id: bson.M{"$in": ids}}, bson.M{
+ "$set": bson.M{
+ FN.PlayletBackflowDailyStat.ReportStatus: ReportStatusFailed,
+ FN.PlayletBackflowDailyStat.ReportError: reportErr,
+ FN.PlayletBackflowDailyStat.UpdatedAt: time.Now(),
+ },
+ "$inc": bson.M{
+ FN.PlayletBackflowDailyStat.ReportRetryCount: int64(1),
+ },
+ })
+ if err != nil {
+ log.Warn("playletbackflowstatmod:MarkPlayletBackflowDailyStatsFailed failed", log.E(err), log.Any("count", len(ids)))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletbackflowstatmod/tag.go b/pub/models/playletbackflowstatmod/tag.go
new file mode 100644
index 0000000..9614b34
--- /dev/null
+++ b/pub/models/playletbackflowstatmod/tag.go
@@ -0,0 +1,78 @@
+package playletbackflowstatmod
+
+// TagPlayletBackflowDailyStat 保存 playlet_backflow_daily_stat 集合字段名。
+type TagPlayletBackflowDailyStat struct {
+ Id string
+ DataDate string
+ PlayletId string
+ ChapterId string
+ CurrentEpisode string
+ MediaCenterId string
+ MediaContentId string
+ Title string
+ PublishedData string
+ ScriptName string
+ DramaName string
+ DramaNo string
+ VideoURL string
+ PlatformURL string
+ Views string
+ Clicks string
+ Likes string
+ LikeRate string
+ CompletionRate string
+ PaidRate string
+ Comments string
+ Shares string
+ CompletionRateScope string
+ ReportStatus string
+ ReportRetryCount string
+ ReportAt string
+ ReportError string
+ LastStatAt string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletBackflowDailyStat TagPlayletBackflowDailyStat
+}
+
+var TN struct {
+ PlayletBackflowDailyStat string
+}
+
+func init() {
+ FN.PlayletBackflowDailyStat.Id = "_id"
+ FN.PlayletBackflowDailyStat.DataDate = "dataDate"
+ FN.PlayletBackflowDailyStat.PlayletId = "playletId"
+ FN.PlayletBackflowDailyStat.ChapterId = "chapterId"
+ FN.PlayletBackflowDailyStat.CurrentEpisode = "currentEpisode"
+ FN.PlayletBackflowDailyStat.MediaCenterId = "mediaCenterId"
+ FN.PlayletBackflowDailyStat.MediaContentId = "mediaContentId"
+ FN.PlayletBackflowDailyStat.Title = "title"
+ FN.PlayletBackflowDailyStat.PublishedData = "publishedData"
+ FN.PlayletBackflowDailyStat.ScriptName = "scriptName"
+ FN.PlayletBackflowDailyStat.DramaName = "dramaName"
+ FN.PlayletBackflowDailyStat.DramaNo = "dramaNo"
+ FN.PlayletBackflowDailyStat.VideoURL = "videoUrl"
+ FN.PlayletBackflowDailyStat.PlatformURL = "platformUrl"
+ FN.PlayletBackflowDailyStat.Views = "views"
+ FN.PlayletBackflowDailyStat.Clicks = "clicks"
+ FN.PlayletBackflowDailyStat.Likes = "likes"
+ FN.PlayletBackflowDailyStat.LikeRate = "likeRate"
+ FN.PlayletBackflowDailyStat.CompletionRate = "completionRate"
+ FN.PlayletBackflowDailyStat.PaidRate = "paidRate"
+ FN.PlayletBackflowDailyStat.Comments = "comments"
+ FN.PlayletBackflowDailyStat.Shares = "shares"
+ FN.PlayletBackflowDailyStat.CompletionRateScope = "completionRateScope"
+ FN.PlayletBackflowDailyStat.ReportStatus = "reportStatus"
+ FN.PlayletBackflowDailyStat.ReportRetryCount = "reportRetryCount"
+ FN.PlayletBackflowDailyStat.ReportAt = "reportAt"
+ FN.PlayletBackflowDailyStat.ReportError = "reportError"
+ FN.PlayletBackflowDailyStat.LastStatAt = "lastStatAt"
+ FN.PlayletBackflowDailyStat.CreatedAt = "createdAt"
+ FN.PlayletBackflowDailyStat.UpdatedAt = "updatedAt"
+
+ TN.PlayletBackflowDailyStat = "playlet_backflow_daily_stat"
+}
diff --git a/pub/models/playletbackflowstatmod/type.go b/pub/models/playletbackflowstatmod/type.go
new file mode 100644
index 0000000..19039e2
--- /dev/null
+++ b/pub/models/playletbackflowstatmod/type.go
@@ -0,0 +1,55 @@
+package playletbackflowstatmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// ReportStatus 是 AI 内容中台数据回流上报状态。
+type ReportStatus int64
+
+const (
+ ReportStatusPending ReportStatus = 0 // 待上报
+ ReportStatusSuccess ReportStatus = 1 // 上报成功
+ ReportStatusFailed ReportStatus = 2 // 上报失败
+)
+
+// PlayletBackflowDailyStat @table: playlet_backflow_daily_stat
+//
+// PlayletBackflowDailyStat 保存原创短剧每天回流给 AI 内容中台的数据快照。
+type PlayletBackflowDailyStat struct {
+ Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` // MongoDB ObjectID
+ DataDate string `json:"dataDate" bson:"dataDate"` // 指标日期,格式 DD/MM/YYYY
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 业务短剧 ID
+ ChapterId int64 `json:"chapterId" bson:"chapterId"` // 上报使用的章节 ID
+ CurrentEpisode int64 `json:"currentEpisode" bson:"currentEpisode"` // 上报使用的章节序号
+ MediaCenterId int64 `json:"mediaCenterId" bson:"mediaCenterId"` // 媒资库资源 ID
+ MediaContentId int64 `json:"mediaContentId" bson:"mediaContentId"` // 上报剧集的媒资章节 ID
+ Title string `json:"title" bson:"title"` // 本地短剧标题
+ PublishedData string `json:"publishedData" bson:"publishedData"` // 中台字段:视频上线日期
+ ScriptName string `json:"scriptName" bson:"scriptName"` // 中台字段:剧本名称
+ DramaName string `json:"dramaName" bson:"dramaName"` // 中台字段:短剧名称
+ DramaNo string `json:"dramaNo" bson:"dramaNo"` // 中台字段:集数
+ VideoURL string `json:"videoUrl" bson:"videoUrl"` // 中台字段:成片下载/存储链接
+ PlatformURL string `json:"platformUrl" bson:"platformUrl"` // 中台字段:产品内播放页链接
+ Views int64 `json:"views" bson:"views"` // 浏览量
+ Clicks int64 `json:"clicks" bson:"clicks"` // 播放量
+ Likes int64 `json:"likes" bson:"likes"` // 点赞量
+ LikeRate string `json:"likeRate" bson:"likeRate"` // 点赞率
+ CompletionRate string `json:"completionRate" bson:"completionRate"` // 完播率
+ PaidRate string `json:"paidRate" bson:"paidRate"` // 付费率
+ Comments int64 `json:"comments" bson:"comments"` // 评论量
+ Shares int64 `json:"shares" bson:"shares"` // 分享量
+ CompletionRateScope string `json:"completionRateScope" bson:"completionRateScope"` // 完播率口径
+ ReportStatus ReportStatus `json:"reportStatus" bson:"reportStatus"` // 上报状态
+ ReportRetryCount int64 `json:"reportRetryCount" bson:"reportRetryCount"` // 上报失败次数
+ ReportAt time.Time `json:"reportAt" bson:"reportAt"` // 最近一次上报成功时间
+ ReportError string `json:"reportError" bson:"reportError"` // 最近一次上报失败原因
+ LastStatAt time.Time `json:"lastStatAt" bson:"lastStatAt"` // 最近一次计算时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+
+// PlayletBackflowDailyStatList 是原创短剧日回流统计列表。
+type PlayletBackflowDailyStatList []*PlayletBackflowDailyStat
diff --git a/pub/models/playletcategorymod/category.go b/pub/models/playletcategorymod/category.go
new file mode 100644
index 0000000..550ed96
--- /dev/null
+++ b/pub/models/playletcategorymod/category.go
@@ -0,0 +1,121 @@
+package playletcategorymod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_category 集合和后台常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletCategory)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletCategory.Status, Value: 1},
+ {Key: FN.PlayletCategory.Rank, Value: -1},
+ {Key: FN.PlayletCategory.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletCategory.Name, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletCategory.CreatedAt, Value: -1},
+ {Key: FN.PlayletCategory.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletcategorymod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletCategory 插入一条短剧分类数据
+func InsertOnePlayletCategory(ctx context.Context, doc *PlayletCategory) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletCategory.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("playletcategorymod:InsertOnePlayletCategory failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOnePlayletCategory 删除一条短剧分类数据
+func DelOnePlayletCategory(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playletcategorymod:DelOnePlayletCategory failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOnePlayletCategory 查询一条短剧分类数据
+func FindOnePlayletCategory(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*PlayletCategory, *srvdef.SrvErr) {
+ var doc PlayletCategory
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("playletcategorymod:FindOnePlayletCategory failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyPlayletCategory 按条件查询多条短剧分类数据
+func FindManyPlayletCategory(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletCategoryList, *srvdef.SrvErr) {
+ data := make(PlayletCategoryList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("playletcategorymod:FindManyPlayletCategory failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletCategory 统计满足条件的短剧分类数量
+func CountPlayletCategory(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("playletcategorymod:CountPlayletCategory failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOnePlayletCategory 更新一条短剧分类数据
+func UpdateOnePlayletCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("playletcategorymod:UpdateOnePlayletCategory failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletcategorymod/tag.go b/pub/models/playletcategorymod/tag.go
new file mode 100644
index 0000000..964a7fd
--- /dev/null
+++ b/pub/models/playletcategorymod/tag.go
@@ -0,0 +1,44 @@
+package playletcategorymod
+
+// TagPlayletCategory 保存 playlet_category 集合字段名
+type TagPlayletCategory struct {
+ Id string
+ Name string
+ Slug string
+ Icon string
+ Status string
+ Rank string
+ Remark string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletCategory TagPlayletCategory
+}
+
+var TN struct {
+ PlayletCategory string
+}
+
+func init() {
+ FN.PlayletCategory.Id = "_id"
+ FN.PlayletCategory.Name = "name"
+ FN.PlayletCategory.Slug = "slug"
+ FN.PlayletCategory.Icon = "icon"
+ FN.PlayletCategory.Status = "status"
+ FN.PlayletCategory.Rank = "rank"
+ FN.PlayletCategory.Remark = "remark"
+ FN.PlayletCategory.SeoInfo.Description = "seoInfo.webDescription"
+ FN.PlayletCategory.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.PlayletCategory.SeoInfo.Title = "seoInfo.webTitle"
+ FN.PlayletCategory.CreatedAt = "createdAt"
+ FN.PlayletCategory.UpdatedAt = "updatedAt"
+
+ TN.PlayletCategory = "playlet_category"
+}
diff --git a/pub/models/playletcategorymod/type.go b/pub/models/playletcategorymod/type.go
new file mode 100644
index 0000000..0188755
--- /dev/null
+++ b/pub/models/playletcategorymod/type.go
@@ -0,0 +1,25 @@
+package playletcategorymod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletCategory @table: playlet_category
+//
+// PlayletCategory 短剧分类集合模型
+type PlayletCategory struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Name string `json:"name" bson:"name"` // 分类名称
+ Slug string `json:"slug" bson:"slug"` // slug
+ Icon string `json:"icon" bson:"icon"` // 分类图标
+ Status bool `json:"status" bson:"status"` // 启用状态
+ Rank int64 `json:"rank" bson:"rank"` // 排序,越大越靠前
+ Remark string `json:"remark" bson:"remark"` // 备注
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletCategoryList 短剧分类列表
+type PlayletCategoryList []*PlayletCategory
diff --git a/pub/models/playletchaptermod/chapter.go b/pub/models/playletchaptermod/chapter.go
new file mode 100644
index 0000000..3547da2
--- /dev/null
+++ b/pub/models/playletchaptermod/chapter.go
@@ -0,0 +1,265 @@
+package playletchaptermod
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_chapter 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletChapter)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: FN.PlayletChapter.CurrentEpisode, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: FN.PlayletChapter.MediaContentId, Value: 1},
+ },
+ Options: options.Index().
+ SetUnique(true).
+ SetPartialFilterExpression(bson.M{
+ FN.PlayletChapter.MediaContentId: bson.M{"$gt": 0},
+ }),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: FN.PlayletChapter.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.CreatedAt, Value: -1},
+ {Key: FN.PlayletChapter.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.WatermarkStatus, Value: 1},
+ {Key: FN.PlayletChapter.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.MediaSourceId, Value: 1},
+ },
+ Options: options.Index().
+ SetPartialFilterExpression(bson.M{
+ FN.PlayletChapter.MediaSourceId: bson.M{"$gt": ""},
+ }),
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletchaptermod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletChapter 插入一条章节数据
+func InsertOnePlayletChapter(ctx context.Context, doc *PlayletChapter) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletChapter.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("playletchaptermod:InsertOnePlayletChapter failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// InsertManyPlayletChapter 批量插入章节数据
+func InsertManyPlayletChapter(ctx context.Context, docs PlayletChapterList) *srvdef.SrvErr {
+ data := make(PlayletChapterList, 0, len(docs))
+ now := time.Now()
+ for _, doc := range docs {
+ if doc == nil {
+ continue
+ }
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletChapter.Get()
+ }
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ data = append(data, doc)
+ }
+ if len(data) == 0 {
+ return nil
+ }
+ if _, err := coll.InsertMany(ctx, &data); err != nil {
+ log.Warn("playletchaptermod:InsertManyPlayletChapter failed", log.Any("count", len(data)), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOnePlayletChapter 删除一条章节数据
+func DelOnePlayletChapter(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playletchaptermod:DelOnePlayletChapter failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyPlayletChapter 按条件批量删除章节数据
+func DelManyPlayletChapter(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("playletchaptermod:DelManyPlayletChapter failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOnePlayletChapter 查询一条章节数据
+func FindOnePlayletChapter(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*PlayletChapter, *srvdef.SrvErr) {
+ var doc PlayletChapter
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("playletchaptermod:FindOnePlayletChapter failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyPlayletChapter 按条件查询多条章节数据
+func FindManyPlayletChapter(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletChapterList, *srvdef.SrvErr) {
+ data := make(PlayletChapterList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("playletchaptermod:FindManyPlayletChapter failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletChapter 统计满足条件的章节数量
+func CountPlayletChapter(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("playletchaptermod:CountPlayletChapter failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOnePlayletChapter 更新一条章节数据
+func UpdateOnePlayletChapter(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("playletchaptermod:UpdateOnePlayletChapter failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// IsLinkedMedia 是否短剧媒资:显式标记或已挂到剧集
+func IsLinkedMedia(ctx context.Context, isPlaylet bool, ids ...string) bool {
+ if isPlaylet {
+ return true
+ }
+ return ExistsByMediaIDs(ctx, ids...)
+}
+
+// ExistsByMediaIDs 判断媒资 ID 是否属于短剧剧集(mediaSourceId / mediaTaskId)
+func ExistsByMediaIDs(ctx context.Context, ids ...string) bool {
+ clean := uniqueNonEmptyStrings(ids)
+ if len(clean) == 0 {
+ return false
+ }
+ item, _ := FindOnePlayletChapter(ctx, bson.M{
+ "$or": []bson.M{
+ {FN.PlayletChapter.MediaSourceId: bson.M{"$in": clean}},
+ {FN.PlayletChapter.MediaTaskId: bson.M{"$in": clean}},
+ },
+ }, options.FindOne().SetProjection(bson.M{FN.PlayletChapter.Id: 1}))
+ return item != nil
+}
+
+// ListLinkedMediaIDs 返回短剧剧集关联的媒资 ID,用于排除短剧视频
+func ListLinkedMediaIDs(ctx context.Context) []string {
+ list, err := FindManyPlayletChapter(ctx, bson.M{
+ "$or": []bson.M{
+ {FN.PlayletChapter.MediaSourceId: bson.M{"$gt": ""}},
+ {FN.PlayletChapter.MediaTaskId: bson.M{"$gt": ""}},
+ },
+ }, options.Find().SetProjection(bson.M{
+ FN.PlayletChapter.MediaSourceId: 1,
+ FN.PlayletChapter.MediaTaskId: 1,
+ }))
+ if err != nil || len(list) == 0 {
+ return nil
+ }
+ ids := make([]string, 0, len(list)*2)
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ ids = append(ids, item.MediaSourceId, item.MediaTaskId)
+ }
+ return uniqueNonEmptyStrings(ids)
+}
+
+func uniqueNonEmptyStrings(ids []string) []string {
+ if len(ids) == 0 {
+ return nil
+ }
+ seen := make(map[string]struct{}, len(ids))
+ out := make([]string, 0, len(ids))
+ for _, id := range ids {
+ id = strings.TrimSpace(id)
+ if id == "" {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ out = append(out, id)
+ }
+ return out
+}
+
+// UpdateManyPlayletChapter 批量更新章节数据
+func UpdateManyPlayletChapter(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("批量更新短剧章节数据失败", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletchaptermod/tag.go b/pub/models/playletchaptermod/tag.go
new file mode 100644
index 0000000..6ec9dac
--- /dev/null
+++ b/pub/models/playletchaptermod/tag.go
@@ -0,0 +1,72 @@
+package playletchaptermod
+
+// TagPlayletChapter 保存 playlet_chapter 集合字段名
+type TagPlayletChapter struct {
+ Id string
+ PlayletId string
+ MediaContentId string
+ CurrentEpisode string
+ Title string
+ Md5 string
+ Hash string
+ Height string
+ Width string
+ Ratio string
+ VideoUrl string
+ WatermarkVideoUrl string
+ MediaSize string
+ MediaTime string
+ CountComment string
+ CountBuy string
+ CountBrowse string
+ CountCollect string
+ CountLike string
+ CountDisLike string
+ PayType string
+ SingleCoins string
+ WatermarkStatus string
+ MediaSourceId string
+ MediaTaskId string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletChapter TagPlayletChapter
+}
+
+var TN struct {
+ PlayletChapter string
+}
+
+func init() {
+ FN.PlayletChapter.Id = "_id"
+ FN.PlayletChapter.PlayletId = "playletId"
+ FN.PlayletChapter.MediaContentId = "mediaContentId"
+ FN.PlayletChapter.CurrentEpisode = "currentEpisode"
+ FN.PlayletChapter.Title = "title"
+ FN.PlayletChapter.Md5 = "md5"
+ FN.PlayletChapter.Hash = "hash"
+ FN.PlayletChapter.Height = "height"
+ FN.PlayletChapter.Width = "width"
+ FN.PlayletChapter.Ratio = "ratio"
+ FN.PlayletChapter.VideoUrl = "videoUrl"
+ FN.PlayletChapter.WatermarkVideoUrl = "watermarkVideoUrl"
+ FN.PlayletChapter.MediaSize = "mediaSize"
+ FN.PlayletChapter.MediaTime = "mediaTime"
+ FN.PlayletChapter.CountComment = "countComment"
+ FN.PlayletChapter.CountBuy = "countBuy"
+ FN.PlayletChapter.CountBrowse = "countBrowse"
+ FN.PlayletChapter.CountCollect = "countCollect"
+ FN.PlayletChapter.CountLike = "countLike"
+ FN.PlayletChapter.CountDisLike = "countDisLike"
+ FN.PlayletChapter.PayType = "payType"
+ FN.PlayletChapter.SingleCoins = "singleCoins"
+ FN.PlayletChapter.WatermarkStatus = "watermarkStatus"
+ FN.PlayletChapter.MediaSourceId = "mediaSourceId"
+ FN.PlayletChapter.MediaTaskId = "mediaTaskId"
+ FN.PlayletChapter.CreatedAt = "createdAt"
+ FN.PlayletChapter.UpdatedAt = "updatedAt"
+
+ TN.PlayletChapter = "playlet_chapter"
+}
diff --git a/pub/models/playletchaptermod/type.go b/pub/models/playletchaptermod/type.go
new file mode 100644
index 0000000..ac7d980
--- /dev/null
+++ b/pub/models/playletchaptermod/type.go
@@ -0,0 +1,59 @@
+package playletchaptermod
+
+import "time"
+
+// PayType 支付方式
+type PayType int32
+
+const (
+ PayTypeFree PayType = 0 // 免费
+ PayTypeCoin PayType = 1 // 金币
+ PayTypeVip PayType = 2 // VIP
+)
+
+// WatermarkStatus 水印状态
+type WatermarkStatus int32
+
+const (
+ WatermarkStatusPending WatermarkStatus = 0 // 未处理(待 mediafile/sync)
+ WatermarkStatusRegistered WatermarkStatus = 1 // 已登记(原片已登记,待 task/add)
+ WatermarkStatusProcessing WatermarkStatus = 2 // 处理中(已提交加水印任务,待回写)
+ WatermarkStatusDone WatermarkStatus = 3 // 已完成
+ WatermarkStatusFailed WatermarkStatus = 4 // 处理失败
+)
+
+// PlayletChapter @table: playlet_chapter
+//
+// PlayletChapter 连载短剧章节集合模型
+type PlayletChapter struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ MediaContentId uint `json:"mediaContentId" bson:"mediaContentId"` // 媒资中心章节ID
+ CurrentEpisode int64 `json:"currentEpisode" bson:"currentEpisode"` // 第几集
+ Title string `json:"title" bson:"title"` // 章节名
+ Md5 string `json:"md5" bson:"md5"` // 媒体md5
+ Hash string `json:"hash" bson:"hash"` // 媒体hash
+ Height int64 `json:"height" bson:"height"` // 高
+ Width int64 `json:"width" bson:"width"` // 宽
+ Ratio float64 `json:"ratio" bson:"ratio"` // 宽高比
+ VideoUrl string `json:"videoUrl" bson:"videoUrl"` // 原片视频地址(加水印后保持不变,方便换水印)
+ WatermarkVideoUrl string `json:"watermarkVideoUrl" bson:"watermarkVideoUrl"` // 加水印后的视频地址
+ MediaSize int64 `json:"mediaSize" bson:"mediaSize"` // 资源大小
+ MediaTime int64 `json:"mediaTime" bson:"mediaTime"` // 影片长度(秒)
+ CountComment int64 `json:"countComment" bson:"countComment"` // 评论数
+ CountBuy int64 `json:"countBuy" bson:"countBuy"` // 购买数
+ CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数
+ CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数
+ CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数
+ CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数
+ PayType PayType `json:"payType" bson:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins" bson:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ WatermarkStatus WatermarkStatus `json:"watermarkStatus" bson:"watermarkStatus"` // 水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败
+ MediaSourceId string `json:"mediaSourceId" bson:"mediaSourceId"` // media切片服原片ID
+ MediaTaskId string `json:"mediaTaskId" bson:"mediaTaskId"` // media切片服水印任务ID
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletChapterList 章节列表
+type PlayletChapterList []*PlayletChapter
diff --git a/pub/models/playletfavoritemod/favorite.go b/pub/models/playletfavoritemod/favorite.go
new file mode 100644
index 0000000..c560f2f
--- /dev/null
+++ b/pub/models/playletfavoritemod/favorite.go
@@ -0,0 +1,100 @@
+package playletfavoritemod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const CollectionName = "playlet_favorite"
+
+const (
+ FieldID = "_id"
+ FieldUserID = "userId"
+ FieldPlayletID = "playletId"
+ FieldCreatedAt = "createdAt"
+ FieldUpdatedAt = "updatedAt"
+)
+
+type PlayletFavorite struct {
+ ID int64 `bson:"_id,omitempty"`
+ UserID int64 `bson:"userId"`
+ PlayletID int64 `bson:"playletId"`
+ CreatedAt time.Time `bson:"createdAt"`
+ UpdatedAt time.Time `bson:"updatedAt"`
+}
+
+var coll *mongos.Collection
+
+// Init 初始化短剧收藏集合及查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(CollectionName)
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: FieldUserID, Value: 1}, {Key: FieldPlayletID, Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: FieldUserID, Value: 1}, {Key: FieldCreatedAt, Value: -1}, {Key: FieldID, Value: -1}}},
+ {Keys: bson.D{{Key: FieldPlayletID, Value: 1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletfavoritemod:Init) Create Index failed! err:%+v:", err))
+ }
+}
+
+// Upsert 按用户和短剧写入收藏关系并返回是否新增
+func Upsert(ctx context.Context, userID, playletID int64, now time.Time) (bool, *srvdef.SrvErr) {
+ filter := bson.M{FieldUserID: userID, FieldPlayletID: playletID}
+ update := bson.M{"$set": bson.M{FieldUpdatedAt: now}, "$setOnInsert": bson.M{
+ FieldID: mongos.Lid.PlayletFavorite.Get(), FieldUserID: userID, FieldPlayletID: playletID, FieldCreatedAt: now,
+ }}
+ result, err := coll.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
+ if err != nil {
+ log.Warn("playletfavoritemod:Upsert failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.UpsertedCount > 0, nil
+}
+
+// Delete 删除收藏关系并返回是否实际删除
+func Delete(ctx context.Context, filter bson.M) (bool, *srvdef.SrvErr) {
+ deleted, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playletfavoritemod:Delete failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return deleted > 0, nil
+}
+
+// Count 统计收藏关系数量
+func Count(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter)
+ if err != nil {
+ log.Warn("playletfavoritemod:Count failed", log.Any("filter", filter), log.E(err))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// FindMany 查询收藏关系列表
+func FindMany(ctx context.Context, filter bson.M, opts ...*options.FindOptions) ([]*PlayletFavorite, *srvdef.SrvErr) {
+ list := make([]*PlayletFavorite, 0)
+ if err := coll.Find(ctx, &list, filter, opts...); err != nil {
+ log.Warn("playletfavoritemod:FindMany failed", log.Any("filter", filter), log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return list, nil
+}
+
+// Aggregate 聚合收藏关系和短剧数据
+func Aggregate(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("playletfavoritemod:Aggregate failed", log.Any("pipeline", pipeline), log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playlethomeconfigmod/config.go b/pub/models/playlethomeconfigmod/config.go
new file mode 100644
index 0000000..5d81f66
--- /dev/null
+++ b/pub/models/playlethomeconfigmod/config.go
@@ -0,0 +1,168 @@
+package playlethomeconfigmod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_home_config 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletHomeConfig)
+ backfillSlugs(ctx)
+ initIndex(ctx)
+}
+
+// backfillSlugs 为历史配置生成稳定路径标识
+func backfillSlugs(ctx context.Context) {
+ var docs PlayletHomeConfigList
+ filter := bson.M{"$or": []bson.M{{FN.PlayletHomeConfig.Slug: bson.M{"$exists": false}}, {FN.PlayletHomeConfig.Slug: nil}, {FN.PlayletHomeConfig.Slug: ""}}}
+ if err := coll.Find(ctx, &docs, filter, options.Find().SetProjection(bson.M{FN.PlayletHomeConfig.Id: 1})); err != nil {
+ panic(fmt.Sprintf("(playlethomeconfigmod:backfillSlugs) Find failed! err:%+v", err))
+ }
+ for _, doc := range docs {
+ if doc == nil {
+ continue
+ }
+ if _, err := coll.UpdateOne(ctx, bson.M{FN.PlayletHomeConfig.Id: doc.Id, "$or": filter["$or"]}, bson.M{"$set": bson.M{FN.PlayletHomeConfig.Slug: fmt.Sprintf("home-config-%d", doc.Id)}}); err != nil {
+ panic(fmt.Sprintf("(playlethomeconfigmod:backfillSlugs) UpdateOne failed! id:%d err:%+v", doc.Id, err))
+ }
+ }
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletHomeConfig.Enabled, Value: 1},
+ {Key: FN.PlayletHomeConfig.ModuleType, Value: 1},
+ {Key: FN.PlayletHomeConfig.Rank, Value: -1},
+ {Key: FN.PlayletHomeConfig.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletHomeConfig.CreatedAt, Value: -1},
+ {Key: FN.PlayletHomeConfig.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{{Key: FN.PlayletHomeConfig.Slug, Value: 1}},
+ Options: options.Index().SetUnique(true).SetName("slug_unique"),
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playlethomeconfigmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletHomeConfig 插入一条首页配置
+func InsertOnePlayletHomeConfig(ctx context.Context, doc *PlayletHomeConfig) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletHomeConfig.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ normalizePlayletHomeConfig(doc)
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ return srvdef.New(srvdef.ErrParamError, "slug已存在", "")
+ }
+ log.Warn("插入首页配置失败", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+func normalizePlayletHomeConfig(doc *PlayletHomeConfig) {
+ if doc == nil {
+ return
+ }
+ if doc.ModuleType != ModuleTypeTopic {
+ doc.ModuleType = ModuleTypePlaylet
+ }
+ if doc.DataType != DataTypeVideo {
+ doc.DataType = DataTypeAll
+ }
+ if doc.Limit <= 0 {
+ doc.Limit = DefaultLimit
+ }
+ if doc.Limit > MaxLimit {
+ doc.Limit = MaxLimit
+ }
+}
+
+// DelManyPlayletHomeConfig 删除多条首页配置
+func DelManyPlayletHomeConfig(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("删除首页配置失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOnePlayletHomeConfig 查询一条首页配置
+func FindOnePlayletHomeConfig(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*PlayletHomeConfig, *srvdef.SrvErr) {
+ var doc PlayletHomeConfig
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("查询首页配置失败", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ normalizePlayletHomeConfig(&doc)
+ return &doc, nil
+}
+
+// FindManyPlayletHomeConfig 按条件查询多条首页配置
+func FindManyPlayletHomeConfig(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletHomeConfigList, *srvdef.SrvErr) {
+ data := make(PlayletHomeConfigList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("查询首页配置列表失败", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ for _, item := range data {
+ normalizePlayletHomeConfig(item)
+ }
+ return data, nil
+}
+
+// CountPlayletHomeConfig 统计满足条件的首页配置数量
+func CountPlayletHomeConfig(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("统计首页配置失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOnePlayletHomeConfig 更新一条首页配置
+func UpdateOnePlayletHomeConfig(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ return srvdef.New(srvdef.ErrParamError, "slug已存在", "")
+ }
+ log.Warn("更新首页配置失败", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playlethomeconfigmod/tag.go b/pub/models/playlethomeconfigmod/tag.go
new file mode 100644
index 0000000..3d3c7cd
--- /dev/null
+++ b/pub/models/playlethomeconfigmod/tag.go
@@ -0,0 +1,52 @@
+package playlethomeconfigmod
+
+// TagPlayletHomeConfig 保存 playlet_home_config 集合字段名
+type TagPlayletHomeConfig struct {
+ Id string
+ Slug string
+ Title string
+ Desc string
+ Label string
+ Style string
+ CardBackgroundImage string
+ ModuleType string
+ DataType string
+ RecommendType string
+ CategoryId string
+ TopicIds string
+ Rank string
+ Enabled string
+ Limit string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletHomeConfig TagPlayletHomeConfig
+}
+
+var TN struct {
+ PlayletHomeConfig string
+}
+
+func init() {
+ FN.PlayletHomeConfig.Id = "_id"
+ FN.PlayletHomeConfig.Slug = "slug"
+ FN.PlayletHomeConfig.Title = "title"
+ FN.PlayletHomeConfig.Desc = "desc"
+ FN.PlayletHomeConfig.Label = "label"
+ FN.PlayletHomeConfig.Style = "style"
+ FN.PlayletHomeConfig.CardBackgroundImage = "cardBackgroundImage"
+ FN.PlayletHomeConfig.ModuleType = "moduleType"
+ FN.PlayletHomeConfig.DataType = "dataType"
+ FN.PlayletHomeConfig.RecommendType = "recommendType"
+ FN.PlayletHomeConfig.CategoryId = "categoryId"
+ FN.PlayletHomeConfig.TopicIds = "topicIds"
+ FN.PlayletHomeConfig.Rank = "rank"
+ FN.PlayletHomeConfig.Enabled = "enabled"
+ FN.PlayletHomeConfig.Limit = "limit"
+ FN.PlayletHomeConfig.CreatedAt = "createdAt"
+ FN.PlayletHomeConfig.UpdatedAt = "updatedAt"
+
+ TN.PlayletHomeConfig = "playlet_home_config"
+}
diff --git a/pub/models/playlethomeconfigmod/type.go b/pub/models/playlethomeconfigmod/type.go
new file mode 100644
index 0000000..b394820
--- /dev/null
+++ b/pub/models/playlethomeconfigmod/type.go
@@ -0,0 +1,60 @@
+package playlethomeconfigmod
+
+import "time"
+
+const (
+ RecommendTypeUpdated int64 = 1 // 按更新
+ RecommendTypeLike int64 = 2 // 按点赞
+ RecommendTypeBrowse int64 = 3 // 按观看
+ RecommendTypeHeat int64 = 4 // 按热度
+ RecommendTypeCategory int64 = 5 // 按分类
+ DefaultLimit int64 = 30
+ MaxLimit int64 = 100
+)
+
+const (
+ DataTypeAll int64 = 1 // 全部上架短剧
+ DataTypeVideo int64 = 2 // 全部上架视频
+)
+
+const (
+ ModuleTypePlaylet int64 = 1 // 列表,具体为短剧或视频由 DataType 决定
+ ModuleTypeTopic int64 = 2 // 专题
+)
+
+const (
+ StyleA = "A"
+ StyleB = "B"
+ StyleC = "C"
+ StyleD = "D"
+ StyleE = "E"
+ StyleF = "F"
+ StyleG = "G"
+ StyleH = "H"
+)
+
+// PlayletHomeConfig @table: playlet_home_config
+//
+// PlayletHomeConfig 短剧首页配置表
+type PlayletHomeConfig struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Slug string `json:"slug" bson:"slug"` // 页面路径标识
+ Title string `json:"title" bson:"title"` // 模块标题
+ Desc string `json:"desc" bson:"desc"` // 模块描述
+ Label string `json:"label" bson:"label"` // 样式名称
+ Style string `json:"style" bson:"style"` // 样式 A-H
+ CardBackgroundImage string `json:"cardBackgroundImage" bson:"cardBackgroundImage"` // 卡片背景图
+ ModuleType int64 `json:"moduleType" bson:"moduleType"` // 模块类型 1列表 2专题,短剧或视频由 DataType 决定
+ DataType int64 `json:"dataType" bson:"dataType"` // 数据类型 1全部上架短剧 2全部上架视频
+ RecommendType int64 `json:"recommendType" bson:"recommendType"` // 推荐类型 1按更新 2按点赞 3按观看 4按热度 5按分类
+ CategoryId int64 `json:"categoryId" bson:"categoryId"` // 按分类推荐时使用的分类ID
+ TopicIds []int64 `json:"topicIds" bson:"topicIds"` // 专题模块关联的题材或视频分类ID
+ Rank int64 `json:"rank" bson:"rank"` // 排序,越大越靠前
+ Enabled bool `json:"enabled" bson:"enabled"` // 是否启用
+ Limit int64 `json:"limit" bson:"limit"` // 展示条数
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletHomeConfigList 首页配置列表
+type PlayletHomeConfigList []*PlayletHomeConfig
diff --git a/pub/models/playletlikemod/playlet_like.go b/pub/models/playletlikemod/playlet_like.go
new file mode 100644
index 0000000..980470f
--- /dev/null
+++ b/pub/models/playletlikemod/playlet_like.go
@@ -0,0 +1,83 @@
+package playletlikemod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_like 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletLike)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: FN.PlayletLike.UserID, Value: 1}, {Key: FN.PlayletLike.PlayletID, Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: FN.PlayletLike.PlayletID, Value: 1}}},
+ {Keys: bson.D{{Key: FN.PlayletLike.CreatedAt, Value: -1}, {Key: FN.PlayletLike.PlayletID, Value: 1}}},
+ {Keys: bson.D{{Key: FN.PlayletLike.UserID, Value: 1}, {Key: FN.PlayletLike.CreatedAt, Value: -1}, {Key: FN.PlayletLike.Id, Value: -1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletlikemod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// UpsertOnePlayletLike 按用户ID和短剧ID写入点赞关系,返回是否新增点赞
+func UpsertOnePlayletLike(ctx context.Context, userId, playletId int64, now time.Time) (bool, *srvdef.SrvErr) {
+ filter := bson.M{FN.PlayletLike.UserID: userId, FN.PlayletLike.PlayletID: playletId}
+ update := bson.M{
+ "$set": bson.M{FN.PlayletLike.UpdatedAt: now},
+ "$setOnInsert": bson.M{
+ FN.PlayletLike.Id: mongos.Lid.PlayletLike.Get(),
+ FN.PlayletLike.UserID: userId,
+ FN.PlayletLike.PlayletID: playletId,
+ FN.PlayletLike.CreatedAt: now,
+ },
+ }
+ result, err := coll.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
+ if err != nil {
+ log.Warn("playletlikemod:UpsertOnePlayletLike db.UpdateOne failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.UpsertedCount > 0, nil
+}
+
+// DeleteOnePlayletLike 删除一条短剧点赞关系,返回是否实际删除
+func DeleteOnePlayletLike(ctx context.Context, filter bson.M) (bool, *srvdef.SrvErr) {
+ result, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playletlikemod:DeleteOnePlayletLike db.DeleteOne failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return result > 0, nil
+}
+
+// CountPlayletLike 统计短剧点赞关系数量
+func CountPlayletLike(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter)
+ if err != nil {
+ log.Warn("playletlikemod:CountPlayletLike db.Count failed", log.Any("filter", filter), log.E(err))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// AggregatePlayletLike 聚合查询短剧点赞关系
+func AggregatePlayletLike(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("playletlikemod:AggregatePlayletLike db.Aggregate failed", log.Any("pipeline", pipeline), log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletlikemod/tag.go b/pub/models/playletlikemod/tag.go
new file mode 100644
index 0000000..1b220e1
--- /dev/null
+++ b/pub/models/playletlikemod/tag.go
@@ -0,0 +1,28 @@
+package playletlikemod
+
+// TagPlayletLike 保存 playlet_like 集合字段名
+type TagPlayletLike struct {
+ Id string
+ UserID string
+ PlayletID string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletLike TagPlayletLike
+}
+
+var TN struct {
+ PlayletLike string
+}
+
+func init() {
+ FN.PlayletLike.Id = "_id"
+ FN.PlayletLike.UserID = "userId"
+ FN.PlayletLike.PlayletID = "playletId"
+ FN.PlayletLike.CreatedAt = "createdAt"
+ FN.PlayletLike.UpdatedAt = "updatedAt"
+
+ TN.PlayletLike = "playlet_like"
+}
diff --git a/pub/models/playletlikemod/type.go b/pub/models/playletlikemod/type.go
new file mode 100644
index 0000000..b754994
--- /dev/null
+++ b/pub/models/playletlikemod/type.go
@@ -0,0 +1,15 @@
+package playletlikemod
+
+import "time"
+
+// PlayletLike 短剧点赞关系
+type PlayletLike struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ UserID int64 `json:"userId" bson:"userId"` // 用户ID
+ PlayletID int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
+}
+
+// PlayletLikeList 短剧点赞列表
+type PlayletLikeList []*PlayletLike
diff --git a/pub/models/playletmod/category_bind.go b/pub/models/playletmod/category_bind.go
new file mode 100644
index 0000000..f6584a7
--- /dev/null
+++ b/pub/models/playletmod/category_bind.go
@@ -0,0 +1,173 @@
+package playletmod
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+const categoryRankField = "_categoryRank"
+
+// PlayletCategoryBind 短剧绑定的分类及该分类下的排序
+type PlayletCategoryBind struct {
+ Id int64 `json:"id" bson:"id"` // 分类ID
+ Rank int64 `json:"rank" bson:"rank"` // 该分类下的排序,越大越靠前
+}
+
+// NormalizeCategoryBinds 去重并丢弃无效分类ID,保留首次出现的排序
+func NormalizeCategoryBinds(binds []PlayletCategoryBind) []PlayletCategoryBind {
+ result := make([]PlayletCategoryBind, 0, len(binds))
+ seen := make(map[int64]struct{}, len(binds))
+ for _, bind := range binds {
+ if bind.Id <= 0 {
+ continue
+ }
+ if _, ok := seen[bind.Id]; ok {
+ continue
+ }
+ seen[bind.Id] = struct{}{}
+ result = append(result, PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank})
+ }
+ return result
+}
+
+// CategoryIdsFromBinds 从分类绑定中提取分类ID
+func CategoryIdsFromBinds(binds []PlayletCategoryBind) []int64 {
+ result := make([]int64, 0, len(binds))
+ for _, bind := range binds {
+ if bind.Id <= 0 {
+ continue
+ }
+ result = append(result, bind.Id)
+ }
+ return result
+}
+
+// CategoryBindsFromIds 仅有分类ID时补默认排序0
+func CategoryBindsFromIds(ids []int64) []PlayletCategoryBind {
+ result := make([]PlayletCategoryBind, 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, PlayletCategoryBind{Id: id})
+ }
+ return result
+}
+
+// MergeCategoryBindsByIds 按新分类ID集合保留旧排序,新增分类rank为0
+func MergeCategoryBindsByIds(exist []PlayletCategoryBind, ids []int64) []PlayletCategoryBind {
+ rankByID := make(map[int64]int64, len(exist))
+ for _, bind := range exist {
+ if bind.Id <= 0 {
+ continue
+ }
+ rankByID[bind.Id] = bind.Rank
+ }
+ result := make([]PlayletCategoryBind, 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, PlayletCategoryBind{Id: id, Rank: rankByID[id]})
+ }
+ return result
+}
+
+// CategoryRankOf 取指定分类下的排序,未绑定返回0
+func CategoryRankOf(binds []PlayletCategoryBind, categoryId int64) int64 {
+ if categoryId <= 0 {
+ return 0
+ }
+ for _, bind := range binds {
+ if bind.Id == categoryId {
+ return bind.Rank
+ }
+ }
+ return 0
+}
+
+// EffectiveCategoryBinds 历史数据只有categoryIds时补默认绑定,便于编辑回显
+func EffectiveCategoryBinds(item *Playlet) []PlayletCategoryBind {
+ if item == nil {
+ return make([]PlayletCategoryBind, 0)
+ }
+ if len(item.CategoryBinds) > 0 {
+ return item.CategoryBinds
+ }
+ return CategoryBindsFromIds(item.CategoryIds)
+}
+
+// CategoryRankAddFields 聚合取出当前分类的排序字段,供分类列表排序
+func CategoryRankAddFields(categoryId int64) bson.M {
+ return bson.M{
+ categoryRankField: bson.M{
+ "$let": bson.M{
+ "vars": bson.M{
+ "hit": bson.M{
+ "$first": bson.M{
+ "$filter": bson.M{
+ "input": bson.M{"$ifNull": bson.A{"$categoryBinds", bson.A{}}},
+ "as": "b",
+ "cond": bson.M{"$eq": bson.A{"$$b.id", categoryId}},
+ },
+ },
+ },
+ },
+ "in": bson.M{"$ifNull": bson.A{"$$hit.rank", 0}},
+ },
+ },
+ }
+}
+
+// CategoryRankSort 分类短剧排序:当前分类rank降序,再按上架时间和ID
+func CategoryRankSort() bson.D {
+ return bson.D{
+ {Key: categoryRankField, Value: -1},
+ {Key: "onlineAt", Value: -1},
+ {Key: "_id", Value: -1},
+ }
+}
+
+// CategoryRankAdminSort 后台按分类查询时的排序:当前分类rank降序
+func CategoryRankAdminSort() bson.D {
+ return bson.D{
+ {Key: categoryRankField, Value: -1},
+ {Key: "_id", Value: -1},
+ }
+}
+
+// FindManyPlayletByCategoryRank 按指定分类的排序分页查询短剧
+func FindManyPlayletByCategoryRank(ctx context.Context, filter bson.M, categoryId, skip, limit int64, sort bson.D, projection bson.D) (PlayletList, *srvdef.SrvErr) {
+ pipeline := []bson.M{
+ {"$match": filter},
+ {"$addFields": CategoryRankAddFields(categoryId)},
+ {"$sort": sort},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$unset": categoryRankField},
+ }
+ if len(projection) > 0 {
+ project := bson.M{}
+ for _, field := range projection {
+ project[field.Key] = field.Value
+ }
+ pipeline = append(pipeline, bson.M{"$project": project})
+ }
+ list := make(PlayletList, 0)
+ if err := AggregatePlaylet(ctx, &list, pipeline); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
diff --git a/pub/models/playletmod/playlet.go b/pub/models/playletmod/playlet.go
new file mode 100644
index 0000000..b8937da
--- /dev/null
+++ b/pub/models/playletmod/playlet.go
@@ -0,0 +1,342 @@
+package playletmod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.Playlet)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ThemeIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ThemeIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CountCollect, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ThemeIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ThemeIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.IsRecommend, Value: 1},
+ {Key: FN.Playlet.RecommendRank, Value: -1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ThemeIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CountBrowse, Value: -1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.CategoryIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.TagIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.TagIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.IsRecommend, Value: 1},
+ {Key: FN.Playlet.RecommendRank, Value: -1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.TagIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CountBrowse, Value: -1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.CategoryIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.RecommendRank, Value: -1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.CategoryIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.IsRecommend, Value: 1},
+ {Key: FN.Playlet.RecommendRank, Value: -1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.CategoryIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.CategoryIds, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CountBrowse, Value: -1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.IsFeatured, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ShowOnBanner, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.TopRank, Value: -1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.IsRecommend, Value: 1},
+ {Key: FN.Playlet.RecommendRank, Value: -1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CountCollect, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CountBrowse, Value: -1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.HeatValue, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.OnlineAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.UpdatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.MediaCenterId, Value: 1},
+ },
+ Options: options.Index().
+ SetUnique(true).
+ SetPartialFilterExpression(bson.M{
+ FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
+ }),
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOnePlaylet 插入一条短剧数据
+func InsertOnePlaylet(ctx context.Context, doc *Playlet) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.Playlet.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("playletmod:InsertOnePlaylet failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOnePlaylet 删除一条短剧数据
+func DelOnePlaylet(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playletmod:DelOnePlaylet failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOnePlaylet 查询一条短剧数据
+func FindOnePlaylet(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*Playlet, *srvdef.SrvErr) {
+ var doc Playlet
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("playletmod:FindOnePlaylet failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyPlaylet 按条件查询多条短剧数据
+func FindManyPlaylet(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletList, *srvdef.SrvErr) {
+ data := make(PlayletList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("playletmod:FindManyPlaylet failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountPlaylet 统计满足条件的短剧数量
+func CountPlaylet(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("playletmod:CountPlaylet failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// AggregatePlaylet 聚合查询短剧数据
+func AggregatePlaylet(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline, opts...); err != nil {
+ log.Warn("playletmod:AggregatePlaylet failed", log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// UpdateOnePlaylet 更新一条短剧数据
+func UpdateOnePlaylet(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("playletmod:UpdateOnePlaylet failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// FindOneAndUpdatePlaylet 更新一条短剧数据并返回更新后的数据
+func FindOneAndUpdatePlaylet(ctx context.Context, filter bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) (*Playlet, *srvdef.SrvErr) {
+ var doc Playlet
+ if err := coll.FindOneAndUpdate(ctx, &doc, filter, update, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("更新并查询短剧数据失败", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// UpdateManyPlaylet 批量更新短剧数据
+func UpdateManyPlaylet(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("批量更新短剧数据失败", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// BulkWritePlaylet 批量写入短剧更新操作。
+func BulkWritePlaylet(ctx context.Context, docs []mongo.WriteModel, opts ...*options.BulkWriteOptions) *srvdef.SrvErr {
+ if len(docs) == 0 {
+ return nil
+ }
+ result, err := coll.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ log.Warn("批量写入短剧数据失败", log.E(err))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletmod/tag.go b/pub/models/playletmod/tag.go
new file mode 100644
index 0000000..a30ba35
--- /dev/null
+++ b/pub/models/playletmod/tag.go
@@ -0,0 +1,134 @@
+package playletmod
+
+// TagPlaylet 保存 playlet 集合字段名
+type TagPlaylet struct {
+ Id string
+ MediaCenterId string
+ MediaCreatedAt string
+ MediaUpdatedAt string
+ MediaSyncTime string
+ Status string
+ IsFeatured string
+ IsOriginal string
+ ShowOriginal string
+ MediaBusiness string
+ IsRecommend string
+ ShowOnBanner string
+ Banner string
+ ReviewAt string
+ OnlineAt string
+ CategoryIds string
+ CategoryBinds string
+ ThemeIds string
+ Title string
+ Slug string
+ Desc string
+ Anchor string
+ Cover string
+ CompressedCover string
+ Preview string
+ TagIds string
+ OldTags string
+ ContentType string
+ PayType string
+ SingleCoins string
+ TotalEpisode string
+ FreeEpisodeCount string
+ UpdateStatus string
+ TopRank string
+ RecommendRank string
+ CountComment string
+ CountBuy string
+ CountBrowse string
+ VirtualCountBrowse string
+ WeekWatchs string
+ MonthWatchs string
+ YearWatchs string
+ WatchUpdatedAt string
+ HeatValue string
+ VirtualHeatValue string
+ CountCollect string
+ CountLike string
+ VirtualCountLike string
+ CountDisLike string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ IndexNow string
+ OperatorId string
+ OperatorName string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ Playlet TagPlaylet
+}
+
+var TN struct {
+ Playlet string
+}
+
+func init() {
+ FN.Playlet.Id = "_id"
+ FN.Playlet.MediaCenterId = "mediaCenterId"
+ FN.Playlet.MediaCreatedAt = "mediaCreatedAt"
+ FN.Playlet.MediaUpdatedAt = "mediaUpdatedAt"
+ FN.Playlet.MediaSyncTime = "mediaSyncTime"
+ FN.Playlet.Status = "status"
+ FN.Playlet.IsFeatured = "isFeatured"
+ FN.Playlet.IsOriginal = "isOriginal"
+ FN.Playlet.ShowOriginal = "showOriginal"
+ FN.Playlet.MediaBusiness = "mediaBusiness"
+ FN.Playlet.IsRecommend = "isRecommend"
+ FN.Playlet.ShowOnBanner = "showOnBanner"
+ FN.Playlet.Banner = "banner"
+ FN.Playlet.ReviewAt = "reviewAt"
+ FN.Playlet.OnlineAt = "onlineAt"
+ FN.Playlet.CategoryIds = "categoryIds"
+ FN.Playlet.CategoryBinds = "categoryBinds"
+ FN.Playlet.ThemeIds = "themeIds"
+ FN.Playlet.Title = "title"
+ FN.Playlet.Slug = "slug"
+ FN.Playlet.Desc = "desc"
+ FN.Playlet.Anchor = "anchor"
+ FN.Playlet.Cover = "cover"
+ FN.Playlet.CompressedCover = "compressedCover"
+ FN.Playlet.Preview = "preview"
+ FN.Playlet.TagIds = "tagIds"
+ FN.Playlet.OldTags = "oldTags"
+ FN.Playlet.ContentType = "contentType"
+ FN.Playlet.PayType = "payType"
+ FN.Playlet.SingleCoins = "singleCoins"
+ FN.Playlet.TotalEpisode = "totalEpisode"
+ FN.Playlet.FreeEpisodeCount = "freeEpisodeCount"
+ FN.Playlet.UpdateStatus = "updateStatus"
+ FN.Playlet.TopRank = "topRank"
+ FN.Playlet.RecommendRank = "recommendRank"
+ FN.Playlet.CountComment = "countComment"
+ FN.Playlet.CountBuy = "countBuy"
+ FN.Playlet.CountBrowse = "countBrowse"
+ FN.Playlet.VirtualCountBrowse = "virtualCountBrowse"
+ FN.Playlet.WeekWatchs = "weekWatchs"
+ FN.Playlet.MonthWatchs = "monthWatchs"
+ FN.Playlet.YearWatchs = "yearWatchs"
+ FN.Playlet.WatchUpdatedAt = "watchUpdatedAt"
+ FN.Playlet.HeatValue = "heatValue"
+ FN.Playlet.VirtualHeatValue = "virtualHeatValue"
+ FN.Playlet.CountCollect = "countCollect"
+ FN.Playlet.CountLike = "countLike"
+ FN.Playlet.VirtualCountLike = "virtualCountLike"
+ FN.Playlet.CountDisLike = "countDisLike"
+ FN.Playlet.SeoInfo.Description = "seoInfo.webDescription"
+ FN.Playlet.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.Playlet.SeoInfo.Title = "seoInfo.webTitle"
+ FN.Playlet.IndexNow = "indexNow"
+ FN.Playlet.OperatorId = "operatorId"
+ FN.Playlet.OperatorName = "operatorName"
+ FN.Playlet.CreatedAt = "createdAt"
+ FN.Playlet.UpdatedAt = "updatedAt"
+
+ TN.Playlet = "playlet"
+}
diff --git a/pub/models/playletmod/type.go b/pub/models/playletmod/type.go
new file mode 100644
index 0000000..8750ec0
--- /dev/null
+++ b/pub/models/playletmod/type.go
@@ -0,0 +1,143 @@
+package playletmod
+
+import (
+ "strings"
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// ContentType 内容类型(与媒资 media_type 对应)
+type ContentType int32
+
+const (
+ ContentTypeTheater ContentType = 0 // 短剧(对应 media_type=5)
+ ContentTypeAlbum ContentType = 1 // 普通短剧/影集(对应 media_type=6)
+ ContentTypeComic ContentType = 2 // 漫画短剧(对应 media_type=4)
+ ContentTypeAnime ContentType = 3 // 动漫(对应 media_type=1)
+ ContentTypeManga ContentType = 4 // 漫画(对应 media_type=2)
+)
+
+// Status 上架状态
+type Status int32
+
+const (
+ StatusDown Status = 0 // 下架
+ StatusUp Status = 1 // 上架
+)
+
+// PayType 支付方式(剧级默认,连载上架后新增剧集沿用)
+type PayType int32
+
+const (
+ PayTypeFree PayType = 0 // 免费
+ PayTypeCoin PayType = 1 // 金币
+ PayTypeVip PayType = 2 // VIP
+)
+
+// Playlet @table: playlet
+//
+// Playlet 连载短剧集合模型
+type Playlet struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ MediaCenterId uint `json:"mediaCenterId" bson:"mediaCenterId"` // 媒资中心资源ID
+ MediaCreatedAt time.Time `json:"mediaCreatedAt" bson:"mediaCreatedAt"` // 媒资中心创建时间
+ MediaUpdatedAt time.Time `json:"mediaUpdatedAt" bson:"mediaUpdatedAt"` // 媒资中心更新时间
+ MediaSyncTime int64 `json:"mediaSyncTime" bson:"mediaSyncTime"` // 媒资中心增量同步时间点
+ Status Status `json:"status" bson:"status"` // 状态: 0下架 1上架
+ IsFeatured bool `json:"isFeatured" bson:"isFeatured"` // 是否爆剧(true-是,false-否)
+ IsOriginal bool `json:"isOriginal" bson:"isOriginal"` // 是否原创:后台新增为true,媒资导入为false
+ ShowOriginal bool `json:"showOriginal" bson:"showOriginal"` // 页面是否展示原创标识
+ MediaBusiness string `json:"mediaBusiness" bson:"mediaBusiness"` // 媒资业务来源,按媒资标题前缀记录,如 CGDJ;空表示未标记
+ IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 是否推荐 true-推荐,false-不推荐
+ ShowOnBanner bool `json:"showOnBanner" bson:"showOnBanner"` // 是否显示在首页banner
+ Banner string `json:"banner" bson:"banner"` // banner图
+ ReviewAt time.Time `json:"reviewAt" bson:"reviewAt"` // 审核时间
+ OnlineAt time.Time `json:"onlineAt" bson:"onlineAt"` // 上架时间
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类ID集合
+ CategoryBinds []PlayletCategoryBind `json:"categoryBinds" bson:"categoryBinds"` // 各分类下的排序
+ ThemeIds []int64 `json:"themeIds" bson:"themeIds"` // 题材ID集合
+ Title string `json:"title" bson:"title"` // 标题
+ Slug string `json:"slug" bson:"slug"` // slug
+ Desc string `json:"desc" bson:"desc"` // 简介
+ Anchor string `json:"anchor" bson:"anchor"` // 作者/演员
+ Cover string `json:"cover" bson:"cover"` // 封面原图
+ CompressedCover string `json:"compressedCover" bson:"compressedCover"` // 压缩封面,压缩成功才有值
+ Preview string `json:"preview" bson:"preview"` // 预告
+ TagIds []int64 `json:"tagIds" bson:"tagIds"` // 标签ID
+ OldTags []string `json:"oldTags" bson:"oldTags"` // 媒资库原标签名称
+ ContentType ContentType `json:"contentType" bson:"contentType"` // 内容类型: 0短剧 1普通短剧/影集 2漫画短剧 3动漫 4漫画
+ PayType PayType `json:"payType" bson:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins" bson:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ TotalEpisode int64 `json:"totalEpisode" bson:"totalEpisode"` // 总集数
+ FreeEpisodeCount int64 `json:"freeEpisodeCount" bson:"freeEpisodeCount"` // 官网开放集数 1至1000表示开放第1集到第N集 -1表示开放全集 0或字段缺失按2集
+ UpdateStatus int `json:"updateStatus" bson:"updateStatus"` // 更新状态 1-连载中 2-已完结
+ TopRank int64 `json:"topRank" bson:"topRank"` // 置顶排名
+ RecommendRank int64 `json:"recommendRank" bson:"recommendRank"` // 推荐排序
+ CountComment int64 `json:"countComment" bson:"countComment"` // 评论数
+ CountBuy int64 `json:"countBuy" bson:"countBuy"` // 购买数
+ CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数
+ VirtualCountBrowse int64 `json:"virtualCountBrowse" bson:"virtualCountBrowse"` // 后台虚拟观看数
+ WeekWatchs int64 `json:"weekWatchs" bson:"weekWatchs"` // 本周观看次数(北京时间)
+ MonthWatchs int64 `json:"monthWatchs" bson:"monthWatchs"` // 本月观看次数(北京时间)
+ YearWatchs int64 `json:"yearWatchs" bson:"yearWatchs"` // 本年观看次数(北京时间)
+ WatchUpdatedAt time.Time `json:"watchUpdatedAt" bson:"watchUpdatedAt"` // 最近一次观看计数时间
+ HeatValue int64 `json:"heatValue" bson:"heatValue"` // 热度值
+ VirtualHeatValue int64 `json:"virtualHeatValue" bson:"virtualHeatValue"` // 后台虚拟热度值
+ CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数
+ CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数
+ VirtualCountLike int64 `json:"virtualCountLike" bson:"virtualCountLike"` // 虚拟喜欢数,后台配置
+ CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 搜索引擎推送标记 0:待提交 1:已入队/已提交
+ OperatorId int64 `json:"operatorId" bson:"operatorId"` // 最后操作人ID(后台账号)
+ OperatorName string `json:"operatorName" bson:"operatorName"` // 最后操作人名称
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// EffectiveFreeEpisodeCount 历史短剧未配置时官网默认开放前2集
+func (p *Playlet) EffectiveFreeEpisodeCount() int64 {
+ if p == nil || p.FreeEpisodeCount == 0 {
+ return 2
+ }
+ return p.FreeEpisodeCount
+}
+
+// IsOfficialEpisodeOpen 判断剧集是否在官网开放范围内
+func (p *Playlet) IsOfficialEpisodeOpen(episode int64) bool {
+ if p == nil || p.Status != StatusUp || episode <= 0 {
+ return false
+ }
+ count := p.EffectiveFreeEpisodeCount()
+ return count == -1 || episode <= count
+}
+
+// PlayletList 短剧列表
+type PlayletList []*Playlet
+
+// DisplayCountBrowse 返回真实观看数与虚拟观看数之和
+func (p *Playlet) DisplayCountBrowse() int64 {
+ if p == nil {
+ return 0
+ }
+ return p.CountBrowse + p.VirtualCountBrowse
+}
+
+// DisplayHeatValue 返回原热度与虚拟热度之和
+func (p *Playlet) DisplayHeatValue() int64 {
+ if p == nil {
+ return 0
+ }
+ return p.HeatValue + p.VirtualHeatValue
+}
+
+// DisplayCover App 展示用封面:压缩图有值则优先,否则回退原图
+func (p *Playlet) DisplayCover() string {
+ if p == nil {
+ return ""
+ }
+ if cover := strings.TrimSpace(p.CompressedCover); cover != "" {
+ return cover
+ }
+ return strings.TrimSpace(p.Cover)
+}
diff --git a/pub/models/playlettagmod/playlettag.go b/pub/models/playlettagmod/playlettag.go
new file mode 100644
index 0000000..f9dcd2d
--- /dev/null
+++ b/pub/models/playlettagmod/playlettag.go
@@ -0,0 +1,121 @@
+package playlettagmod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_tag 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletTag)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTag.Status, Value: 1},
+ {Key: FN.PlayletTag.Rank, Value: -1},
+ {Key: FN.PlayletTag.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTag.Name, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTag.CreatedAt, Value: -1},
+ {Key: FN.PlayletTag.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playlettagmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletTag 插入一条标签数据
+func InsertOnePlayletTag(ctx context.Context, doc *PlayletTag) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletTag.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("playlettagmod:InsertOnePlayletTag failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOnePlayletTag 删除一条标签数据
+func DelOnePlayletTag(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playlettagmod:DelOnePlayletTag failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOnePlayletTag 查询一条标签数据
+func FindOnePlayletTag(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*PlayletTag, *srvdef.SrvErr) {
+ var doc PlayletTag
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("playlettagmod:FindOnePlayletTag failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyPlayletTag 按条件查询多条标签数据
+func FindManyPlayletTag(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletTagList, *srvdef.SrvErr) {
+ data := make(PlayletTagList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("playlettagmod:FindManyPlayletTag failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletTag 统计满足条件的标签数量
+func CountPlayletTag(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("playlettagmod:CountPlayletTag failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOnePlayletTag 更新一条标签数据
+func UpdateOnePlayletTag(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("playlettagmod:UpdateOnePlayletTag failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playlettagmod/tag.go b/pub/models/playlettagmod/tag.go
new file mode 100644
index 0000000..9751ed3
--- /dev/null
+++ b/pub/models/playlettagmod/tag.go
@@ -0,0 +1,38 @@
+package playlettagmod
+
+// TagPlayletTag 保存 playlet_tag 集合字段名
+type TagPlayletTag struct {
+ Id string
+ Name string
+ Status string
+ Rank string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletTag TagPlayletTag
+}
+
+var TN struct {
+ PlayletTag string
+}
+
+func init() {
+ FN.PlayletTag.Id = "_id"
+ FN.PlayletTag.Name = "name"
+ FN.PlayletTag.Status = "status"
+ FN.PlayletTag.Rank = "rank"
+ FN.PlayletTag.SeoInfo.Description = "seoInfo.webDescription"
+ FN.PlayletTag.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.PlayletTag.SeoInfo.Title = "seoInfo.webTitle"
+ FN.PlayletTag.CreatedAt = "createdAt"
+ FN.PlayletTag.UpdatedAt = "updatedAt"
+
+ TN.PlayletTag = "playlet_tag"
+}
diff --git a/pub/models/playlettagmod/type.go b/pub/models/playlettagmod/type.go
new file mode 100644
index 0000000..960fb80
--- /dev/null
+++ b/pub/models/playlettagmod/type.go
@@ -0,0 +1,22 @@
+package playlettagmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletTag @table: playlet_tag
+//
+// PlayletTag 短剧标签集合模型
+type PlayletTag struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // ID
+ Name string `json:"name" bson:"name"` // 标签名称
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Rank int64 `json:"rank" bson:"rank"` // 排序,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletTagList 标签列表
+type PlayletTagList []*PlayletTag
diff --git a/pub/models/playletthememod/tag.go b/pub/models/playletthememod/tag.go
new file mode 100644
index 0000000..fb8e962
--- /dev/null
+++ b/pub/models/playletthememod/tag.go
@@ -0,0 +1,42 @@
+package playletthememod
+
+// TagPlayletTheme 保存 playlet_theme 集合字段名
+type TagPlayletTheme struct {
+ Id string
+ Name string
+ Slug string
+ Icon string
+ Status string
+ Rank string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletTheme TagPlayletTheme
+}
+
+var TN struct {
+ PlayletTheme string
+}
+
+func init() {
+ FN.PlayletTheme.Id = "_id"
+ FN.PlayletTheme.Name = "name"
+ FN.PlayletTheme.Slug = "slug"
+ FN.PlayletTheme.Icon = "icon"
+ FN.PlayletTheme.Status = "status"
+ FN.PlayletTheme.Rank = "rank"
+ FN.PlayletTheme.SeoInfo.Description = "seoInfo.webDescription"
+ FN.PlayletTheme.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.PlayletTheme.SeoInfo.Title = "seoInfo.webTitle"
+ FN.PlayletTheme.CreatedAt = "createdAt"
+ FN.PlayletTheme.UpdatedAt = "updatedAt"
+
+ TN.PlayletTheme = "playlet_theme"
+}
diff --git a/pub/models/playletthememod/theme.go b/pub/models/playletthememod/theme.go
new file mode 100644
index 0000000..bba101d
--- /dev/null
+++ b/pub/models/playletthememod/theme.go
@@ -0,0 +1,121 @@
+package playletthememod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_theme 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletTheme)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTheme.Status, Value: 1},
+ {Key: FN.PlayletTheme.Rank, Value: -1},
+ {Key: FN.PlayletTheme.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTheme.Name, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTheme.CreatedAt, Value: -1},
+ {Key: FN.PlayletTheme.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletthememod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletTheme 插入一条题材数据
+func InsertOnePlayletTheme(ctx context.Context, doc *PlayletTheme) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletTheme.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("playletthememod:InsertOnePlayletTheme failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOnePlayletTheme 删除一条题材数据
+func DelOnePlayletTheme(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("playletthememod:DelOnePlayletTheme failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOnePlayletTheme 查询一条题材数据
+func FindOnePlayletTheme(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*PlayletTheme, *srvdef.SrvErr) {
+ var doc PlayletTheme
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("playletthememod:FindOnePlayletTheme failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyPlayletTheme 按条件查询多条题材数据
+func FindManyPlayletTheme(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletThemeList, *srvdef.SrvErr) {
+ data := make(PlayletThemeList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("playletthememod:FindManyPlayletTheme failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletTheme 统计满足条件的题材数量
+func CountPlayletTheme(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("playletthememod:CountPlayletTheme failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOnePlayletTheme 更新一条题材数据
+func UpdateOnePlayletTheme(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("playletthememod:UpdateOnePlayletTheme failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletthememod/type.go b/pub/models/playletthememod/type.go
new file mode 100644
index 0000000..8d08d59
--- /dev/null
+++ b/pub/models/playletthememod/type.go
@@ -0,0 +1,24 @@
+package playletthememod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletTheme @table: playlet_theme
+//
+// PlayletTheme 短剧题材集合模型
+type PlayletTheme struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Name string `json:"name" bson:"name"` // 题材名称
+ Slug string `json:"slug" bson:"slug"` // slug
+ Icon string `json:"icon" bson:"icon"` // 图标
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Rank int64 `json:"rank" bson:"rank"` // 排序,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
+}
+
+// PlayletThemeList 题材列表
+type PlayletThemeList []*PlayletTheme
diff --git a/pub/models/questionnairemod/questionnaire.go b/pub/models/questionnairemod/questionnaire.go
new file mode 100644
index 0000000..004a2ff
--- /dev/null
+++ b/pub/models/questionnairemod/questionnaire.go
@@ -0,0 +1,88 @@
+package questionnairemod
+
+import (
+ "context"
+ "fmt"
+
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.Questionnaire)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Questionnaire.Status, Value: 1},
+ {Key: FN.Questionnaire.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Questionnaire.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(questionnaire:Init) Create Index failed! err:%+v", err))
+ }
+}
+
+// InsertOne 插入一条问卷
+func InsertOne(ctx context.Context, doc *QuestionnaireModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, doc); err != nil {
+ log.Warn("questionnairemod:InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOne 查找一条问卷
+func FindOne(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*QuestionnaireModel, *srvdef.SrvErr) {
+ var doc QuestionnaireModel
+ if err := db.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ log.Warn("questionnairemod:FindOne failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &doc, nil
+}
+
+// FindMany 查找多条问卷
+func FindMany(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (QuestionnaireModelList, *srvdef.SrvErr) {
+ data := make(QuestionnaireModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("questionnairemod:FindMany failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// Count 统计问卷数量
+func Count(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("questionnairemod:Count failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOne 更新一条问卷,返回 MatchedCount
+func UpdateOne(ctx context.Context, filter bson.M, update bson.M) (int64, *srvdef.SrvErr) {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("questionnairemod:UpdateOne failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.MatchedCount, nil
+}
diff --git a/pub/models/questionnairemod/tag.go b/pub/models/questionnairemod/tag.go
new file mode 100644
index 0000000..ccd97e6
--- /dev/null
+++ b/pub/models/questionnairemod/tag.go
@@ -0,0 +1,64 @@
+package questionnairemod
+
+// 问卷状态枚举
+const (
+ StatusPending = "pending" //待处理
+ StatusHandled = "processed" //已处理
+ StatusRejected = "reject" //拒绝
+)
+
+// 附件类型枚举
+const (
+ AttachTypeImage = "图片"
+ AttachTypeVideo = "视频"
+)
+
+type TagQuestionnaire struct {
+ Id string
+ UserId string
+ QuestionType string
+ Frequency string
+ Province string
+ City string
+ Operator string
+ NetworkType string
+ DeviceType string
+ DeviceModel string
+ Description string
+ Contact string
+ Status string
+ Remark string
+ Attachments string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ Questionnaire TagQuestionnaire
+}
+
+var TN struct {
+ Questionnaire string
+}
+
+func init() {
+ FN.Questionnaire.Id = "_id"
+ FN.Questionnaire.UserId = "userId"
+ FN.Questionnaire.QuestionType = "questionType"
+ FN.Questionnaire.Frequency = "frequency"
+ FN.Questionnaire.Province = "province"
+ FN.Questionnaire.City = "city"
+ FN.Questionnaire.Operator = "operator"
+ FN.Questionnaire.NetworkType = "networkType"
+ FN.Questionnaire.DeviceType = "deviceType"
+ FN.Questionnaire.DeviceModel = "deviceModel"
+ FN.Questionnaire.Description = "description"
+ FN.Questionnaire.Contact = "contact"
+ FN.Questionnaire.Status = "status"
+ FN.Questionnaire.Remark = "remark"
+ FN.Questionnaire.Attachments = "attachments"
+ FN.Questionnaire.CreatedAt = "createdAt"
+ FN.Questionnaire.UpdatedAt = "updatedAt"
+
+ TN.Questionnaire = "questionnaires"
+}
diff --git a/pub/models/questionnairemod/type.go b/pub/models/questionnairemod/type.go
new file mode 100644
index 0000000..b38f7c1
--- /dev/null
+++ b/pub/models/questionnairemod/type.go
@@ -0,0 +1,37 @@
+package questionnairemod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// Attachment 内嵌附件文档
+type Attachment struct {
+ FileType string `json:"fileType" bson:"fileType"` // 附件类型(img / video)
+ Url string `json:"url" bson:"url"` // 附件URL地址
+}
+
+// QuestionnaireModel 问卷调查主文档
+// @table: questionnaires
+type QuestionnaireModel struct {
+ Id primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 唯一标识(MongoDB自动生成)
+ UserId int64 `json:"userId" bson:"userId"` // App端登录用户ID,0表示匿名
+ QuestionType string `json:"questionType" bson:"questionType"` // 问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题
+ Frequency string `json:"frequency" bson:"frequency"` // 出现频率:每次都会、经常出现、偶尔出现、仅现一次
+ Province string `json:"province" bson:"province"` // 所在省份(必填)
+ City string `json:"city" bson:"city"` // 所在城市(选填)
+ Operator string `json:"operator" bson:"operator"` // 运营商:电信、联通、移动、广电
+ NetworkType string `json:"networkType" bson:"networkType"` // 网络类型:WI-FI、5G、4G、其他
+ DeviceType string `json:"deviceType" bson:"deviceType"` // 设备类型:iPhone、Android、iPad、其他
+ DeviceModel string `json:"deviceModel" bson:"deviceModel"` // 设备型号(选填)
+ Description string `json:"description" bson:"description"` // 问题描述(必填)
+ Contact string `json:"contact" bson:"contact"` // 联系方式(选填)
+ Status string `json:"status" bson:"status"` // 处理状态:待处理、已处理、拒绝
+ Remark string `json:"remark" bson:"remark"` // 备注,处理问卷时填写(选填)
+ Attachments []Attachment `json:"attachments" bson:"attachments"` // 附件列表,最多3个
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间(自动生成)
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间(自动更新)
+}
+
+type QuestionnaireModelList []*QuestionnaireModel
diff --git a/pub/models/richtextcontentmod/richtextcontent.go b/pub/models/richtextcontentmod/richtextcontent.go
new file mode 100644
index 0000000..4a0990a
--- /dev/null
+++ b/pub/models/richtextcontentmod/richtextcontent.go
@@ -0,0 +1,160 @@
+package richtextcontentmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.RichTextContentModel)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.RichTextContentModel.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.RichTextContentModel.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.RichTextContentModel.Status, Value: 1},
+ {Key: FN.RichTextContentModel.Sort, Value: -1},
+ {Key: FN.RichTextContentModel.UpdatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(RichTextContentModelModel:initRichTextContentModelModelIndex) Create RichTextContentModelModel Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneRichTextContentModelModel 插入定时任务
+func InsertOneRichTextContentModelModel(ctx context.Context, RichTextContentModelModel *RichTextContentModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, RichTextContentModelModel); err != nil {
+ log.Warn("RichTextContentModelModel:InsertOneRichTextContentModelModel db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneRichTextContentModelModel 插入定时任务
+func FindOneRichTextContentModelModel(ctx context.Context, filter bson.M) (
+ *RichTextContentModel, *srvdef.SrvErr) {
+ var RichTextContentModelModel RichTextContentModel
+ if err := db.FindOneEx(ctx, &RichTextContentModelModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("RichTextContentModelModel:FindOneRichTextContentModelModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &RichTextContentModelModel, nil
+}
+
+// DelOneRichTextContentModelModel 插入定时任务
+func DelOneRichTextContentModelModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:DelOneRichTextContentModelModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneRichTextContentModelModel 插入定时任务
+func UpdateOneRichTextContentModelModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:UpdateOneRichTextContentModelModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpsertOneRichTextContentModelModel 更新插入一个记录
+func UpsertOneRichTextContentModelModel(ctx context.Context, filter bson.M, upsertDoc bson.M) *srvdef.SrvErr {
+ _, err := db.UpsertOne(ctx, filter, upsertDoc)
+ if err != nil {
+ log.Info("RichTextContentModelModel:UpsertOneRichTextContentModelModel db.UpsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// CountRichTextContentModelModels 统计用户数量
+func CountRichTextContentModelModels(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:CountRichTextContentModelModels db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateManyRichTextContentModelModel 更新满足条件的用户
+func UpdateManyRichTextContentModelModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:UpdateManyRichTextContentModelModel db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManyRichTextContentModelModel 查找一批用户数据
+func FindManyRichTextContentModelModel(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (RichTextContentModelList, *srvdef.SrvErr) {
+ data := make(RichTextContentModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("RichTextContentModelModel:FindManyRichTextContentModelModel db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManyRichTextContentModelModelWithBind 用自己定义结构查找一批用户数据
+func FindManyRichTextContentModelModelWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("RichTextContentModelModel:FindManyRichTextContentModelModelWithBind db.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateRichTextContentModelModels 聚合查找一批用户信息
+func AggregateRichTextContentModelModels(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("feedback:AggregateRichTextContentModelModels db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/richtextcontentmod/tag.go b/pub/models/richtextcontentmod/tag.go
new file mode 100755
index 0000000..8598aa1
--- /dev/null
+++ b/pub/models/richtextcontentmod/tag.go
@@ -0,0 +1,40 @@
+package richtextcontentmod
+
+type TagRichTextContentModel struct {
+ Content string
+ CreatedAt string
+ Desc string
+ Id string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ Title string
+ UpdatedAt string
+}
+
+var FN struct {
+ RichTextContentModel TagRichTextContentModel
+}
+
+var TN struct {
+ RichTextContentModel string
+}
+
+func init() {
+ FN.RichTextContentModel.Content = "content"
+ FN.RichTextContentModel.CreatedAt = "createdAt"
+ FN.RichTextContentModel.Desc = "desc"
+ FN.RichTextContentModel.Id = "_id"
+ FN.RichTextContentModel.SeoInfo.Description = "seoInfo.webDescription"
+ FN.RichTextContentModel.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.RichTextContentModel.SeoInfo.Title = "seoInfo.webTitle"
+ FN.RichTextContentModel.Sort = "sort"
+ FN.RichTextContentModel.Status = "status"
+ FN.RichTextContentModel.Title = "title"
+ FN.RichTextContentModel.UpdatedAt = "updatedAt"
+ TN.RichTextContentModel = "rich_text_content"
+}
diff --git a/pub/models/richtextcontentmod/type.go b/pub/models/richtextcontentmod/type.go
new file mode 100644
index 0000000..bdc8223
--- /dev/null
+++ b/pub/models/richtextcontentmod/type.go
@@ -0,0 +1,22 @@
+package richtextcontentmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// RichTextContentModel 富文本内容
+// @table:rich_text_content
+type RichTextContentModel struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 标题(内部)
+ Content string `json:"content" bson:"content"` // 内容
+ Status bool `json:"status" bson:"status"` // 是否启用
+ Sort int64 `json:"sort" bson:"sort"` // 排序值
+ Desc string `json:"desc" bson:"desc"` // 标题(app)
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt" ` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt" ` // 更新时间
+}
+
+type RichTextContentModelList []*RichTextContentModel
diff --git a/pub/models/seopushmod/push.go b/pub/models/seopushmod/push.go
new file mode 100644
index 0000000..6ed088c
--- /dev/null
+++ b/pub/models/seopushmod/push.go
@@ -0,0 +1,116 @@
+package seopushmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.SeoPushTask)
+ indexes := []mongo.IndexModel{
+ {
+ // 同一个页面的同一种动作只保留一条任务,重复发布时复用并重置状态
+ Keys: bson.D{
+ {Key: FN.SeoPushTask.Url, Value: 1},
+ {Key: FN.SeoPushTask.Action, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.SeoPushTask.GoogleStatus, Value: 1},
+ {Key: FN.SeoPushTask.IndexNowStatus, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.SeoPushTask.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(SeoPushTask:Init) Create SeoPushTask Index failed! err:%+v:", err))
+ }
+}
+
+// UpsertPendingSeoPushTask 登记一条待推送任务,已存在则重置为待推送状态
+func UpsertPendingSeoPushTask(ctx context.Context, task *SeoPushTask) *srvdef.SrvErr {
+ filter := bson.M{
+ FN.SeoPushTask.Url: task.Url,
+ FN.SeoPushTask.Action: task.Action,
+ }
+ update := bson.M{
+ "$set": bson.M{
+ FN.SeoPushTask.VideoId: task.VideoId,
+ FN.SeoPushTask.GoogleStatus: PushStatusPending,
+ FN.SeoPushTask.GoogleRetry: 0,
+ FN.SeoPushTask.GoogleErr: "",
+ FN.SeoPushTask.IndexNowStatus: PushStatusPending,
+ FN.SeoPushTask.IndexNowRetry: 0,
+ FN.SeoPushTask.IndexNowErr: "",
+ FN.SeoPushTask.UpdatedAt: task.UpdatedAt,
+ },
+ "$setOnInsert": bson.M{
+ FN.SeoPushTask.CreatedAt: task.CreatedAt,
+ },
+ }
+ if _, err := db.UpsertOne(ctx, filter, update); err != nil {
+ log.Warn("seopushmod:UpsertPendingSeoPushTask db.UpsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManySeoPushTasks 查找一批推送任务
+func FindManySeoPushTasks(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (SeoPushTaskList, *srvdef.SrvErr) {
+ data := make(SeoPushTaskList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("seopushmod:FindManySeoPushTasks db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// UpdateOneSeoPushTask 更新一条推送任务
+func UpdateOneSeoPushTask(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ if _, err := db.UpdateOne(ctx, filter, update); err != nil {
+ log.Warn("seopushmod:UpdateOneSeoPushTask db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// DelManySeoPushTask 删除一批推送任务
+func DelManySeoPushTask(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("seopushmod:DelManySeoPushTask db.DeleteMany failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// CountSeoPushTasks 统计推送任务数量
+func CountSeoPushTasks(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("seopushmod:CountSeoPushTasks db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
diff --git a/pub/models/seopushmod/tag.go b/pub/models/seopushmod/tag.go
new file mode 100644
index 0000000..cac28f7
--- /dev/null
+++ b/pub/models/seopushmod/tag.go
@@ -0,0 +1,51 @@
+package seopushmod
+
+type TagSeoPushTask struct {
+ ID string
+ Url string
+ VideoId string
+ Action string
+
+ GoogleStatus string
+ GoogleRetry string
+ GoogleErr string
+ GoogleAt string
+
+ IndexNowStatus string
+ IndexNowRetry string
+ IndexNowErr string
+ IndexNowAt string
+
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ SeoPushTask TagSeoPushTask
+}
+
+var TN struct {
+ SeoPushTask string
+}
+
+func init() {
+ FN.SeoPushTask.ID = "_id"
+ FN.SeoPushTask.Url = "url"
+ FN.SeoPushTask.VideoId = "videoId"
+ FN.SeoPushTask.Action = "action"
+
+ FN.SeoPushTask.GoogleStatus = "googleStatus"
+ FN.SeoPushTask.GoogleRetry = "googleRetry"
+ FN.SeoPushTask.GoogleErr = "googleErr"
+ FN.SeoPushTask.GoogleAt = "googleAt"
+
+ FN.SeoPushTask.IndexNowStatus = "indexNowStatus"
+ FN.SeoPushTask.IndexNowRetry = "indexNowRetry"
+ FN.SeoPushTask.IndexNowErr = "indexNowErr"
+ FN.SeoPushTask.IndexNowAt = "indexNowAt"
+
+ FN.SeoPushTask.CreatedAt = "createdAt"
+ FN.SeoPushTask.UpdatedAt = "updatedAt"
+
+ TN.SeoPushTask = "seo_push_task"
+}
diff --git a/pub/models/seopushmod/type.go b/pub/models/seopushmod/type.go
new file mode 100644
index 0000000..d8e8e1d
--- /dev/null
+++ b/pub/models/seopushmod/type.go
@@ -0,0 +1,47 @@
+package seopushmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// SeoPushTask 搜索引擎推送任务(Google Indexing API / IndexNow)
+// 发布/更新/删除时即时推送一次,本表用于失败兜底重试。
+// @table:seo_push_task
+type SeoPushTask struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Url string `json:"url" bson:"url"` // 页面完整地址
+ VideoId int64 `json:"videoId" bson:"videoId"` // 关联资源ID
+ Action string `json:"action" bson:"action"` // 推送动作 updated/deleted
+
+ GoogleStatus int64 `json:"googleStatus" bson:"googleStatus"` // Google 推送状态
+ GoogleRetry int64 `json:"googleRetry" bson:"googleRetry"` // Google 已重试次数
+ GoogleErr string `json:"googleErr" bson:"googleErr"` // Google 最近一次错误
+ GoogleAt time.Time `json:"googleAt" bson:"googleAt"` // Google 最近一次推送时间
+
+ IndexNowStatus int64 `json:"indexNowStatus" bson:"indexNowStatus"` // IndexNow 推送状态
+ IndexNowRetry int64 `json:"indexNowRetry" bson:"indexNowRetry"` // IndexNow 已重试次数
+ IndexNowErr string `json:"indexNowErr" bson:"indexNowErr"` // IndexNow 最近一次错误
+ IndexNowAt time.Time `json:"indexNowAt" bson:"indexNowAt"` // IndexNow 最近一次推送时间
+
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+
+type SeoPushTaskList []*SeoPushTask
+
+// 推送动作
+const (
+ ActionUpdated = "updated" // 发布或更新,对应 Google URL_UPDATED
+ ActionDeleted = "deleted" // 删除,对应 Google URL_DELETED
+)
+
+// 推送状态
+const (
+ PushStatusPending = int64(0) // 待推送
+ PushStatusSuccess = int64(1) // 推送成功
+)
+
+// MaxPushRetry 单个通道最大重试次数,达到上限后不再被补推任务捞取
+const MaxPushRetry = int64(5)
diff --git a/pub/models/skdrecord/record.go b/pub/models/skdrecord/record.go
new file mode 100644
index 0000000..b93294a
--- /dev/null
+++ b/pub/models/skdrecord/record.go
@@ -0,0 +1,153 @@
+package skdrecord
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.SkdRecord)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.SkdRecord.Job, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.SkdRecord.CreatedAt, Value: 1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(SkdRecord:initSkdRecordIndex) Create SkdRecord Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneSkdRecord 插入定时任务
+func InsertOneSkdRecord(ctx context.Context, SkdRecord *SkdRecord) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, SkdRecord); err != nil {
+ log.Warn("SkdRecord:InsertOneSkdRecord db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneSkdRecord 插入定时任务
+func FindOneSkdRecord(ctx context.Context, filter bson.M) (
+ *SkdRecord, *srvdef.SrvErr) {
+ var SkdRecord SkdRecord
+ if err := db.FindOneEx(ctx, &SkdRecord, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("SkdRecord:FindOneSkdRecord db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrSkdRecordNotExsit, "", "")
+ }
+ return &SkdRecord, nil
+}
+
+// DelOneSkdRecord 插入定时任务
+func DelOneSkdRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("SkdRecord:DelOneSkdRecord db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneSkdRecord 插入定时任务
+func UpdateOneSkdRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("SkdRecord:UpdateOneSkdRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrSkdRecordNotExsit, "", "")
+ }
+ return nil
+}
+
+// UpsertOneSkdRecord 更新插入一个记录
+func UpsertOneSkdRecord(ctx context.Context, filter bson.M, upsertDoc bson.M) *srvdef.SrvErr {
+ _, err := db.UpsertOne(ctx, filter, upsertDoc)
+ if err != nil {
+ log.Info("SkdRecord:UpsertOneSkdRecord db.UpsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// CountSkdRecords 统计用户数量
+func CountSkdRecords(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("SkdRecord:CountSkdRecords db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateManySkdRecord 更新满足条件的用户
+func UpdateManySkdRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("SkdRecord:UpdateManySkdRecord db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManySkdRecord 查找一批用户数据
+func FindManySkdRecord(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (SkdRecordList, *srvdef.SrvErr) {
+ data := make(SkdRecordList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("SkdRecord:FindManySkdRecord db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManySkdRecordWithBind 用自己定义结构查找一批用户数据
+func FindManySkdRecordWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("SkdRecord:FindManySkdRecordWithBind db.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateSkdRecords 聚合查找一批用户信息
+func AggregateSkdRecords(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("feedback:AggregateSkdRecords db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/skdrecord/tag.go b/pub/models/skdrecord/tag.go
new file mode 100755
index 0000000..0344cef
--- /dev/null
+++ b/pub/models/skdrecord/tag.go
@@ -0,0 +1,30 @@
+package skdrecord
+
+type TagSkdRecord struct {
+ CreatedAt string
+ FirstRecordTime string
+ ID string
+ Item string
+ Job string
+ RecordTime string
+ UpdatedAt string
+}
+
+var FN struct {
+ SkdRecord TagSkdRecord
+}
+
+var TN struct {
+ SkdRecord string
+}
+
+func init() {
+ FN.SkdRecord.CreatedAt = "createdAt"
+ FN.SkdRecord.FirstRecordTime = "firstRecordTime"
+ FN.SkdRecord.ID = "_id"
+ FN.SkdRecord.Item = "item"
+ FN.SkdRecord.Job = "job"
+ FN.SkdRecord.RecordTime = "recordTime"
+ FN.SkdRecord.UpdatedAt = "updatedAt"
+ TN.SkdRecord = "skd_record"
+}
diff --git a/pub/models/skdrecord/type.go b/pub/models/skdrecord/type.go
new file mode 100644
index 0000000..bbd641c
--- /dev/null
+++ b/pub/models/skdrecord/type.go
@@ -0,0 +1,27 @@
+package skdrecord
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 表名
+// SkdRecord 获取充值记录
+// @table:skd_record
+type SkdRecord struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Job string `bson:"job"` // 工作
+ Item string `bson:"item"` // 统计项
+ RecordTime time.Time `bson:"recordTime"` // 提交时间
+ FirstRecordTime time.Time `bson:"firstRecordTime"` // 第一次提交时间
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+}
+
+const (
+ GameRecordJob = "GameRecordJob" // 按天统计访问信息
+ LoufengUpdate = "LoufengUpdate" // 楼风更新信息
+)
+
+type SkdRecordList []*SkdRecord
diff --git a/pub/models/syncfilemod/sync.go b/pub/models/syncfilemod/sync.go
new file mode 100644
index 0000000..60577bb
--- /dev/null
+++ b/pub/models/syncfilemod/sync.go
@@ -0,0 +1,118 @@
+package syncfilemod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.SyncRecord)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.SyncRecord.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(SyncRecord:InitSyncRecord) Create SyncRecord Index failed! err:%+v:", err))
+ }
+}
+
+// CountSyncRecord 统计活动数量
+func CountSyncRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("SyncRecordmod:CountSyncRecord db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// InsertOneSyncRecord 插入一条活动
+func InsertOneSyncRecord(ctx context.Context, SyncRecord *SyncRecord) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, SyncRecord); err != nil {
+ log.Info("SyncRecordmod:InsertOneSyncRecord db.InsertOne failed",
+ log.E(err), log.Any("SyncRecord", SyncRecord))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneSyncRecord 删除一个活动
+func DelOneSyncRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("SyncRecordmod:DelOneSyncRecord db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneSyncRecord 更新一个SyncRecord
+func UpdateOneSyncRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("SyncRecordmod:UpdateOneSyncRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("SyncRecordmod:UpdateOneSyncRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ObjectNotExist, "", "")
+ }
+ return nil
+}
+
+// UpsertOneSyncRecord 更新一个 SyncRecord,不存在则新建
+func UpsertOneSyncRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ if _, err := db.UpsertOne(ctx, filter, update); err != nil {
+ log.Warn("SyncRecordmod:UpsertOneSyncRecord db.UpsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManySyncRecord 查找一批活动数据
+func FindManySyncRecord(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (SyncRecordList, *srvdef.SrvErr) {
+ data := make(SyncRecordList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("SyncRecordmod:FindManyAcivities db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindOneSyncRecord 查找一个媒体
+func FindOneSyncRecord(ctx context.Context, filter bson.M) (
+ *SyncRecord, *srvdef.SrvErr) {
+ var SyncRecord SyncRecord
+ if err := db.FindOneEx(ctx, &SyncRecord, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("SyncRecord:FindOneSyncRecord db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ObjectDayStatDoesNotExist, "", "")
+ }
+ return &SyncRecord, nil
+}
diff --git a/pub/models/syncfilemod/tag.go b/pub/models/syncfilemod/tag.go
new file mode 100755
index 0000000..b9da6f4
--- /dev/null
+++ b/pub/models/syncfilemod/tag.go
@@ -0,0 +1,30 @@
+package syncfilemod
+
+type TagSyncRecord struct {
+ CreatedAt string
+ ID string
+ Job string
+ RecordId string
+ SuccessAt string
+ UpdatedAt string
+ RecordTime string
+}
+
+var FN struct {
+ SyncRecord TagSyncRecord
+}
+
+var TN struct {
+ SyncRecord string
+}
+
+func init() {
+ FN.SyncRecord.CreatedAt = "createdAt"
+ FN.SyncRecord.ID = "_id"
+ FN.SyncRecord.Job = "job"
+ FN.SyncRecord.RecordId = "recordId"
+ FN.SyncRecord.SuccessAt = "successAt"
+ FN.SyncRecord.UpdatedAt = "updatedAt"
+ FN.SyncRecord.RecordTime = "recordTime"
+ TN.SyncRecord = "sync_record"
+}
\ No newline at end of file
diff --git a/pub/models/syncfilemod/type.go b/pub/models/syncfilemod/type.go
new file mode 100644
index 0000000..9abe355
--- /dev/null
+++ b/pub/models/syncfilemod/type.go
@@ -0,0 +1,30 @@
+package syncfilemod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 资源同步
+// @table:sync_record
+type SyncRecord struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Job string `json:"job" bson:"job"` // 工作
+ RecordId int64 `json:"recordId" bson:"recordId"` // 用户ID
+ RecordTime time.Time `json:"recordTime" bson:"recordTime"` // 记录时间
+ SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+type SyncRecordList []*SyncRecord
+
+const (
+ VideoJob = "videoJob" // 视频同步
+ PlayletWatermarkJob = "playletWatermarkJob" // 短剧水印同步
+ SitemapDirtyJob = "sitemapDirtyJob" // sitemap 内容变更标记(web 写入,app 消费)
+)
+
+var SyncJobDescribe = map[string]string{
+ SitemapDirtyJob: "sitemap内容变更标记",
+}
diff --git a/pub/models/translationcategorymod/category.go b/pub/models/translationcategorymod/category.go
new file mode 100644
index 0000000..6f1b04e
--- /dev/null
+++ b/pub/models/translationcategorymod/category.go
@@ -0,0 +1,120 @@
+package translationcategorymod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.TranslationCategory)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationCategory.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationCategory.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationCategory.Status, Value: 1},
+ {Key: FN.TranslationCategory.CategoryType, Value: 1},
+ {Key: FN.TranslationCategory.Rank, Value: -1},
+ {Key: FN.TranslationCategory.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initTranslationCategoryIndex) Create TranslationCategory Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneTranslationCategory 插入一个导航分类
+func InsertOneTranslationCategory(ctx context.Context, qa *TranslationCategory) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("TranslationCategorymod:InsertOneTranslationCategory db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneTranslationCategory 删除一个导航分类
+func DelOneTranslationCategory(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("TranslationCategorymod:DelOneTranslationCategory db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneTranslationCategory 查找一个导航分类
+func FindOneTranslationCategory(ctx context.Context, filter bson.M) (
+ *TranslationCategory, *srvdef.SrvErr) {
+ var TranslationCategory TranslationCategory
+ if err := db.FindOneEx(ctx, &TranslationCategory, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("TranslationCategorymod:FindOneTranslationCategory db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &TranslationCategory, nil
+}
+
+func FindManyTranslationCategorys(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (TranslationCategoryList, *srvdef.SrvErr) {
+ data := make(TranslationCategoryList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyTranslationCategorys", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountTranslationCategorys 统计导航分类数量
+func CountTranslationCategorys(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountTranslationCategorys db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneTranslationCategory 更新一个导航分类
+func UpdateOneTranslationCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneTranslationCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneTranslationCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/translationcategorymod/tag.go b/pub/models/translationcategorymod/tag.go
new file mode 100755
index 0000000..9bf402d
--- /dev/null
+++ b/pub/models/translationcategorymod/tag.go
@@ -0,0 +1,34 @@
+package translationcategorymod
+
+type TagTranslationCategory struct {
+ CategoryType string
+ CreatedAt string
+ Id string
+ Name string
+ Rank string
+ Status string
+ UpdatedAt string
+ UrlValue string
+ Icon string
+}
+
+var FN struct {
+ TranslationCategory TagTranslationCategory
+}
+
+var TN struct {
+ TranslationCategory string
+}
+
+func init() {
+ FN.TranslationCategory.CategoryType = "categoryType"
+ FN.TranslationCategory.CreatedAt = "createdAt"
+ FN.TranslationCategory.Id = "_id"
+ FN.TranslationCategory.Name = "name"
+ FN.TranslationCategory.Rank = "rank"
+ FN.TranslationCategory.Status = "status"
+ FN.TranslationCategory.UpdatedAt = "updatedAt"
+ FN.TranslationCategory.UrlValue = "urlValue"
+ FN.TranslationCategory.Icon = "icon"
+ TN.TranslationCategory = "translation_category"
+}
diff --git a/pub/models/translationcategorymod/type.go b/pub/models/translationcategorymod/type.go
new file mode 100644
index 0000000..d49c172
--- /dev/null
+++ b/pub/models/translationcategorymod/type.go
@@ -0,0 +1,20 @@
+package translationcategorymod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:translation_category
+type TranslationCategory struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ Icon string `json:"icon" bson:"icon"` // Icon
+ Status bool `json:"status" bson:"status"` // 上架状态
+ CategoryType pubdef.TranslationType `json:"categoryType" bson:"categoryType"` // 导航类型 1:url 2:二级导航 3:底部导航
+ UrlValue string `json:"urlValue" bson:"urlValue"` // 跳转URL
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type TranslationCategoryList []*TranslationCategory
diff --git a/pub/models/translationmod/tag.go b/pub/models/translationmod/tag.go
new file mode 100755
index 0000000..671d058
--- /dev/null
+++ b/pub/models/translationmod/tag.go
@@ -0,0 +1,36 @@
+package translationmod
+
+type TagTranslationInfo struct {
+ CreatedAt string
+ HtmlId string
+ Id string
+ Link string
+ Name string
+ Rank string
+ Status string
+ TransCategoryId string
+ UpdatedAt string
+ IsFriendAds string
+}
+
+var FN struct {
+ TranslationInfo TagTranslationInfo
+}
+
+var TN struct {
+ TranslationInfo string
+}
+
+func init() {
+ FN.TranslationInfo.CreatedAt = "createdAt"
+ FN.TranslationInfo.HtmlId = "htmlId"
+ FN.TranslationInfo.Id = "_id"
+ FN.TranslationInfo.Link = "link"
+ FN.TranslationInfo.Name = "name"
+ FN.TranslationInfo.Rank = "rank"
+ FN.TranslationInfo.Status = "status"
+ FN.TranslationInfo.TransCategoryId = "transCategoryId"
+ FN.TranslationInfo.UpdatedAt = "updatedAt"
+ FN.TranslationInfo.IsFriendAds = "isFriendAds"
+ TN.TranslationInfo = "translation_info"
+}
diff --git a/pub/models/translationmod/translation.go b/pub/models/translationmod/translation.go
new file mode 100644
index 0000000..9f838bc
--- /dev/null
+++ b/pub/models/translationmod/translation.go
@@ -0,0 +1,120 @@
+package translationmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.TranslationInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationInfo.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationInfo.Status, Value: 1},
+ {Key: FN.TranslationInfo.TransCategoryId, Value: 1},
+ {Key: FN.TranslationInfo.Rank, Value: -1},
+ {Key: FN.TranslationInfo.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initTranslationInfoIndex) Create TranslationInfo Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneTranslationInfo 插入一个渠道统计
+func InsertOneTranslationInfo(ctx context.Context, qa *TranslationInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("TranslationInfomod:InsertOneTranslationInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneTranslationInfo 删除一个渠道统计
+func DelOneTranslationInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("TranslationInfomod:DelOneTranslationInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneTranslationInfo 查找一个渠道统计
+func FindOneTranslationInfo(ctx context.Context, filter bson.M) (
+ *TranslationInfo, *srvdef.SrvErr) {
+ var TranslationInfo TranslationInfo
+ if err := db.FindOneEx(ctx, &TranslationInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("TranslationInfomod:FindOneTranslationInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &TranslationInfo, nil
+}
+
+func FindManyTranslationInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (TranslationInfoList, *srvdef.SrvErr) {
+ data := make(TranslationInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyTranslationInfos", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountTranslationInfos 统计渠道统计数量
+func CountTranslationInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountTranslationInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneTranslationInfo 更新一个渠道统计
+func UpdateOneTranslationInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneTranslationInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneTranslationInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/translationmod/type.go b/pub/models/translationmod/type.go
new file mode 100644
index 0000000..f0270ef
--- /dev/null
+++ b/pub/models/translationmod/type.go
@@ -0,0 +1,20 @@
+package translationmod
+
+import (
+ "time"
+)
+
+// @table:translation_info
+type TranslationInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ TransCategoryId int64 `json:"transCategoryId" bson:"transCategoryId"` // 导航分类
+ IsFriendAds bool `json:"isFriendAds" bson:"isFriendAds"` // 是否是友链广告位
+ HtmlId int64 `json:"htmlId" bson:"htmlId"` // 富文本ID
+ Link string `json:"link" bson:"link"` // 外部跳转
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ Status bool `json:"status" bson:"status"` // 上架状态
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type TranslationInfoList []*TranslationInfo
diff --git a/pub/models/userlogin/tag.go b/pub/models/userlogin/tag.go
new file mode 100755
index 0000000..aaef1ff
--- /dev/null
+++ b/pub/models/userlogin/tag.go
@@ -0,0 +1,26 @@
+package userlogin
+
+type TagUserLogin struct {
+ CreatedAt string
+ Date string
+ Id string
+ UserId string
+ ContinuousLoginCount string
+}
+
+var FN struct {
+ UserLogin TagUserLogin
+}
+
+var TN struct {
+ UserLogin string
+}
+
+func init() {
+ FN.UserLogin.CreatedAt = "createdAt"
+ FN.UserLogin.Date = "date"
+ FN.UserLogin.Id = "_id"
+ FN.UserLogin.UserId = "user_id"
+ FN.UserLogin.ContinuousLoginCount = "continuousLoginCount"
+ TN.UserLogin = "user_login"
+}
diff --git a/pub/models/userlogin/type.go b/pub/models/userlogin/type.go
new file mode 100644
index 0000000..c4744e6
--- /dev/null
+++ b/pub/models/userlogin/type.go
@@ -0,0 +1,17 @@
+package userlogin
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// @table:user_login
+type UserLogin struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"` //ID
+ Date string `json:"date" bson:"date"` //签到日期,格式:pubdef.DateISO
+ UserId int64 `json:"user_id" bson:"user_id"` //用户ID
+ ContinuousLoginCount int `json:"continuousLoginCount" bson:"continuousLoginCount"` //连续登陆次数
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间
+}
+type UserLoginList []*UserLogin
diff --git a/pub/models/userlogin/userlogin.go b/pub/models/userlogin/userlogin.go
new file mode 100644
index 0000000..2ac0052
--- /dev/null
+++ b/pub/models/userlogin/userlogin.go
@@ -0,0 +1,108 @@
+package userlogin
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.UserLogin)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.UserLogin.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.UserLogin.UserId, Value: 1},
+ {Key: FN.UserLogin.Date, Value: 1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(userlogin:InitUserLogin) Create UserLogin Index failed! err:%+v:", err))
+ }
+}
+
+// InsertUserLogin 插入一个登陆记录
+func InsertUserLogin(ctx context.Context, login *UserLogin) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, login); err != nil {
+ log.Warn("userlogin:InsertUserLogin db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateLogin 聚合查找一批登陆记录
+func AggregateLogin(ctx context.Context,
+ pipeline []bson.M) (*UserLogin, *srvdef.SrvErr) {
+ var login UserLogin
+ if err := db.ObjectAggregate(ctx, &login, pipeline); err != nil {
+ log.Warn("userlogin:AggregateLogin db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &login, nil
+}
+
+// FindOneLogin 查找一个登陆记录
+func FindOneLogin(ctx context.Context, filter bson.M) (
+ *UserLogin, *srvdef.SrvErr) {
+ var login UserLogin
+ if err := db.FindOneEx(ctx, &login, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("User:FindOneUser db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, nil
+ }
+ return &login, nil
+}
+
+// CountLogins 统计登陆记录数量
+func CountLogin(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("userlogin:CountLogins db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// FindManyClickStats 查找一批数据
+func FindManyLogin(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (UserLoginList, *srvdef.SrvErr) {
+ data := make(UserLoginList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("userlogin:FindManyLogins db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// DelOneLogin 删除一个用户登陆记录
+func DelOneLogin(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("userloginsetup:DelOneSetup db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
diff --git a/pub/models/usermod/def.go b/pub/models/usermod/def.go
new file mode 100644
index 0000000..7fcee9e
--- /dev/null
+++ b/pub/models/usermod/def.go
@@ -0,0 +1,18 @@
+package usermod
+
+import (
+ "videoweb/pub/pubdef"
+)
+
+func (user *User) GetID() int64 {
+ return user.Id
+}
+
+func (user *User) GetStatus() pubdef.UserStatus {
+ return user.Status
+}
+
+// CheckCoinEnough 检查金币是否足够扣除
+func (user *User) CheckCoinEnough(need int64) bool {
+ return user.Recharge >= need
+}
diff --git a/pub/models/usermod/tag.go b/pub/models/usermod/tag.go
new file mode 100755
index 0000000..59a3862
--- /dev/null
+++ b/pub/models/usermod/tag.go
@@ -0,0 +1,99 @@
+package usermod
+
+type TagUser struct {
+ Id string // ID
+ Status string // 状态
+ Region string // 地区
+ RegIp string // 注册IP
+ LastIp string // 上次登陆IP
+ Backgroud string // 背景图
+ Avatar string // 头像
+ NickName string // 昵称
+ Introduction string // 简介
+ Gender string // 性别 0-未知,1-男,2-女
+ LoginType string // 登陆方式
+ Age string // 年龄
+ LastLoginAt string // 上次登陆时间
+ InviteCode string // 邀请码
+ DevID string // 设备唯一标示
+ Mac string // 设备唯一mac地址
+ DevV2 string // 设备唯一设备ID
+ Device string // 设备
+ SysType string // 系统类型 ios android
+ AppVer string // app版本
+ Invites string // 我邀请的人的个数(我邀请他人)
+ AgentInviteCode string // 被邀请的inviteCode
+ Password string // 登陆密码
+ Recharge string // 当前充值余额,金币个数
+ RechargeTotal string // 累计充值额,金币个数
+ ConsumptionTotal string // 金币总消费数量
+ ModifyUserNameAt string // 上一次修改用户时间
+ Auth string // 是否认证
+ ConsecutiveLogin string // 连续登陆天数
+ DistrictCode string // 商区码
+ IsRegister string // 是否已注册
+ Score string // 积分
+ ScoreTotal string // 累计积分
+ ScoreLvName string // 积分等级名称
+ ScoreInvites string // 邀请人获得的积分累计
+ RegisterTime string //
+ CreatedAt string // 创建时间
+ UpdatedAt string // 更新时间
+ CurrentVipExpire string // 当前vip过期时间
+ SysMsgAt string // 获取系统消息时间
+ OriVer string // 注册时的app版本
+ AccountNo string // 账号
+}
+
+var FN struct {
+ User TagUser
+}
+var TN struct {
+ User string
+}
+
+func init() {
+ FN.User.Id = "_id"
+ FN.User.Status = "status"
+ FN.User.Region = "region"
+ FN.User.RegIp = "regIp"
+ FN.User.LastIp = "lastIp"
+ FN.User.Backgroud = "backgroud"
+ FN.User.Avatar = "avatar"
+ FN.User.NickName = "nickName"
+ FN.User.Introduction = "introduction"
+ FN.User.Gender = "gender"
+ FN.User.LoginType = "loginType"
+ FN.User.Age = "age"
+ FN.User.LastLoginAt = "lastLoginAt"
+ FN.User.InviteCode = "inviteCode"
+ FN.User.DevID = "devID"
+ FN.User.Mac = "mac"
+ FN.User.DevV2 = "devV2"
+ FN.User.Device = "device"
+ FN.User.SysType = "sysType"
+ FN.User.AppVer = "appVer"
+ FN.User.Invites = "invites"
+ FN.User.AgentInviteCode = "agentInviteCode"
+ FN.User.Password = "password"
+ FN.User.Recharge = "recharge"
+ FN.User.RechargeTotal = "rechargeTotal"
+ FN.User.ConsumptionTotal = "consumptionTotal"
+ FN.User.ModifyUserNameAt = "modifyUserNameAt"
+ FN.User.Auth = "auth"
+ FN.User.ConsecutiveLogin = "consecutiveLogin"
+ FN.User.DistrictCode = "districtCode"
+ FN.User.IsRegister = "isRegister"
+ FN.User.Score = "score"
+ FN.User.ScoreTotal = "scoreTotal"
+ FN.User.ScoreLvName = "scoreLvName"
+ FN.User.ScoreInvites = "scoreInvites"
+ FN.User.RegisterTime = "registerTime"
+ FN.User.CreatedAt = "createdAt"
+ FN.User.UpdatedAt = "updatedAt"
+ FN.User.CurrentVipExpire = "currentVipExpire"
+ FN.User.SysMsgAt = "sysMsgAt"
+ FN.User.OriVer = "oriVer"
+ FN.User.AccountNo = "accountNo"
+ TN.User = "user"
+}
diff --git a/pub/models/usermod/type.go b/pub/models/usermod/type.go
new file mode 100644
index 0000000..2b858a3
--- /dev/null
+++ b/pub/models/usermod/type.go
@@ -0,0 +1,82 @@
+//go:generate gen at -t table -s User
+//go:generate gen bt
+//go:generate gen mm
+//go:generate gen g update
+package usermod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// NormalizeDate 将时间归一化为当天北京时间零点
+func NormalizeDate(t time.Time) time.Time {
+ loc, _ := time.LoadLocation("Asia/Shanghai")
+ t = t.In(loc)
+ return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
+}
+
+type TagStat struct {
+ //标签名字
+ Tag string `json:"tag" bson:"tag"`
+ Count int64 `json:"count" bson:"count"`
+}
+
+type TagStatList []*TagStat
+
+// @table:user
+//
+//go:generate fox mongo --tag
+type User struct {
+ Id int64 `json:"id" bson:"_id"` // ID
+ AccountNo string `json:"accountNo" bson:"accountNo"` // 账号
+ Status pubdef.UserStatus `json:"status" bson:"status"` // 状态
+ Region string `json:"region" bson:"region"` // 地区
+ RegIp string `json:"regIp" bson:"regIp"` // 注册IP
+ LastIp string `json:"lastIp" bson:"lastIp"` // 上次登陆IP
+ Backgroud string `json:"backgroud" bson:"backgroud"` // 背景图
+ Avatar string `json:"avatar" bson:"avatar"` // 头像
+ NickName string `json:"nickName" bson:"nickName"` // 昵称
+ Introduction string `json:"introduction" bson:"introduction"` // 简介
+ Gender int64 `json:"gender" bson:"gender"` // 性别 0-未知,1-男,2-女
+ LoginType pubdef.LoginType `json:"loginType" bson:"loginType"` // 登陆方式
+ Age int64 `json:"age" bson:"age"` // 年龄
+ LastLoginAt time.Time `json:"lastLoginAt" bson:"lastLoginAt"` // 上次登陆时间
+ InviteCode string `json:"inviteCode" bson:"inviteCode"` // 邀请码
+ DevID string `json:"devID" bson:"devID"` // 设备唯一标示
+ Mac string `json:"mac" bson:"mac"` // 设备唯一mac地址
+ DevV2 string `json:"devV2" bson:"devV2"` // 设备唯一设备ID
+ Device string `json:"device" bson:"device"` // 设备
+ SysType string `json:"sysType" bson:"sysType"` // 系统类型 ios android
+ AppVer string `json:"appVer" bson:"appVer"` // app版本
+ Invites int64 `json:"invites" bson:"invites"` // 我邀请的人的个数(我邀请他人)
+ AgentInviteCode string `json:"agentInviteCode" bson:"agentInviteCode"` // 被邀请的inviteCode
+ Password string `json:"password" bson:"password"` // 登陆密码
+ Recharge int64 `json:"recharge" bson:"recharge"` // 当前充值余额,金币个数
+ RechargeTotal int64 `json:"rechargeTotal" bson:"rechargeTotal"` // 累计充值额,金币个数
+ ConsumptionTotal int64 `json:"consumptionTotal" bson:"consumptionTotal"` // 金币总消费数量
+ ModifyUserNameAt time.Time `json:"modifyUserNameAt" bson:"modifyUserNameAt"` // 上一次修改用户时间
+ Auth bool `json:"auth" bson:"auth"` // 是否认证
+ ConsecutiveLogin int64 `json:"consecutiveLogin" bson:"consecutiveLogin"` // 连续登陆天数
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 商区码
+ IsRegister bool `json:"isRegister" bson:"isRegister"` // 是否已注册
+ Score int64 `json:"score" bson:"score"` // 积分
+ ScoreTotal int64 `json:"scoreTotal" bson:"scoreTotal"` // 累计积分
+ ScoreLvName string `json:"scoreLvName" bson:"scoreLvName"` // 积分等级名称
+ ScoreInvites int64 `json:"scoreInvites" bson:"scoreInvites"` // 邀请人获得的积分累计
+ RegisterTime time.Time `json:"registerTime" bson:"registerTime"` //
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CurrentVipExpire time.Time `json:"currentVipExpire" bson:"currentVipExpire"` // 当前vip过期时间
+ SysMsgAt time.Time `json:"sysMsgAt" bson:"sysMsgAt"` // 获取系统消息时间
+ OriVer string `json:"oriVer" bson:"oriVer"` // 注册时的app版本
+
+}
+
+type UserList []*User
+type UserMap map[int64]*User
+
+// CheckCoinNumber 检查金币数量是否足够
+func (user *User) CheckCoinNumber(needDeduct int64) bool {
+ return user.Recharge >= needDeduct
+}
diff --git a/pub/models/usermod/type_update.go b/pub/models/usermod/type_update.go
new file mode 100644
index 0000000..aa2edb8
--- /dev/null
+++ b/pub/models/usermod/type_update.go
@@ -0,0 +1,55 @@
+// Code generated by gen g update. DO NOT EDIT.
+package usermod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+type UserUpdate struct {
+ Status *pubdef.UserStatus `json:"status" bson:"status,omitempty"` // 状态
+ Region *string `json:"region" bson:"region,omitempty"` // 地区
+ RegIp *string `json:"regIp" bson:"regIp,omitempty"` // 注册IP
+ LastIp *string `json:"lastIp" bson:"lastIp,omitempty"` // 上次登陆IP
+ Backgroud *string `json:"backgroud" bson:"backgroud,omitempty"` // 背景图
+ Avatar *string `json:"avatar" bson:"avatar,omitempty"` // 头像
+ NickName *string `json:"nickName" bson:"nickName,omitempty"` // 昵称
+ Introduction *string `json:"introduction" bson:"introduction,omitempty"` // 简介
+ Gender *int64 `json:"gender" bson:"gender,omitempty"` // 性别 0-未知,1-男,2-女
+ LoginType *pubdef.LoginType `json:"loginType" bson:"loginType,omitempty"` // 登陆方式
+ Age *int64 `json:"age" bson:"age,omitempty"` // 年龄
+ LastLoginAt *time.Time `json:"lastLoginAt" bson:"lastLoginAt,omitempty"` // 上次登陆时间
+ InviteCode *string `json:"inviteCode" bson:"inviteCode,omitempty"` // 邀请码
+ DevID *string `json:"devID" bson:"devID,omitempty"` // 设备唯一标示
+ Mac *string `json:"mac" bson:"mac,omitempty"` // 设备唯一mac地址
+ DevV2 *string `json:"devV2" bson:"devV2,omitempty"` // 设备唯一设备ID
+ Device *string `json:"device" bson:"device,omitempty"` // 设备
+ SysType *string `json:"sysType" bson:"sysType,omitempty"` // 系统类型 ios android
+ AppVer *string `json:"appVer" bson:"appVer,omitempty"` // app版本
+ Invites *int64 `json:"invites" bson:"invites,omitempty"` // 我邀请的人的个数(我邀请他人)
+ AgentInviteCode *string `json:"agentInviteCode" bson:"agentInviteCode,omitempty"` // 被邀请的inviteCode
+ Password *string `json:"password" bson:"password,omitempty"` // 登陆密码
+ Recharge *int64 `json:"recharge" bson:"recharge,omitempty"` // 当前充值余额,金币个数
+ RechargeTotal *int64 `json:"rechargeTotal" bson:"rechargeTotal,omitempty"` // 累计充值额,金币个数
+ ConsumptionTotal *int64 `json:"consumptionTotal" bson:"consumptionTotal,omitempty"` // 金币总消费数量
+ ModifyUserNameAt *time.Time `json:"modifyUserNameAt" bson:"modifyUserNameAt,omitempty"` // 上一次修改用户时间
+ Auth *bool `json:"auth" bson:"auth,omitempty"` // 是否认证
+ ConsecutiveLogin *int64 `json:"consecutiveLogin" bson:"consecutiveLogin,omitempty"` // 连续登陆天数
+ DistrictCode *string `json:"districtCode" bson:"districtCode,omitempty"` // 商区码
+ IsRegister *bool `json:"isRegister" bson:"isRegister,omitempty"` // 是否已注册
+ Score *int64 `json:"score" bson:"score,omitempty"` // 积分
+ ScoreTotal *int64 `json:"scoreTotal" bson:"scoreTotal,omitempty"` // 累计积分
+ ScoreLvName *string `json:"scoreLvName" bson:"scoreLvName,omitempty"` // 积分等级名称
+ ScoreInvites *int64 `json:"scoreInvites" bson:"scoreInvites,omitempty"` // 邀请人获得的积分累计
+ RegisterTime *time.Time `json:"registerTime" bson:"registerTime,omitempty"` //
+ UpdatedAt *time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 更新时间
+ CurrentVipExpire *time.Time `json:"currentVipExpire" bson:"currentVipExpire,omitempty"` // 当前vip过期时间
+ SysMsgAt *time.Time `json:"sysMsgAt" bson:"sysMsgAt,omitempty"` // 获取系统消息时间
+ ShowCardId *int64 `json:"showCardId" bson:"showCardId,omitempty"` // 最新会员卡id
+ ShowCardName *string `json:"showCardName" bson:"showCardName,omitempty"` // 最新会员卡名称
+ ShowCardExpiredAt *time.Time `json:"showCardExpiredAt" bson:"showCardExpiredAt,omitempty"` // 最新会员卡过期时间
+ OriVer *string `json:"oriVer" bson:"oriVer,omitempty"` // 注册时的app版本
+ AccountNo *string `json:"accountNo" bson:"accountNo,omitempty"` // 账号
+ Email *string `json:"email" bson:"email,omitempty"` // 邮箱
+ MerchantExpiredAt *time.Time `json:"merchantExpiredAt" bson:"merchantExpiredAt,omitempty"` // 商家/兼职有效期
+}
diff --git a/pub/models/usermod/user.go b/pub/models/usermod/user.go
new file mode 100644
index 0000000..50bb6df
--- /dev/null
+++ b/pub/models/usermod/user.go
@@ -0,0 +1,350 @@
+package usermod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ db = mongos.Shop.Coll(TN.User)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.User.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.AgentInviteCode, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.InviteCode, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.CreatedAt, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.DevID, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.Mac, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.DevV2, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.Recharge, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.DistrictCode, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.NickName, Value: "text"},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(User:initUserIndex) Create User Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneUser 插入一个用户
+func InsertOneUser(ctx context.Context, user *User) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, user); err != nil {
+ log.Warn("User:InsertOneUser db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneUser 查找一个用户
+func FindOneUser(ctx context.Context, filter bson.M) (*User, *srvdef.SrvErr) {
+ var userInfo User
+ if err := db.FindOneEx(ctx, &userInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("User:FindOneUser db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ return &userInfo, nil
+}
+
+// DelOneUser 删除一个用户
+func DelOneUser(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("User:DelOneUser db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneUser 更新一个用户
+func UpdateOneUser(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("user:UpdateOneUser db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ return nil
+}
+
+// CountUsers 统计用户数量
+func CountUsers(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("User:CountUsers db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateManyUser 更新满足条件的用户
+func UpdateManyUser(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("user:UpdateManyUser db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManyUser 查找一批用户数据
+func FindManyUser(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (UserList, *srvdef.SrvErr) {
+ data := make(UserList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("User:FindManyUser db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManyUserWithBind 用自定义结构查找一批用户数据
+func FindManyUserWithBind(ctx context.Context, bind interface{}, filter bson.M, opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("User:FindManyUserWithBind db.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateUsers 聚合查找一批用户信息
+func AggregateUsers(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline, opts...); err != nil {
+ log.Warn("User:AggregateUsers db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregate 聚合查找
+func ObjectAggregate(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("User:ObjectAggregate db.ObjectAggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// UpdateRecharge 更新用户充值金额(正数为充值,负数为消费)
+func UpdateRecharge(ctx context.Context, userId, recharge, rechargeTotal int64) (*User, *srvdef.SrvErr) {
+ userInfo := &User{}
+ incDoc := bson.M{
+ FN.User.Recharge: recharge,
+ FN.User.RechargeTotal: rechargeTotal,
+ }
+ if recharge < 0 {
+ incDoc[FN.User.ConsumptionTotal] = -recharge
+ }
+ err := db.FindOneAndUpdate(ctx, &userInfo, bson.M{
+ FN.User.Id: userId,
+ FN.User.Recharge: bson.M{"$gte": -recharge},
+ }, bson.M{
+ "$inc": incDoc,
+ "$set": bson.M{FN.User.UpdatedAt: time.Now()},
+ })
+ if err != nil {
+ log.Error("UpdateRecharge:FindOneAndUpdate failed", log.E(err))
+ return nil, srvdef.New(srvdef.DBUpdate, "数据更新出错", "")
+ }
+ if userInfo.Id <= 0 {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ return userInfo, nil
+}
+
+// UpdateScore 更新用户积分(正数加分,负数扣分)
+func UpdateScore(ctx context.Context, userId, score int64) (*User, *srvdef.SrvErr) {
+ userInfo := &User{}
+ incDoc := bson.M{FN.User.Score: score}
+ filter := bson.M{FN.User.Id: userId}
+ if score > 0 {
+ incDoc[FN.User.ScoreTotal] = score
+ } else {
+ filter[FN.User.Score] = bson.M{"$gte": -score}
+ }
+ err := db.FindOneAndUpdate(ctx, &userInfo, filter, bson.M{
+ "$inc": incDoc,
+ "$set": bson.M{FN.User.UpdatedAt: time.Now()},
+ })
+ if err != nil {
+ log.Warn("usermod:UpdateScore FindOneAndUpdate failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.DBUpdate, "数据更新出错", "")
+ }
+ if userInfo.Id <= 0 {
+ return nil, srvdef.New(srvdef.DBUpdate, "为0", "")
+ }
+ return userInfo, nil
+}
+
+type OnAddScore func(userId int64, add int64)
+
+// UpdateScoreWithEvent 更新用户积分并触发回调
+func UpdateScoreWithEvent(ctx context.Context, userId, score int64, onAdd OnAddScore) (*User, *srvdef.SrvErr) {
+ userInfo := &User{}
+ incDoc := bson.M{FN.User.Score: score}
+ filter := bson.M{FN.User.Id: userId}
+ if score > 0 {
+ incDoc[FN.User.ScoreTotal] = score
+ }
+ err := db.FindOneAndUpdate(ctx, &userInfo, filter, bson.M{
+ "$inc": incDoc,
+ "$set": bson.M{FN.User.UpdatedAt: time.Now()},
+ })
+ if err != nil {
+ log.Warn("usermod:UpdateScoreWithEvent FindOneAndUpdate failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.DBUpdate, "数据更新出错", "")
+ }
+ if userInfo.Id <= 0 {
+ return nil, srvdef.New(srvdef.DBUpdate, "为0", "")
+ }
+ if score > 0 && onAdd != nil {
+ onAdd(userInfo.Id, score)
+ }
+ return userInfo, nil
+}
+
+// UserPay 扣除用户充值金币
+func UserPay(ctx context.Context, userId int64, amount int64) (*User, *srvdef.SrvErr) {
+ if amount <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "扣除金额不能小于0", "扣除金额不能小于0")
+ }
+ userInfo := &User{}
+ err := db.FindOneAndUpdate(ctx, &userInfo, bson.M{
+ FN.User.Id: userId,
+ FN.User.Recharge: bson.M{"$gte": amount},
+ }, bson.M{
+ "$inc": bson.M{
+ FN.User.Recharge: -amount,
+ FN.User.ConsumptionTotal: amount,
+ },
+ "$set": bson.M{FN.User.UpdatedAt: time.Now()},
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.DBUpdate, "数据更新出错", "")
+ }
+ if userInfo.Id <= 0 {
+ return nil, srvdef.New(srvdef.DBUpdate, "余额不足或用户不存在", "")
+ }
+ return userInfo, nil
+}
+
+// FindOneUpdateAndReturnNew 更新用户数据并返回新的用户数据
+func FindOneUpdateAndReturnNew(ctx context.Context, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) (*User, *srvdef.SrvErr) {
+ userInfo := &User{}
+ err := db.FindOneAndUpdate(ctx, &userInfo, query, update)
+ if err != nil {
+ return nil, srvdef.New(srvdef.DBUpdate, "数据更新出错", "")
+ }
+ if userInfo.Id <= 0 {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ return userInfo, nil
+}
+
+// GetMapUsers 获取 map 结构的 users
+func GetMapUsers(ctx context.Context, ids []int64) (UserMap, *srvdef.SrvErr) {
+ mapUsers := make(UserMap)
+ if len(ids) == 0 {
+ return mapUsers, nil
+ }
+ listUsers, serr := FindManyUser(ctx, bson.M{FN.User.Id: bson.M{"$in": ids}})
+ if serr != nil {
+ return mapUsers, serr
+ }
+ for _, user := range listUsers {
+ mapUsers[user.Id] = user
+ }
+ return mapUsers, nil
+}
+
+// FindManyUserByQuery 从 user 表中查找一批数据(自定义 bind)
+func FindManyUserByQuery(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, query, opts...); err != nil {
+ log.Warn("user:FindManyUserByQuery db.Find failed",
+ log.E(err), log.Any("filter", query))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// InsertManyUser 插入一批用户
+func InsertManyUser(ctx context.Context, users *UserList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, users); err != nil {
+ log.Warn("User:InsertManyUser db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// StatcenterSyncList 按创建时间增量拉取用户列表(数据统计同步用)
+func StatcenterSyncList(ctx context.Context, createAt time.Time, size int64) (UserList, error) {
+ opt := (&options.FindOptions{})
+ opt.SetLimit(size)
+ opt.SetSort(bson.M{FN.User.Id: 1})
+ userList := UserList{}
+ err := db.Find(ctx, &userList, bson.M{FN.User.CreatedAt: bson.M{"$gt": createAt}}, opt)
+ if err != nil {
+ return nil, err
+ }
+ return userList, nil
+}
diff --git a/pub/models/usermod/user_gen.go b/pub/models/usermod/user_gen.go
new file mode 100644
index 0000000..f10b001
--- /dev/null
+++ b/pub/models/usermod/user_gen.go
@@ -0,0 +1,324 @@
+// Code generated by gen mm. DO NOT EDIT.
+package usermod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+/*
+
+var (
+ collUser *mongos.Collection
+)
+
+type getIndexeser interface {
+ GetIndexes() []mongo.IndexModel
+}
+
+func Init(ctx context.Context) {
+ collUser = mongos.Shop.Coll(TN.User)
+
+
+ // create indexes for collection 'user's
+ if v, ok := interface{}(&User{}).(getIndexeser); ok {
+ if indexes := v.GetIndexes(); len(indexes) > 0 {
+ if _, err := collUser.CreateIndex(ctx, indexes); err != nil {
+ log.Fatal("collUser.CreateIndex failed", log.E(err))
+ return
+ }
+ }
+ }
+}
+*/
+/*
+// InsertOneUser 创建一个User
+func InsertOneUser(ctx context.Context, data *User) *srvdef.SrvErr {
+ _, err := db.InsertOne(ctx, data)
+ if err != nil {
+ log.Error("usermod:InsertOneUser db.InsertOne failed",
+ log.Any("doc", data),
+ log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+*/
+
+// InsertManyUsers 创建多个User
+func InsertManyUsers(ctx context.Context, data []*User, opts ...*options.InsertManyOptions) *srvdef.SrvErr {
+ _, err := db.InsertMany(ctx, &data)
+ if err != nil {
+ log.Error("usermod:InsertManyUsers db.InsertMany failed",
+ log.Any("doc", data),
+ log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+/*
+// FindOneUser 查找一个User
+func FindOneUser(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *User, *srvdef.SrvErr) {
+ var data *User
+ if err := db.FindOneEx(ctx, &data, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("usermod:FindOneUser db.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return data, nil
+}
+*/
+// FindOneUserById 根据Id查找一个User
+func FindOneUserById(ctx context.Context, id int64, opts ...*options.FindOneOptions) (
+ *User, *srvdef.SrvErr) {
+ filter := bson.M{
+ FN.User.Id: id,
+ }
+ var data *User
+ if err := db.FindOneEx(ctx, &data, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("usermod:FindOneUser db.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return data, nil
+}
+
+// FindOneUserWithBind 用自定义结构查找一个User
+func FindOneUserWithBind(ctx context.Context, bind interface{}, filter bson.M, opts ...*options.FindOneOptions) *srvdef.SrvErr {
+ if err := db.FindOneEx(ctx, bind, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("usermod:FindOneUserWithBind db.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// FindOneUserAndUpdate 更新查找一个User
+func FindOneUserAndUpdate(ctx context.Context, filter bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) (
+ *User, *srvdef.SrvErr) {
+ var data User
+ err := db.FindOneAndUpdate(ctx, &data, filter, update, opts...)
+ if err != nil {
+ log.Warn("usermod:FindOneUserAndUpdate db.FindOneAndUpdate failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return &data, nil
+}
+
+/*
+
+// DelOneUser 删除一个User
+func DelOneUser(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("usermod:DelOneUser db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+*/
+
+// DelOneUserById 根据Id删除一个User
+func DelOneUserById(ctx context.Context, id int64, opts ...*options.FindOneOptions) (
+ int64, *srvdef.SrvErr) {
+ filter := bson.M{
+ FN.User.Id: id,
+ }
+ return DelOneUser(ctx, filter)
+}
+
+// DelManyUsers 删除多个User
+func DelManyUsers(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("usermod:DelManyUsers} db.DeleteMany failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+} /*
+// UpdateOneUser 更新一个User
+func UpdateOneUser(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("usermod:UpdateOneUser db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}*/
+// MustUpdateOneUser 更新一个User
+func MustUpdateOneUser(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("usermod:UpdateOneUser db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateOneUserWithResult 更新一个User
+func UpdateOneUserWithResult(ctx context.Context, filter bson.M, update bson.M, opts ...*options.UpdateOptions) (*mongo.UpdateResult, *srvdef.SrvErr) {
+ result, err := db.UpdateOne(ctx, filter, update, opts...)
+ if err != nil {
+ log.Warn("usermod:UpdateOneUserWithResult db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return result, nil
+}
+
+// UpdateManyUsers 更新多个User
+func UpdateManyUsers(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("usermod:UpdateManyUsers db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("usermod:UpdateManyUsers db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ }
+ return nil
+}
+
+/*
+
+// CountUsers 统计User数量
+func CountUsers(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("usermod:CountUsers db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+*/
+
+// FindManyUsers 查找一批User
+func FindManyUsers(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) ([]*User, *srvdef.SrvErr) {
+ data := make([]*User, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("usermod:FindManyUsers db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManyUsers 查找一批User
+func FindManyUsersAsInt64Map(ctx context.Context, filter bson.M, keyFn func(*User) int64, opts ...*options.FindOptions) (map[int64]*User, *srvdef.SrvErr) {
+ data := make([]*User, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("usermod:FindManyUsers db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ result := make(map[int64]*User)
+ for _, item := range data {
+ key := keyFn(item)
+ if _, ok := result[key]; !ok {
+ result[key] = item
+ }
+ }
+ return result, nil
+}
+
+// FindManyUsersWithBind 用自己定义结构查找一批User数据
+func FindManyUsersWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("usermod:FindManyUsersWithBind db.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+/*
+
+// AggregateUsers 聚合查找一批User
+func AggregateUsers(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("usermod:AggregateUsers db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+*/
+
+// ObjectAggregateUsers 聚合查找一批User数据
+func ObjectAggregateUsers(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("usermod:ObjectAggregateUsers db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// BulkWriteUsers 批量写入或者更新User数据
+func BulkWriteUsers(ctx context.Context,
+ docs []mongo.WriteModel, opts ...*options.BulkWriteOptions) *srvdef.SrvErr {
+ _, err := db.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// UpsertOneUser UpsertOneUser
+func UpsertOneUser(ctx context.Context, filter bson.M, update bson.M) (*mongo.UpdateResult, *srvdef.SrvErr) {
+ result, err := db.UpsertOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("usermod:UpsertOneUser db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result, nil
+}
+
+// DistinctUser 查找指定字段的不同值
+func DistinctUser(ctx context.Context, filter bson.M, fieldName string) ([]interface{}, *srvdef.SrvErr) {
+ result, err := db.Distince(ctx, filter, fieldName)
+ if err != nil {
+ log.Warn("usermod:DistinctUser db.Distince failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return result, nil
+}
diff --git a/pub/models/utils/dbhelper.go b/pub/models/utils/dbhelper.go
new file mode 100644
index 0000000..d4b585b
--- /dev/null
+++ b/pub/models/utils/dbhelper.go
@@ -0,0 +1 @@
+package utils
diff --git a/pub/models/utils/dbop.go b/pub/models/utils/dbop.go
new file mode 100644
index 0000000..05c75d3
--- /dev/null
+++ b/pub/models/utils/dbop.go
@@ -0,0 +1,11 @@
+package utils
+
+import (
+ "context"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ //initIndex(ctx)
+ return
+}
diff --git a/pub/models/utils/tag.go b/pub/models/utils/tag.go
new file mode 100755
index 0000000..bee5413
--- /dev/null
+++ b/pub/models/utils/tag.go
@@ -0,0 +1,30 @@
+package utils
+
+type TagGenerateId struct {
+ BusinessId string
+ CreateAt string
+ Desc string
+ Id string
+ MaxId string
+ Step string
+ UpdateAt string
+}
+
+var FN struct {
+ GenerateId TagGenerateId
+}
+
+var TN struct {
+ GenerateId string
+}
+
+func init() {
+ FN.GenerateId.BusinessId = "businessId"
+ FN.GenerateId.CreateAt = "createAt"
+ FN.GenerateId.Desc = "desc"
+ FN.GenerateId.Id = "_id"
+ FN.GenerateId.MaxId = "maxId"
+ FN.GenerateId.Step = "step"
+ FN.GenerateId.UpdateAt = "updateAt"
+ TN.GenerateId = "generate_id"
+}
diff --git a/pub/models/utils/type.go b/pub/models/utils/type.go
new file mode 100644
index 0000000..394afbc
--- /dev/null
+++ b/pub/models/utils/type.go
@@ -0,0 +1,18 @@
+package utils
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// @table:generate_id
+type GenerateId struct {
+ Id primitive.ObjectID `bson:"_id"`
+ UpdateAt time.Time `bson:"updateAt"`
+ BusinessId string `bson:"businessId"`
+ MaxId int64 `bson:"maxId"`
+ Step int64 `bson:"step"`
+ Desc string `bson:"desc"`
+ CreateAt time.Time `bson:"createAt"`
+}
diff --git a/pub/models/videocategorymod/category.go b/pub/models/videocategorymod/category.go
new file mode 100644
index 0000000..f3bd22c
--- /dev/null
+++ b/pub/models/videocategorymod/category.go
@@ -0,0 +1,144 @@
+package videocategorymod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoCategory)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCategory.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{{Key: FN.VideoCategory.Name, Value: 1}},
+ Options: options.Index().
+ SetName("uniq_video_category_name").
+ SetUnique(true).
+ SetPartialFilterExpression(bson.M{FN.VideoCategory.Name: bson.M{"$type": "string", "$gt": ""}}),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCategory.Status, Value: 1},
+ {Key: FN.VideoCategory.Rank, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCategory.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoCategoryIndex) Create VideoCategory Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoCategory 插入一个渠道统计
+func InsertOneVideoCategory(ctx context.Context, qa *VideoCategory) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ log.Warn("VideoCategorymod:InsertOneVideoCategory duplicate key", log.E(err))
+ return srvdef.New(srvdef.ErrDbInputExist, "视频分类已存在")
+ }
+ log.Warn("VideoCategorymod:InsertOneVideoCategory db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoCategory 删除一个渠道统计
+func DelOneVideoCategory(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCategorymod:DelOneVideoCategory db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoCategory 查找一个渠道统计
+func FindOneVideoCategory(ctx context.Context, filter bson.M) (
+ *VideoCategory, *srvdef.SrvErr) {
+ var VideoCategory VideoCategory
+ if err := db.FindOneEx(ctx, &VideoCategory, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoCategorymod:FindOneVideoCategory db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoCategory, nil
+}
+
+func FindManyVideoCategorys(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoCategoryList, *srvdef.SrvErr) {
+ data := make(VideoCategoryList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoCategorys", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoCategorys 统计渠道统计数量
+func CountVideoCategorys(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoCategorys db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoCategory 更新一个渠道统计
+func UpdateOneVideoCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVideoCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/videocategorymod/tag.go b/pub/models/videocategorymod/tag.go
new file mode 100755
index 0000000..36aa5c2
--- /dev/null
+++ b/pub/models/videocategorymod/tag.go
@@ -0,0 +1,54 @@
+package videocategorymod
+
+type TagVideoCategory struct {
+ CreatedAt string
+ Desc string
+ Id string
+ DisplayPosition string
+ IndexNow string
+ Name string
+ Slug string
+ Rank string
+ SortType string
+ Tags string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Status string
+ Title string
+ UpdatedAt string
+ TranslationCategoryId string
+ DisplayMode string
+}
+
+var FN struct {
+ VideoCategory TagVideoCategory
+}
+
+var TN struct {
+ VideoCategory string
+}
+
+func init() {
+ FN.VideoCategory.CreatedAt = "createdAt"
+ FN.VideoCategory.Desc = "desc"
+ FN.VideoCategory.Id = "_id"
+ FN.VideoCategory.DisplayPosition = "displayPosition"
+ FN.VideoCategory.IndexNow = "indexNow"
+ FN.VideoCategory.Name = "name"
+ FN.VideoCategory.Slug = "slug"
+ FN.VideoCategory.Rank = "rank"
+ FN.VideoCategory.SortType = "sortType"
+ FN.VideoCategory.Tags = "tags"
+ FN.VideoCategory.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoCategory.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoCategory.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoCategory.Status = "status"
+ FN.VideoCategory.Title = "title"
+ FN.VideoCategory.UpdatedAt = "updatedAt"
+ FN.VideoCategory.TranslationCategoryId = "translationCategoryId"
+ FN.VideoCategory.DisplayMode = "displayMode"
+ TN.VideoCategory = "video_category"
+}
diff --git a/pub/models/videocategorymod/type.go b/pub/models/videocategorymod/type.go
new file mode 100644
index 0000000..3ff2116
--- /dev/null
+++ b/pub/models/videocategorymod/type.go
@@ -0,0 +1,28 @@
+package videocategorymod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// VideoCategory 视频分类集合
+// @table:video_category
+type VideoCategory struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ Slug string `json:"slug" bson:"slug"` // slug
+ Title string `json:"title" bson:"title"` // APP显示
+ Desc string `json:"desc" bson:"desc"` // 描述
+ Status bool `json:"status" bson:"status"` // 上架状态
+ DisplayPosition pubdef.CategoryDisplayPosition `json:"displayPosition" bson:"displayPosition"` // 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ SortType pubdef.CategorySortType `json:"sortType" bson:"sortType"` // 排序方式
+ Tags []int64 `json:"tags" bson:"tags"` // 关联视频标签
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ TranslationCategoryId int64 `json:"translationCategoryId" bson:"translationCategoryId"` // 导航分类
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ DisplayMode pubdef.DisplayMode `json:"displayMode" bson:"displayMode"` // 展示方式 0:默认 1:卡片层叠轮播 2:头像列表 3:图文列表 4:大封面列表
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoCategoryList []*VideoCategory
diff --git a/pub/models/videocollectionmediamod/tag.go b/pub/models/videocollectionmediamod/tag.go
new file mode 100755
index 0000000..4c3f916
--- /dev/null
+++ b/pub/models/videocollectionmediamod/tag.go
@@ -0,0 +1,30 @@
+package videocollectionmediamod
+
+type TagVideoCollectionMediaModel struct {
+ ID string
+ CollectionID string
+ VideoId string
+ Status string
+ Rank string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoCollectionMediaModel TagVideoCollectionMediaModel
+}
+
+var TN struct {
+ VideoCollectionMediaModel string
+}
+
+func init() {
+ FN.VideoCollectionMediaModel.ID = "_id"
+ FN.VideoCollectionMediaModel.CollectionID = "collectionId"
+ FN.VideoCollectionMediaModel.VideoId = "videoId"
+ FN.VideoCollectionMediaModel.Status = "status"
+ FN.VideoCollectionMediaModel.Rank = "rank"
+ FN.VideoCollectionMediaModel.CreatedAt = "createdAt"
+ FN.VideoCollectionMediaModel.UpdatedAt = "updatedAt"
+ TN.VideoCollectionMediaModel = "video_collection_media"
+}
\ No newline at end of file
diff --git a/pub/models/videocollectionmediamod/type.go b/pub/models/videocollectionmediamod/type.go
new file mode 100644
index 0000000..f68661e
--- /dev/null
+++ b/pub/models/videocollectionmediamod/type.go
@@ -0,0 +1,22 @@
+package videocollectionmediamod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// VideoCollectionMediaModel 视频合集资源
+// @table:video_collection_media
+type VideoCollectionMediaModel struct {
+ ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 唯一ID
+ CollectionID int64 `json:"collectionId" bson:"collectionId"` // 合集ID
+ VideoId int64 `json:"videoId" bson:"videoId"` // 视频ID
+ Status pubdef.InformationStatus `json:"status" bson:"status"` // 状态
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间
+
+}
+type VideoCollectionMediaModelList []*VideoCollectionMediaModel
diff --git a/pub/models/videocollectionmediamod/videocollection.go b/pub/models/videocollectionmediamod/videocollection.go
new file mode 100644
index 0000000..dcbfcb3
--- /dev/null
+++ b/pub/models/videocollectionmediamod/videocollection.go
@@ -0,0 +1,154 @@
+package videocollectionmediamod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoCollectionMediaModel)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CollectionID, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CollectionID, Value: 1},
+ {Key: FN.VideoCollectionMediaModel.Rank, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CollectionID, Value: 1},
+ {Key: FN.VideoCollectionMediaModel.VideoId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.VideoId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoCollectionMediaModelIndex) Create VideoCollectionMediaModel Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoCollectionMediaModel 插入一个渠道统计
+func InsertOneVideoCollectionMediaModel(ctx context.Context, qa *VideoCollectionMediaModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoCollectionMediaModelmod:InsertOneVideoCollectionMediaModel db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoCollectionMediaModel 删除一个渠道统计
+func DelOneVideoCollectionMediaModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCollectionMediaModelmod:DelOneVideoCollectionMediaModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+func DelManyVideoCollectionMediaModel(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCollectionMediaModelmod:DelManyVideoCollectionMediaModel db.DeleteMany failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoCollectionMediaModel 查找一个渠道统计
+func FindOneVideoCollectionMediaModel(ctx context.Context, filter bson.M) (
+ *VideoCollectionMediaModel, *srvdef.SrvErr) {
+ var VideoCollectionMediaModel VideoCollectionMediaModel
+ if err := db.FindOneEx(ctx, &VideoCollectionMediaModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoCollectionMediaModelmod:FindOneVideoCollectionMediaModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoCollectionMediaModel, nil
+}
+
+func FindManyVideoCollectionMediaModels(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoCollectionMediaModelList, *srvdef.SrvErr) {
+ data := make(VideoCollectionMediaModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoCollectionMediaModels", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoCollectionMediaModels 统计渠道统计数量
+func CountVideoCollectionMediaModels(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoCollectionMediaModels db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoCollectionMediaModel 更新一个渠道统计
+func UpdateOneVideoCollectionMediaModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoCollectionMediaModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoCollectionMediaModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func InsertManyVideoCollectionMedia(ctx context.Context, list []*VideoCollectionMediaModel) *srvdef.SrvErr {
+ _, err := db.InsertMany(ctx, &list)
+ if err != nil {
+ log.Warn("post:InsertManyVideoCollectionMediaModel db.InsertMany failed",
+ log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/videocreatermod/creater.go b/pub/models/videocreatermod/creater.go
new file mode 100644
index 0000000..957b472
--- /dev/null
+++ b/pub/models/videocreatermod/creater.go
@@ -0,0 +1,126 @@
+package videocreatermod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoCreaterInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCreaterInfo.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCreaterInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoCreaterInfoIndex) Create VideoCreaterInfo Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoCreaterInfo 插入一个渠道统计
+func InsertOneVideoCreaterInfo(ctx context.Context, qa *VideoCreaterInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ log.Warn("VideoCreaterInfomod:InsertOneVideoCreaterInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoCreaterInfo 删除一个渠道统计
+func DelOneVideoCreaterInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCreaterInfomod:DelOneVideoCreaterInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoCreaterInfo 查找一个渠道统计
+func FindOneVideoCreaterInfo(ctx context.Context, filter bson.M) (
+ *VideoCreaterInfo, *srvdef.SrvErr) {
+ var VideoCreaterInfo VideoCreaterInfo
+ if err := db.FindOneEx(ctx, &VideoCreaterInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoCreaterInfomod:FindOneVideoCreaterInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoCreaterInfo, nil
+}
+
+func FindManyVideoCreaterInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoCreaterInfoList, *srvdef.SrvErr) {
+ data := make(VideoCreaterInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoCreaterInfos", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoCreaterInfos 统计渠道统计数量
+func CountVideoCreaterInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoCreaterInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoCreaterInfo 更新一个渠道统计
+func UpdateOneVideoCreaterInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoCreaterInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoCreaterInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+func UpdateManyVideoCreaterInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoCreaterInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/videocreatermod/tag.go b/pub/models/videocreatermod/tag.go
new file mode 100755
index 0000000..6b59b81
--- /dev/null
+++ b/pub/models/videocreatermod/tag.go
@@ -0,0 +1,52 @@
+package videocreatermod
+
+type TagVideoCreaterInfo struct {
+ CreatedAt string
+ Id string
+ IndexNow string
+ Name string
+ Slug string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ UpdatedAt string
+ Works string
+ Image string
+ CreateType string
+ Desc string
+ Background string
+ MediaTypes string
+}
+
+var FN struct {
+ VideoCreaterInfo TagVideoCreaterInfo
+}
+
+var TN struct {
+ VideoCreaterInfo string
+}
+
+func init() {
+ FN.VideoCreaterInfo.CreatedAt = "createdAt"
+ FN.VideoCreaterInfo.Id = "_id"
+ FN.VideoCreaterInfo.IndexNow = "indexNow"
+ FN.VideoCreaterInfo.Name = "name"
+ FN.VideoCreaterInfo.Slug = "slug"
+ FN.VideoCreaterInfo.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoCreaterInfo.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoCreaterInfo.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoCreaterInfo.Sort = "sort"
+ FN.VideoCreaterInfo.Status = "status"
+ FN.VideoCreaterInfo.UpdatedAt = "updatedAt"
+ FN.VideoCreaterInfo.Works = "works"
+ FN.VideoCreaterInfo.Image = "image"
+ FN.VideoCreaterInfo.Background = "background"
+ FN.VideoCreaterInfo.CreateType = "createType"
+ FN.VideoCreaterInfo.Desc = "desc"
+ FN.VideoCreaterInfo.MediaTypes = "mediaTypes"
+ TN.VideoCreaterInfo = "video_creater"
+}
diff --git a/pub/models/videocreatermod/type.go b/pub/models/videocreatermod/type.go
new file mode 100644
index 0000000..d588bec
--- /dev/null
+++ b/pub/models/videocreatermod/type.go
@@ -0,0 +1,26 @@
+package videocreatermod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_creater
+type VideoCreaterInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Name string `json:"name" bson:"name"` // 帖子标题
+ Slug string `json:"slug" bson:"slug"` // slug
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Works int64 `json:"works" bson:"works"` // 作品数量
+ Image string `json:"image" bson:"image"` // 头像
+ CreateType int64 `json:"createType" bson:"createType"` // 创作者类型 0=普通 1=官方
+ MediaTypes []int64 `json:"mediaTypes" bson:"mediaTypes"` // 作者关联的资源类型 1视频
+ Background string `json:"background" bson:"background"` // 背景
+ Desc string `json:"desc" bson:"desc"` // 简介
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoCreaterInfoList []*VideoCreaterInfo
diff --git a/pub/models/videofavoritemod/favorite.go b/pub/models/videofavoritemod/favorite.go
new file mode 100644
index 0000000..d1ab1c1
--- /dev/null
+++ b/pub/models/videofavoritemod/favorite.go
@@ -0,0 +1,92 @@
+package videofavoritemod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 video_favorite 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.VideoFavorite)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: FN.VideoFavorite.UserID, Value: 1}, {Key: FN.VideoFavorite.VideoID, Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: FN.VideoFavorite.UserID, Value: 1}, {Key: FN.VideoFavorite.CreatedAt, Value: -1}, {Key: FN.VideoFavorite.Id, Value: -1}}},
+ {Keys: bson.D{{Key: FN.VideoFavorite.VideoID, Value: 1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(videofavoritemod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// UpsertOneVideoFavorite 按用户ID和视频ID写入收藏关系,返回是否新增收藏
+func UpsertOneVideoFavorite(ctx context.Context, userId, videoId int64, now time.Time) (bool, *srvdef.SrvErr) {
+ filter := bson.M{FN.VideoFavorite.UserID: userId, FN.VideoFavorite.VideoID: videoId}
+ update := bson.M{
+ "$set": bson.M{FN.VideoFavorite.UpdatedAt: now},
+ "$setOnInsert": bson.M{
+ FN.VideoFavorite.Id: mongos.Lid.VideoFavorite.Get(),
+ FN.VideoFavorite.UserID: userId,
+ FN.VideoFavorite.VideoID: videoId,
+ FN.VideoFavorite.CreatedAt: now,
+ },
+ }
+ result, err := coll.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
+ if err != nil {
+ log.Warn("videofavoritemod:UpsertOneVideoFavorite db.UpdateOne failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.UpsertedCount > 0, nil
+}
+
+// DeleteOneVideoFavorite 删除一条视频收藏关系,返回是否实际删除
+func DeleteOneVideoFavorite(ctx context.Context, filter bson.M) (bool, *srvdef.SrvErr) {
+ result, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("videofavoritemod:DeleteOneVideoFavorite db.DeleteOne failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return result > 0, nil
+}
+
+// FindManyVideoFavorite 分页查询视频收藏关系
+func FindManyVideoFavorite(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoFavoriteList, *srvdef.SrvErr) {
+ data := make(VideoFavoriteList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("videofavoritemod:FindManyVideoFavorite db.Find failed", log.Any("filter", filter), log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoFavorite 统计视频收藏关系数量
+func CountVideoFavorite(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter)
+ if err != nil {
+ log.Warn("videofavoritemod:CountVideoFavorite db.Count failed", log.Any("filter", filter), log.E(err))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// AggregateVideoFavorite 聚合视频收藏数据
+func AggregateVideoFavorite(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("videofavoritemod:AggregateVideoFavorite db.Aggregate failed", log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/videofavoritemod/tag.go b/pub/models/videofavoritemod/tag.go
new file mode 100644
index 0000000..8a5f7b6
--- /dev/null
+++ b/pub/models/videofavoritemod/tag.go
@@ -0,0 +1,28 @@
+package videofavoritemod
+
+// TagVideoFavorite 保存 video_favorite 集合字段名
+type TagVideoFavorite struct {
+ Id string
+ UserID string
+ VideoID string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoFavorite TagVideoFavorite
+}
+
+var TN struct {
+ VideoFavorite string
+}
+
+func init() {
+ FN.VideoFavorite.Id = "_id"
+ FN.VideoFavorite.UserID = "userId"
+ FN.VideoFavorite.VideoID = "videoId"
+ FN.VideoFavorite.CreatedAt = "createdAt"
+ FN.VideoFavorite.UpdatedAt = "updatedAt"
+
+ TN.VideoFavorite = "video_favorite"
+}
diff --git a/pub/models/videofavoritemod/type.go b/pub/models/videofavoritemod/type.go
new file mode 100644
index 0000000..91ae91d
--- /dev/null
+++ b/pub/models/videofavoritemod/type.go
@@ -0,0 +1,15 @@
+package videofavoritemod
+
+import "time"
+
+// VideoFavorite 视频收藏关系
+type VideoFavorite struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ UserID int64 `json:"userId" bson:"userId"`
+ VideoID int64 `json:"videoId" bson:"videoId"`
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
+}
+
+// VideoFavoriteList 视频收藏列表
+type VideoFavoriteList []*VideoFavorite
diff --git a/pub/models/videoinformationmod/category_rank.go b/pub/models/videoinformationmod/category_rank.go
new file mode 100644
index 0000000..88aa07c
--- /dev/null
+++ b/pub/models/videoinformationmod/category_rank.go
@@ -0,0 +1,53 @@
+package videoinformationmod
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+const CategoryRankField = "_categoryRank"
+
+// VideoCategoryRank 视频在指定分类下的排序
+type VideoCategoryRank struct {
+ Id int64 `json:"id" bson:"id"` // 分类ID
+ Rank int64 `json:"rank" bson:"rank"` // 排序值,越大越靠前
+}
+
+// CategoryRankAddFields 提取当前分类的排序值,未配置时为0
+func CategoryRankAddFields(categoryId int64) bson.M {
+ return bson.M{CategoryRankField: bson.M{"$let": bson.M{
+ "vars": bson.M{"hit": bson.M{"$first": bson.M{"$filter": bson.M{
+ "input": bson.M{"$ifNull": bson.A{"$categoryRanks", bson.A{}}},
+ "as": "b",
+ "cond": bson.M{"$eq": bson.A{"$$b.id", categoryId}},
+ }}}},
+ "in": bson.M{"$ifNull": bson.A{"$$hit.rank", 0}},
+ }}}
+}
+
+// CategoryRankSort 按当前分类排序后使用上架时间和ID稳定分页
+func CategoryRankSort() bson.D {
+ return bson.D{{Key: CategoryRankField, Value: -1}, {Key: FN.VideoInformation.AddedTime, Value: -1}, {Key: FN.VideoInformation.Id, Value: -1}}
+}
+
+// FindManyVideoByCategoryRank 按指定分类排序分页查询视频
+func FindManyVideoByCategoryRank(ctx context.Context, filter bson.M, categoryId, skip, limit int64, projection bson.M) (VideoInformationList, *srvdef.SrvErr) {
+ pipeline := []bson.M{
+ {"$match": filter},
+ {"$addFields": CategoryRankAddFields(categoryId)},
+ {"$sort": CategoryRankSort()},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$unset": CategoryRankField},
+ }
+ if len(projection) > 0 {
+ pipeline = append(pipeline, bson.M{"$project": projection})
+ }
+ list := make(VideoInformationList, 0)
+ if err := AggregateVideoInformations(ctx, &list, pipeline); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
diff --git a/pub/models/videoinformationmod/information.go b/pub/models/videoinformationmod/information.go
new file mode 100644
index 0000000..24bfb45
--- /dev/null
+++ b/pub/models/videoinformationmod/information.go
@@ -0,0 +1,448 @@
+package videoinformationmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoInformation)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.IndexNow, Value: 1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.UpdatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Video.FileId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: "nodes.nodeId", Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Id, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.HeatValue, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.IsRecommend, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.Real.WeekWatchs, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CategoryId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.IsRecommend, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.Real.WeekWatchs, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CategoryId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CategoryId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ {Key: FN.VideoInformation.Fake.WatchTimes, Value: -1},
+ {Key: FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.MediaType, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.MediaType, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.IsRecommend, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.Real.WeekWatchs, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.AddedTime, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.MonthWatchs, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.IsTop, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.VideoCount, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.Likes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.ActressIds, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.Likes, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CategoryId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.Likes, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ {Key: FN.VideoInformation.Fake.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.Likes, Value: -1},
+ {Key: FN.VideoInformation.Fake.Likes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.Real.WeekWatchs, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: 1},
+ {Key: FN.VideoInformation.Fake.WatchTimes, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.Likes, Value: 1},
+ {Key: FN.VideoInformation.Fake.Likes, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.HevcStatus, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ {Key: FN.VideoInformation.Fake.WatchTimes, Value: -1},
+ {Key: FN.VideoInformation.UpdatedAt, Value: -1},
+ {Key: FN.VideoInformation.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.HevcStatus, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.UpdatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoInformationIndex) Create VideoInformation Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneVideoInformation 插入一个渠道统计
+func InsertOneVideoInformation(ctx context.Context, qa *VideoInformation) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoInformationmod:InsertOneVideoInformation db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+func InsertManyVideoInformation(ctx context.Context, qa []*VideoInformation) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, &qa); err != nil {
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoInformation 删除一个渠道统计
+func DelOneVideoInformation(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoInformationmod:DelOneVideoInformation db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoInformation 查找一个渠道统计
+func FindOneVideoInformation(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *VideoInformation, *srvdef.SrvErr) {
+ var VideoInformation VideoInformation
+ if err := db.FindOneEx(ctx, &VideoInformation, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoInformationmod:FindOneVideoInformation db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoInformation, nil
+}
+
+func FindManyVideoInformations(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoInformationList, *srvdef.SrvErr) {
+ data := make(VideoInformationList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoInformations", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoInformations 统计渠道统计数量
+func CountVideoInformations(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoInformations db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoInformation 更新一个渠道统计
+func UpdateOneVideoInformation(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoInformation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoInformation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVideoInformation(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoInformation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func BulkWriteVideoInformation(ctx context.Context, docs []mongo.WriteModel,
+ opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, *srvdef.SrvErr) {
+ result, err := db.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ log.Warn("post:BulkWriteVideoInformation db.BulkWrite failed", log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result, nil
+}
+
+// AggregateVideoInformations 聚合查询视频信息
+func AggregateVideoInformations(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("VideoInformation:AggregateVideoInformations db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/videoinformationmod/tag.go b/pub/models/videoinformationmod/tag.go
new file mode 100755
index 0000000..a00562a
--- /dev/null
+++ b/pub/models/videoinformationmod/tag.go
@@ -0,0 +1,156 @@
+package videoinformationmod
+
+type TagVideoInformation struct {
+ ActressIds string
+ AddedTime string
+ CategoryId string
+ CategoryRanks string
+ CoverImg string
+ CreatedAt string
+ CreatorId string
+ CreatorName string
+ Desc string
+ FirstTag string
+ Id string
+ IndexNow string
+ IsHot string
+ IsTop string
+ IssuedAt string
+ Keyworld string
+ MediaType string
+ Nodes string
+ Fake struct {
+ Likes string
+ WatchTimes string
+ }
+ Real struct {
+ Comments string
+ Likes string
+ MonthWatchs string
+ ShowTimes string
+ TodayWatchs string
+ WatchTimes string
+ WeekWatchs string
+ YearWatchs string
+ WeekLikes string
+ MonthLikes string
+ YearLikes string
+ }
+ Remark string
+ SecondTags string
+ Sort string
+ HeatValue string
+ Status string
+ Tags string
+ Title string
+ Slug string
+ IsRecommend string
+ UpdatedAt string
+ PageUpdatedAt string
+ Video struct {
+ Cover string
+ FileId string
+ Height string
+ H265Url string
+ PreMp4FileNameFs string
+ TaskId string
+ Url string
+ VideoTime string
+ Width string
+ UnencryptedPreMp4 string
+ }
+ VideoCount string
+ IssuedBrand string
+ Bango string
+ RealUpdateAt string
+ UploadUserName string
+ OptionAccount string
+ UnencryptedImage string
+ SecondUpdatedAt string
+ SyncId string
+ HevcStatus string
+ HevcFailCount string
+ PreviewVtt string
+ AudioFile string
+ SmartCover string
+ HevcMetaInfo string
+}
+
+var FN struct {
+ VideoInformation TagVideoInformation
+}
+
+var TN struct {
+ VideoInformation string
+}
+
+func init() {
+ FN.VideoInformation.ActressIds = "actressIds"
+ FN.VideoInformation.AddedTime = "addedTime"
+ FN.VideoInformation.CategoryId = "categoryId"
+ FN.VideoInformation.CategoryRanks = "categoryRanks"
+ FN.VideoInformation.CoverImg = "coverImg"
+ FN.VideoInformation.CreatedAt = "createdAt"
+ FN.VideoInformation.CreatorId = "creatorId"
+ FN.VideoInformation.CreatorName = "creatorName"
+ FN.VideoInformation.Desc = "desc"
+ FN.VideoInformation.FirstTag = "firstTag"
+ FN.VideoInformation.Id = "_id"
+ FN.VideoInformation.IndexNow = "indexNow"
+ FN.VideoInformation.IsHot = "isHot"
+ FN.VideoInformation.IsTop = "isTop"
+ FN.VideoInformation.IssuedAt = "issuedAt"
+ FN.VideoInformation.Keyworld = "keyworld"
+ FN.VideoInformation.MediaType = "mediaType"
+ FN.VideoInformation.Nodes = "nodes"
+ FN.VideoInformation.Fake.Likes = "fake.likes"
+ FN.VideoInformation.Fake.WatchTimes = "fake.watchTimes"
+ FN.VideoInformation.Real.Comments = "real.comments"
+ FN.VideoInformation.Real.Likes = "real.likes"
+ FN.VideoInformation.Real.MonthWatchs = "real.monthWatchs"
+ FN.VideoInformation.Real.ShowTimes = "real.showTimes"
+ FN.VideoInformation.Real.TodayWatchs = "real.todayWatchs"
+ FN.VideoInformation.Real.WatchTimes = "real.watchTimes"
+ FN.VideoInformation.Real.WeekWatchs = "real.weekWatchs"
+ FN.VideoInformation.Real.YearWatchs = "real.yearWatchs"
+ FN.VideoInformation.Real.WeekLikes = "real.weekLikes"
+ FN.VideoInformation.Real.MonthLikes = "real.monthLikes"
+ FN.VideoInformation.Real.YearLikes = "real.yearLikes"
+ FN.VideoInformation.Remark = "remark"
+ FN.VideoInformation.SecondTags = "secondTags"
+ FN.VideoInformation.Sort = "sort"
+ FN.VideoInformation.HeatValue = "heatValue"
+ FN.VideoInformation.Status = "status"
+ FN.VideoInformation.Tags = "tags"
+ FN.VideoInformation.Title = "title"
+ FN.VideoInformation.Slug = "slug"
+ FN.VideoInformation.IsRecommend = "isRecommend"
+ FN.VideoInformation.UpdatedAt = "updatedAt"
+ FN.VideoInformation.PageUpdatedAt = "pageUpdatedAt"
+ FN.VideoInformation.Video.Cover = "video.cover"
+ FN.VideoInformation.Video.FileId = "video.fileId"
+ FN.VideoInformation.Video.Height = "video.height"
+ FN.VideoInformation.Video.H265Url = "video.h265Url"
+ FN.VideoInformation.Video.PreMp4FileNameFs = "video.preMp4FileNameFs"
+ FN.VideoInformation.Video.TaskId = "video.taskId"
+ FN.VideoInformation.Video.Url = "video.url"
+ FN.VideoInformation.Video.VideoTime = "video.videoTime"
+ FN.VideoInformation.Video.Width = "video.width"
+ FN.VideoInformation.Video.UnencryptedPreMp4 = "video.unencryptedPreMp4"
+ FN.VideoInformation.VideoCount = "videoCount"
+ FN.VideoInformation.IssuedBrand = "issuedBrand"
+ FN.VideoInformation.RealUpdateAt = "realUpdateAt"
+ FN.VideoInformation.Bango = "bango"
+ FN.VideoInformation.UploadUserName = "uploadUserName"
+ FN.VideoInformation.OptionAccount = "optionAccount"
+ FN.VideoInformation.UnencryptedImage = "unencryptedImage"
+ FN.VideoInformation.SyncId = "syncId"
+ FN.VideoInformation.SecondUpdatedAt = "secondUpdatedAt"
+ FN.VideoInformation.HevcStatus = "hevcStatus"
+ FN.VideoInformation.HevcFailCount = "hevcFailCount"
+ FN.VideoInformation.PreviewVtt = "previewVtt"
+ FN.VideoInformation.AudioFile = "audioFile"
+ FN.VideoInformation.SmartCover = "smartCover"
+ FN.VideoInformation.HevcMetaInfo = "hevcMetaInfo"
+ TN.VideoInformation = "video_information"
+}
diff --git a/pub/models/videoinformationmod/type.go b/pub/models/videoinformationmod/type.go
new file mode 100644
index 0000000..0075f53
--- /dev/null
+++ b/pub/models/videoinformationmod/type.go
@@ -0,0 +1,172 @@
+package videoinformationmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// VideoInformation 视频信息集合
+// @table:video_information
+type VideoInformation struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 标题
+ Slug string `json:"slug" bson:"slug"` // slug
+ Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+ MediaType MediaType `json:"mediaType" bson:"mediaType"` // 资源类型
+ AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 上架时间
+ ActressIds []int64 `json:"actressIds" bson:"actressIds"` // 女优ID
+ CreatorId int64 `json:"creatorId" bson:"creatorId"` // 作者Id
+ CreatorName string `json:"creatorName" bson:"creatorName"` // 作者名称
+ Keyworld []string `json:"keyworld,omitempty" bson:"keyworld"` // 关键字
+ CategoryId int64 `json:"categoryId,omitempty" bson:"categoryId"` // 分类ID
+ CategoryRanks []VideoCategoryRank `json:"categoryRanks" bson:"categoryRanks"` // 各分类下的排序
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Nodes []Node `json:"nodes" bson:"nodes"` // 节点
+ Status pubdef.InformationStatus `json:"status" bson:"status"` // 帖子上架状态
+ Video VideoInfo `json:"video" bson:"video"` // 视频
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
+ IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ HeatValue int64 `json:"heatValue" bson:"heatValue"` // 后台热度值
+ Real pubdef.Flow `json:"real" bson:"real"` // 真实流量
+ Fake pubdef.FakeFlow `json:"fake" bson:"fake"` // 伪造流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ IssuedAt time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
+ IssuedBrand string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ PageUpdatedAt time.Time `json:"pageUpdatedAt" bson:"pageUpdatedAt"` // 页面展示更新时间
+ RealUpdateAt time.Time `json:"realUpdateAt" bson:"realUpdateAt"` // 播放相关数据更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ IsTop bool `json:"isTop" bson:"isTop"` // 是否置顶
+ UploadUserName string `bson:"uploadUserName" json:"uploadUserName,omitempty"` // 上传用户名
+ OptionAccount string `bson:"optionAccount,omitempty" json:"optionAccount"` // 最后操作人
+ SyncId string `json:"syncId" bson:"syncId"` // 同步ID
+ SecondUpdatedAt time.Time `json:"secondUpdatedAt" bson:"secondUpdatedAt"` // 二次修改时间
+ HevcStatus HevcStatus `json:"hevcStatus" bson:"hevcStatus"` // 主视频 H265 转码状态
+ HevcFailCount int `json:"hevcFailCount" bson:"hevcFailCount"` // 主视频转码累计失败次数
+ PreviewVtt string `json:"previewVtt" bson:"previewVtt,omitempty"` // 雪碧图 vtt
+ AudioFile string `json:"audioFile" bson:"audioFile,omitempty"` // 音频文件路径
+ SmartCover string `json:"smartCover" bson:"smartCover,omitempty"` // 智能封面
+ HevcMetaInfo *VideoMetaInfo `json:"hevcMetaInfo" bson:"hevcMetaInfo,omitempty"` // H265 视频元信息
+}
+type VideoInformationList []*VideoInformation
+
+// HevcStatus H265 转码状态(主视频 video.url)。
+type HevcStatus int
+
+const (
+ HevcStatusNone HevcStatus = 0 // 未提交转码
+ HevcStatusPending HevcStatus = 1 // 转码中
+ HevcStatusSuccess HevcStatus = 2 // 转码成功
+ HevcStatusFailed HevcStatus = -1 // 转码失败
+)
+
+// VideoMetaInfo H265 转码视频元信息(对应老司机 hevc_meta_info)。
+type VideoMetaInfo struct {
+ M3u8URL string `json:"m3u8Url" bson:"m3u8Url,omitempty"`
+ Codec string `json:"codec" bson:"codec,omitempty"`
+ Profile string `json:"profile" bson:"profile,omitempty"`
+ Width int `json:"width" bson:"width,omitempty"`
+ Height int `json:"height" bson:"height,omitempty"`
+ Fps float64 `json:"fps" bson:"fps,omitempty"`
+ VideoBitrate int `json:"videoBitrate" bson:"videoBitrate,omitempty"`
+ PixFmt string `json:"pixFmt" bson:"pixFmt,omitempty"`
+ AudioCodec string `json:"audioCodec" bson:"audioCodec,omitempty"`
+ AudioBitrate int `json:"audioBitrate" bson:"audioBitrate,omitempty"`
+ AudioSampleRate int `json:"audioSampleRate" bson:"audioSampleRate,omitempty"`
+ AudioChannels int `json:"audioChannels" bson:"audioChannels,omitempty"`
+ Bitrate int `json:"bitrate" bson:"bitrate,omitempty"`
+ Size int64 `json:"size" bson:"size,omitempty"`
+ Duration float64 `json:"duration" bson:"duration,omitempty"`
+}
+
+type Node struct {
+ NodeId string `json:"nodeId" bson:"nodeId"` // 节点ID
+ Title string `bson:"title" json:"title"` // 节点标题
+ Videos []VideoInfo `json:"videos" bson:"videos"` // 视频信息列表
+ Images []string `json:"images" bson:"images"` // 图片
+ Url string `json:"url" bson:"url"` // 链接
+ Text string `json:"text" bson:"text"` // 文本
+}
+
+type VideoInfo struct {
+ TaskId string `json:"taskId" bson:"taskId"` // 视频ID(视频才会存在)
+ FileId string `json:"fileId" bson:"fileId"` // 媒资库ID
+ Url string `json:"url" bson:"url"` // H.264 m3u8 播放地址(始终存在)
+ H265Url string `json:"h265Url" bson:"h265Url"` // H.265 m3u8 播放地址,空字符串表示暂无该编码源
+ Cover string `json:"cover" bson:"cover"` // 视频封面
+ Height int64 `json:"height" bson:"height"` // 分辨率高(视频才会存在)
+ Width int64 `json:"width" bson:"width"` // 分辨率宽(视频才会存在)
+ VideoTime int32 `bson:"videoTime" json:"videoTime"` // 影片长度
+ PreMp4FileNameFs string `bson:"preMp4FileNameFs,omitempty" json:"preMp4FileNameFs"` // 预览MP4文件
+ UnencryptedPreMp4 string `bson:"unencryptedPreMp4,omitempty" json:"unencryptedPreMp4"` // 不加密的预览MP4
+}
+
+type VideoSortItem struct {
+ Key VideoSortType `json:"key" form:"key"` // 传递值
+ Value string `json:"value" form:"value"` // 显示名字
+}
+
+type MediaType int64
+
+const (
+ MediaType_Video MediaType = 0 // 视频
+ MediaType_Collection MediaType = 1 // 合集
+ MediaType_Advitise MediaType = 2 // 广告
+ MediaType_AV MediaType = 3 // AV视频
+)
+
+// VideoSortType 排序条件
+type VideoSortType int64
+
+const (
+ VideoSortType_Recommend VideoSortType = 1 // 最近最佳
+ VideoSortType_Hot VideoSortType = 2 // 最多观看
+ VideoSortType_New VideoSortType = 3 // 最新更新
+)
+
+var VideoSortTypeName = map[VideoSortType]string{
+ VideoSortType_Recommend: "最近最佳",
+ VideoSortType_New: "最新更新",
+ VideoSortType_Hot: "最多观看",
+}
+var VideoSortItems = []VideoSortItem{
+ {Key: VideoSortType_Recommend, Value: "最近最佳"},
+ {Key: VideoSortType_New, Value: "最新更新"},
+ {Key: VideoSortType_Hot, Value: "最多观看"},
+}
+
+func (s VideoSortType) GetSortBson() bson.D {
+ switch s {
+ case VideoSortType_Recommend:
+ return bson.D{{FN.VideoInformation.Sort, -1}, {FN.VideoInformation.Real.WeekWatchs, -1}}
+ case VideoSortType_Hot:
+ return bson.D{{FN.VideoInformation.Real.WatchTimes, -1}}
+ case VideoSortType_New:
+ return bson.D{{FN.VideoInformation.AddedTime, -1}}
+ default:
+ return bson.D{{FN.VideoInformation.AddedTime, -1}}
+ }
+}
+
+func (s VideoSortType) GetSortItem() VideoSortItem {
+ switch s {
+ case VideoSortType_Recommend:
+ return VideoSortItems[0]
+ case VideoSortType_Hot:
+ return VideoSortItems[2]
+ case VideoSortType_New:
+ return VideoSortItems[1]
+ default:
+ return VideoSortItems[0]
+ }
+}
diff --git a/pub/models/videolikemod/tag.go b/pub/models/videolikemod/tag.go
new file mode 100644
index 0000000..232902d
--- /dev/null
+++ b/pub/models/videolikemod/tag.go
@@ -0,0 +1,28 @@
+package videolikemod
+
+// TagVideoLike 保存 video_like 集合字段名
+type TagVideoLike struct {
+ Id string
+ UserID string
+ VideoID string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoLike TagVideoLike
+}
+
+var TN struct {
+ VideoLike string
+}
+
+func init() {
+ FN.VideoLike.Id = "_id"
+ FN.VideoLike.UserID = "userId"
+ FN.VideoLike.VideoID = "videoId"
+ FN.VideoLike.CreatedAt = "createdAt"
+ FN.VideoLike.UpdatedAt = "updatedAt"
+
+ TN.VideoLike = "video_like"
+}
diff --git a/pub/models/videolikemod/type.go b/pub/models/videolikemod/type.go
new file mode 100644
index 0000000..bcd2b66
--- /dev/null
+++ b/pub/models/videolikemod/type.go
@@ -0,0 +1,15 @@
+package videolikemod
+
+import "time"
+
+// VideoLike 视频点赞关系
+type VideoLike struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ UserID int64 `json:"userId" bson:"userId"` // 用户ID
+ VideoID int64 `json:"videoId" bson:"videoId"` // 视频ID
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
+}
+
+// VideoLikeList 视频点赞列表
+type VideoLikeList []*VideoLike
diff --git a/pub/models/videolikemod/video_like.go b/pub/models/videolikemod/video_like.go
new file mode 100644
index 0000000..618b8bc
--- /dev/null
+++ b/pub/models/videolikemod/video_like.go
@@ -0,0 +1,83 @@
+package videolikemod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 video_like 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.VideoLike)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: FN.VideoLike.UserID, Value: 1}, {Key: FN.VideoLike.VideoID, Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: FN.VideoLike.VideoID, Value: 1}}},
+ {Keys: bson.D{{Key: FN.VideoLike.CreatedAt, Value: -1}, {Key: FN.VideoLike.VideoID, Value: 1}}},
+ {Keys: bson.D{{Key: FN.VideoLike.UserID, Value: 1}, {Key: FN.VideoLike.CreatedAt, Value: -1}, {Key: FN.VideoLike.Id, Value: -1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(videolikemod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// UpsertOneVideoLike 按用户ID和视频ID写入点赞关系,返回是否新增点赞
+func UpsertOneVideoLike(ctx context.Context, userId, videoId int64, now time.Time) (bool, *srvdef.SrvErr) {
+ filter := bson.M{FN.VideoLike.UserID: userId, FN.VideoLike.VideoID: videoId}
+ update := bson.M{
+ "$set": bson.M{FN.VideoLike.UpdatedAt: now},
+ "$setOnInsert": bson.M{
+ FN.VideoLike.Id: mongos.Lid.VideoLike.Get(),
+ FN.VideoLike.UserID: userId,
+ FN.VideoLike.VideoID: videoId,
+ FN.VideoLike.CreatedAt: now,
+ },
+ }
+ result, err := coll.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
+ if err != nil {
+ log.Warn("videolikemod:UpsertOneVideoLike db.UpdateOne failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.UpsertedCount > 0, nil
+}
+
+// DeleteOneVideoLike 删除一条视频点赞关系,返回是否实际删除
+func DeleteOneVideoLike(ctx context.Context, filter bson.M) (bool, *srvdef.SrvErr) {
+ result, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("videolikemod:DeleteOneVideoLike db.DeleteOne failed", log.Any("filter", filter), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return result > 0, nil
+}
+
+// CountVideoLike 统计视频点赞关系数量
+func CountVideoLike(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter)
+ if err != nil {
+ log.Warn("videolikemod:CountVideoLike db.Count failed", log.Any("filter", filter), log.E(err))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// AggregateVideoLike 聚合查询视频点赞关系
+func AggregateVideoLike(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("videolikemod:AggregateVideoLike db.Aggregate failed", log.Any("pipeline", pipeline), log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/videotagmod/tag.go b/pub/models/videotagmod/tag.go
new file mode 100755
index 0000000..d3a781e
--- /dev/null
+++ b/pub/models/videotagmod/tag.go
@@ -0,0 +1,48 @@
+package videotagmod
+
+type TagVideoTagInfo struct {
+ CreatedAt string
+ Id string
+ IndexNow string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ Title string
+ UpdatedAt string
+ WatchTimes string
+ Works string
+ Images string
+ MediaTypes string
+ IsShowHome string
+}
+
+var FN struct {
+ VideoTagInfo TagVideoTagInfo
+}
+
+var TN struct {
+ VideoTagInfo string
+}
+
+func init() {
+ FN.VideoTagInfo.CreatedAt = "createdAt"
+ FN.VideoTagInfo.Id = "_id"
+ FN.VideoTagInfo.IndexNow = "indexNow"
+ FN.VideoTagInfo.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoTagInfo.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoTagInfo.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoTagInfo.Sort = "sort"
+ FN.VideoTagInfo.Status = "status"
+ FN.VideoTagInfo.Title = "title"
+ FN.VideoTagInfo.UpdatedAt = "updatedAt"
+ FN.VideoTagInfo.WatchTimes = "watchTimes"
+ FN.VideoTagInfo.Works = "works"
+ FN.VideoTagInfo.Images = "images"
+ FN.VideoTagInfo.MediaTypes = "mediaTypes"
+ FN.VideoTagInfo.IsShowHome = "isShowHome"
+ TN.VideoTagInfo = "video_taginfo"
+}
diff --git a/pub/models/videotagmod/taginfo.go b/pub/models/videotagmod/taginfo.go
new file mode 100644
index 0000000..102c0ac
--- /dev/null
+++ b/pub/models/videotagmod/taginfo.go
@@ -0,0 +1,155 @@
+package videotagmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoTagInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.Title, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.Status, Value: 1},
+ {Key: FN.VideoTagInfo.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.Status, Value: 1},
+ {Key: FN.VideoTagInfo.WatchTimes, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoTagInfoIndex) Create VideoTagInfo Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoTagInfo 插入一个渠道统计
+func InsertOneVideoTagInfo(ctx context.Context, qa *VideoTagInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoTagInfomod:InsertOneVideoTagInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoTagInfo 删除一个渠道统计
+func DelOneVideoTagInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoTagInfomod:DelOneVideoTagInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoTagInfo 查找一个渠道统计
+func FindOneVideoTagInfo(ctx context.Context, filter bson.M) (
+ *VideoTagInfo, *srvdef.SrvErr) {
+ var VideoTagInfo VideoTagInfo
+ if err := db.FindOneEx(ctx, &VideoTagInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoTagInfomod:FindOneVideoTagInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoTagInfo, nil
+}
+
+func FindManyVideoTagInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoTagInfoList, *srvdef.SrvErr) {
+ data := make(VideoTagInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoTagInfos", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoTagInfos 统计渠道统计数量
+func CountVideoTagInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoTagInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoTagInfo 更新一个渠道统计
+func UpdateOneVideoTagInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoTagInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoTagInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// BulkWriteVideoTagInfo 批量写入/更新视频标签
+func BulkWriteVideoTagInfo(ctx context.Context, docs []mongo.WriteModel,
+ opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, *srvdef.SrvErr) {
+ result, err := db.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ log.Warn("VideoTagInfo:BulkWriteVideoTagInfo db.BulkWrite failed", log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result, nil
+}
+
+func UpdateManyVideoTagInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoTagInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/videotagmod/type.go b/pub/models/videotagmod/type.go
new file mode 100644
index 0000000..387b133
--- /dev/null
+++ b/pub/models/videotagmod/type.go
@@ -0,0 +1,24 @@
+package videotagmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_taginfo
+type VideoTagInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Images string `json:"images" bson:"images"` // 背景
+ Works int64 `json:"works" bson:"works"` // 作品数量
+ MediaTypes []int64 `json:"mediaTypes" bson:"mediaTypes"` // 作者关联的资源类型 1视频
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 预计算:该标签下所有视频的累计播放量
+ IsShowHome bool `json:"isShowHome" bson:"isShowHome"` // 是否显示在首页
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoTagInfoList []*VideoTagInfo
diff --git a/pub/mongos/QueryBuilder.go b/pub/mongos/QueryBuilder.go
new file mode 100644
index 0000000..9cdb5df
--- /dev/null
+++ b/pub/mongos/QueryBuilder.go
@@ -0,0 +1,150 @@
+package mongos
+
+import (
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+type QueryBuilder struct {
+ query bson.M
+}
+
+func NewQueryBuilder() *QueryBuilder {
+ return &QueryBuilder{query: bson.M{}}
+}
+
+func (q *QueryBuilder) Build() bson.M {
+ return q.query
+}
+func (q *QueryBuilder) addCondition(condition bool, field string, value interface{}) {
+ if condition {
+ q.query[field] = value
+ }
+}
+
+func (q *QueryBuilder) Eq(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = value
+ }
+ return q
+}
+
+func (q *QueryBuilder) Ne(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$ne": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Or(conditions ...bson.M) *QueryBuilder {
+ if len(conditions) > 0 {
+ q.query["$or"] = conditions
+ }
+ return q
+}
+
+func (q *QueryBuilder) Set(value bson.M) *QueryBuilder {
+ q.query["$set"] = value
+ return q
+}
+
+func (q *QueryBuilder) Gt(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$gt": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Gte(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$gte": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Lt(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$lt": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Lte(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$lte": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) In(field string, values *[]int64, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$in": *values}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Nin(field string, values []interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$nin": values}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Regex(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"regex": value.(string)}
+ }
+ return q
+}
+
+func (q *QueryBuilder) All(field string, values []interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$all": values}
+ }
+ return q
+}
+
+func (q *QueryBuilder) ElemMatch(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$elemMatch": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Size(field string, value int, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$size": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Exists(field string, value bool, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$exists": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Mod(field string, divisor, remainder int, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$mod": []int{divisor, remainder}}
+ }
+ return q
+}
+
+func (q *QueryBuilder) GeoWithin(field string, geometry interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$geoWithin": bson.M{
+ "$geometry": geometry,
+ }}
+ }
+ return q
+}
+
+func (q *QueryBuilder) GeoIntersects(field string, geometry interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$geoIntersects": bson.M{
+ "$geometry": geometry,
+ }}
+ }
+ return q
+}
diff --git a/pub/mongos/base_mod.go b/pub/mongos/base_mod.go
new file mode 100644
index 0000000..e4efa22
--- /dev/null
+++ b/pub/mongos/base_mod.go
@@ -0,0 +1,582 @@
+package mongos
+
+import (
+ "context"
+ "errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+
+ "reflect"
+ "strings"
+ "time"
+
+ "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"
+)
+
+type Collection struct {
+ name string
+ coll *mongo.Collection
+}
+
+func (b *Collection) FindOne(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOneOptions) error {
+ err := b.coll.FindOne(ctx, query, opts...).Decode(bind)
+ if err != nil && err == mongo.ErrNoDocuments {
+ return nil
+ }
+ return err
+}
+
+func (b *Collection) FindOneEx(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOneOptions) error {
+ err := b.coll.FindOne(ctx, query, opts...).Decode(bind)
+ return err
+}
+
+func (b *Collection) FindMOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update interface{}, opts ...*options.FindOneAndUpdateOptions) error {
+ setReturn := false
+ for _, opt := range opts {
+ if opt != nil && opt.ReturnDocument != nil {
+ setReturn = true
+ break
+ }
+ }
+ if !setReturn {
+ after := options.After
+ opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+ }
+ um := utils.Struct2Map(update)
+ // um["update_at"] = time.Now()
+ // set := bson.M{
+ // "$set": um,
+ // }
+ result := b.coll.FindOneAndUpdate(ctx, query, um, opts...)
+ err := result.Decode(bind)
+ return err
+}
+
+func (b *Collection) FindMOneAndUpdateInc(ctx context.Context, bind interface{}, query bson.M, update interface{}, inc interface{}, opts ...*options.FindOneAndUpdateOptions) error {
+ um := utils.Struct2Map(update)
+ um["update_at"] = time.Now()
+ im := utils.Struct2Map(inc)
+ set := bson.M{
+ "$set": um,
+ }
+ if im != nil {
+ set["$inc"] = inc
+ }
+
+ setReturn := false
+ for _, opt := range opts {
+ if opt != nil && opt.ReturnDocument != nil {
+ setReturn = true
+ break
+ }
+ }
+ if !setReturn {
+ after := options.After
+ opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+ }
+
+ result := b.coll.FindOneAndUpdate(ctx, query, set, opts...)
+ err := result.Decode(bind)
+ return err
+}
+
+// func (b *Collection) FindOneAndUpdateTiny(
+// ctx context.Context,
+// query interface{},
+// update bson.M,
+// opts ...*options.FindOneAndUpdateOptions) error {
+// result := b.collection().FindOneAndUpdate(ctx, query, update, opts...)
+// return result.Err()
+// }
+
+// func (b *MongoTool) FindOneAndUpdateReturnTiny(ctx context.Context,
+// bind interface{},
+// query bson.M,
+// update bson.M,
+// afterDoc bool,
+// opts ...*options.FindOneAndUpdateOptions) error {
+
+// if afterDoc {
+// opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.After))
+// } else {
+// opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.Before))
+// }
+
+// result := b.collection().FindOneAndUpdate(ctx, query, update, opts...).Decode(bind)
+// return result
+// }
+
+// func (b *Collection) FindOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
+// setReturn := false
+// for _, opt := range opts {
+// if opt != nil && opt.ReturnDocument != nil {
+// setReturn = true
+// break
+// }
+// }
+// if !setReturn {
+// after := options.After
+// opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+// }
+// b.setUpdatedAt(update, time.Now())
+// result := b.collection().FindOneAndUpdate(ctx, query, update, opts...)
+// err := result.Decode(bind)
+// return err
+// }
+
+func (b *Collection) Find(ctx context.Context, bind interface{}, query bson.M, opt ...*options.FindOptions) error {
+ err := b.validSlice(bind)
+ cur, err := b.coll.Find(ctx, query, opt...)
+ if err != nil && err == mongo.ErrNoDocuments {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ err = cur.All(ctx, bind)
+ return err
+}
+
+// func (b *Collection) FindByPage(ctx context.Context, bind interface{}, query bson.M, pageAndSort *PageAndSort, opts ...*options.FindOptions) error {
+// err := b.validSlice(bind)
+
+// if err != nil {
+// return err
+// }
+
+// opt := &options.FindOptions{}
+// if pageAndSort == nil {
+// return errors.New("page must not nil")
+// }
+
+// if pageAndSort.Page < 0 || pageAndSort.PageSize == 0 {
+// return errors.New("page and PageSize must not nil")
+// }
+
+// skip := pageAndSort.Page * pageAndSort.PageSize
+// opt.Skip = &skip
+// opt.Limit = &pageAndSort.PageSize
+// if pageAndSort.Sort != nil {
+// opt.Sort = pageAndSort.Sort
+// }
+// opts = append(opts, opt)
+// cur, err := b.collection().Find(ctx, query, opts...)
+// if err != nil && err == mongo.ErrNoDocuments {
+// return nil
+// } else if err != nil {
+// return err
+// }
+// err = cur.All(ctx, bind)
+// return err
+
+// }
+
+func (b *Collection) FindOneAndDelete(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOneAndDeleteOptions) error {
+ result := b.coll.FindOneAndDelete(ctx, query, opts...)
+ if result.Err() != nil {
+ return result.Err()
+ }
+ return result.Decode(bind)
+}
+
+func (b *Collection) InsertOneTiny(ctx context.Context, doc interface{}) (err error) {
+ _, err = b.coll.InsertOne(ctx, doc)
+ return
+}
+
+func (b *Collection) InsertOne(ctx context.Context, doc interface{}) (int, error) {
+ result, err := b.coll.InsertOne(ctx, doc)
+ if err != nil {
+ // log.Info("Collection:InsertOne failed", log.Any("colName", b.name), log.E(err))
+ return 0, err
+ }
+ if _, ok := result.InsertedID.(primitive.ObjectID); ok {
+ return 1, err
+ }
+ return 0, err
+}
+func (b *Collection) IDInsertOne(ctx context.Context, doc interface{}) (string, error) {
+ result, err := b.coll.InsertOne(ctx, doc)
+ if err != nil {
+ log.Info("Collection:InsertOne failed", log.Any("colName", b.name), log.E(err))
+ return "", err
+ }
+ if id, ok := result.InsertedID.(primitive.ObjectID); ok {
+ return id.Hex(), err
+ }
+ return "", err
+}
+
+func (b *Collection) validSlice(bind interface{}) error {
+ if reflect.TypeOf(bind).Kind() != reflect.Ptr {
+ return errors.New("interface must be a Ptr")
+ }
+ if reflect.TypeOf(bind).Elem().Kind() != reflect.Slice {
+ return errors.New("interface must be a slice")
+ }
+ return nil
+}
+
+func (b *Collection) InsertMany(ctx context.Context, docs interface{}) ([]interface{}, error) {
+ err := b.validSlice(docs)
+ if err != nil {
+ return nil, err
+ }
+ res := b.setTimeForSlice(docs)
+ result, err := b.coll.InsertMany(ctx, res)
+ if err != nil {
+ return nil, err
+ }
+ return result.InsertedIDs, err
+}
+
+// struct方式
+func (b *Collection) UpdateMOne(ctx context.Context, query bson.M, update interface{}) (*mongo.UpdateResult, error) {
+ um := utils.Struct2Map(update)
+ um["update_at"] = time.Now()
+ set := bson.M{
+ "$set": um,
+ }
+ return b.coll.UpdateOne(ctx, query, set)
+}
+
+func (b *Collection) UpdateMOneInc(ctx context.Context, query bson.M, update interface{}, inc interface{}) (*mongo.UpdateResult, error) {
+ um := utils.Struct2Map(update)
+ um["update_at"] = time.Now()
+ set := bson.M{
+ "$set": um,
+ }
+ im := utils.Struct2Map(inc)
+ if im != nil {
+ set["$inc"] = im
+ }
+ return b.coll.UpdateOne(ctx, query, set)
+}
+
+// 原生bson.M 方式
+func (b *Collection) UpdateOne(ctx context.Context, query bson.M, update bson.M, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) {
+ //b.setUpdatedAt(update, time.Now())
+ return b.coll.UpdateOne(ctx, query, update, opts...)
+}
+
+func (b *Collection) UpsertOne(ctx context.Context, query bson.M, update bson.M) (*mongo.UpdateResult, error) {
+ opt := &options.UpdateOptions{}
+ opt.SetUpsert(true)
+ //b.setDeaultTime(update)
+ result, err := b.coll.UpdateOne(ctx, query, update, opt)
+ return result, err
+
+}
+
+func (b *Collection) UpdateMMany(ctx context.Context, query bson.M, update interface{}) (*mongo.UpdateResult, error) {
+ // b.setUpdatedAt(update, time.Now())
+ return b.coll.UpdateMany(ctx, query, update)
+}
+
+func (b *Collection) UpdateMany(ctx context.Context, query bson.M, update bson.M) (*mongo.UpdateResult, error) {
+ //b.setUpdatedAt(update, time.Now())
+ return b.coll.UpdateMany(ctx, query, update)
+}
+
+// func (b *Collection) DeleteById(ctx context.Context, id interface{}) (int64, error) {
+
+// param := bson.M{"_id": id}
+// result, err := b.collection().DeleteOne(ctx, param)
+// if err != nil {
+// return 0, err
+// }
+// return result.DeletedCount, nil
+
+// }
+
+func (b *Collection) DeleteOne(ctx context.Context, query bson.M, opts ...*options.DeleteOptions) (int64, error) {
+ result, err := b.coll.DeleteOne(ctx, query)
+ if err != nil {
+ return 0, err
+ }
+ return result.DeletedCount, nil
+}
+
+func (b *Collection) DeleteMany(ctx context.Context, query bson.M, opts ...*options.DeleteOptions) (int64, error) {
+ result, err := b.coll.DeleteMany(ctx, query, opts...)
+ if err != nil {
+ return 0, err
+ }
+ deletedCount := result.DeletedCount
+ return deletedCount, nil
+
+}
+
+func (b *Collection) Distince(ctx context.Context, filter bson.M, fieldName string) ([]interface{}, error) {
+ return b.coll.Distinct(ctx, fieldName, filter)
+}
+
+func (b *Collection) Aggregate(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error {
+
+ err := b.validSlice(bind)
+ if err != nil {
+ return err
+ }
+ //bs, _ := json.Marshal(pipeline)
+ //log.Debug(string(bs))
+ cur, err := b.coll.Aggregate(ctx, pipeline, opts...)
+ if err != nil {
+ return err
+ }
+ return cur.All(ctx, bind)
+}
+
+func (b *Collection) ObjectAggregate(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error {
+ cur, err := b.coll.Aggregate(ctx, pipeline, opts...)
+ if err != nil {
+ return err
+ }
+ if cur.Next(ctx) {
+ return cur.Decode(bind)
+ }
+ return nil
+}
+
+func (b *Collection) QueryOne(ctx context.Context, bind interface{}, query interface{}, opts ...*options.FindOneOptions) error {
+ err := b.coll.FindOne(ctx, query, opts...).Decode(bind)
+ if err != nil && err == mongo.ErrNoDocuments {
+ return nil
+ }
+ return err
+}
+
+func (b *Collection) Count(ctx context.Context, query interface{}, opts ...*options.CountOptions) (int64, error) {
+ return b.coll.CountDocuments(ctx, query, opts...)
+}
+
+func (b *Collection) EstimatedCount(ctx context.Context, opts ...*options.EstimatedDocumentCountOptions) (int64, error) {
+ return b.coll.EstimatedDocumentCount(ctx, opts...)
+}
+
+func (b *Collection) BulkWrite(ctx context.Context, models []mongo.WriteModel, opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, error) {
+ return b.coll.BulkWrite(ctx, models, opts...)
+}
+
+// CreateIndex 创建数据索引.options 在index里面创建
+func (b *Collection) CreateIndex(ctx context.Context, models []mongo.IndexModel) ([]string, error) {
+ data, err := b.coll.Indexes().CreateMany(ctx, models)
+ if err != nil {
+ if strings.Contains(err.Error(), "IndexKeySpecsConflict") {
+ return data, nil
+ }
+ if strings.Contains(err.Error(), "IndexOptionsConflict") {
+ return data, nil
+ }
+ return data, err
+ }
+ return data, nil
+}
+
+// DropIndex 删除数据索引.options 在index里面创建
+// func (b *Collection) DropIndex(ctx context.Context, indexName string) error {
+// _, err := b.coll.Indexes().DropOne(ctx, indexName)
+// if err != nil {
+// return err
+// }
+// return nil
+// }
+
+// func (b *MongoTool) addCreatedAt(val interface{}) {
+// if _, ok := val.(bson.M); ok {
+// return
+// }
+// if _, ok := val.(map[string]interface{}); ok {
+// return
+// }
+// v := reflect.ValueOf(val).Elem().FieldByName("CreatedAt")
+// if v.IsValid() {
+// v.Set(reflect.ValueOf(time.Now()))
+// }
+
+// }
+// func (b *MongoTool) addUpdatedAt(val interface{}) {
+// if _, ok := val.(bson.M); ok {
+// return
+// }
+// if _, ok := val.(map[string]interface{}); ok {
+// return
+// }
+// v := reflect.ValueOf(val).Elem().FieldByName("UpdatedAt")
+// if v.IsValid() {
+// v.Set(reflect.ValueOf(time.Now()))
+// }
+// }
+
+func (b *Collection) setTimeForSlice(docs interface{}) []interface{} {
+ arr := reflect.ValueOf(docs).Elem()
+ result := make([]interface{}, 0)
+ for i := 0; i < arr.Len(); i++ {
+ ele := arr.Index(i).Elem()
+ now := time.Now()
+ if ma := ele.FieldByName("update_at"); ma.IsValid() {
+ ma.Set(reflect.ValueOf(now))
+ }
+ if ca := ele.FieldByName("create_at"); ca.IsValid() {
+ ca.Set(reflect.ValueOf(now))
+ }
+ result = append(result, ele.Interface())
+ }
+ return result
+}
+
+// func (b *Collection) validSlice(bind interface{}) error {
+
+// if reflect.TypeOf(bind).Kind() != reflect.Ptr {
+// return errors.New("interface must be a Ptr")
+// }
+// if reflect.TypeOf(bind).Elem().Kind() != reflect.Slice {
+// return errors.New("interface must be a slice")
+// }
+// return nil
+// }
+
+// func (b *Collection) setUpdatedAt(doc bson.M, now time.Time) {
+// set, exists := doc["$set"]
+// if !exists {
+// doc["$set"] = bson.M{"updatedAt": now}
+// return
+// }
+
+// if res, ok := set.(bson.M); ok {
+// res["updatedAt"] = now
+// }
+// }
+
+// func (b *Collection) setCreatedAt(doc bson.M, now time.Time) {
+// set, exists := doc["$set"]
+// if !exists {
+// doc["$set"] = bson.M{"createdAt": now}
+// return
+// }
+// if res, ok := set.(bson.M); ok {
+// res["createdAt"] = now
+// }
+// }
+
+// func (b *Collection) setDeaultTime(doc bson.M) {
+// now := time.Now()
+// set, exists := doc["$set"]
+// if exists {
+// if res, ok := set.(bson.M); ok {
+// delete(res, "createdAt")
+// res["updatedAt"] = now
+// if d, ok := doc["$setOnInsert"]; ok {
+// if ss, ok := d.(bson.M); ok {
+// if _, ok := ss["createdAt"]; !ok {
+// ss["createdAt"] = now
+// }
+// }
+// } else {
+// doc["$setOnInsert"] = bson.M{"createdAt": now}
+// }
+// return
+// }
+// if res, ok := set.(interface{}); ok {
+// struct2Map := utils.Struct2Map(res)
+// delete(struct2Map, "createdAt")
+// struct2Map["updatedAt"] = now
+// doc["$set"] = struct2Map
+// doc["$setOnInsert"] = bson.M{"createdAt": now}
+// return
+// }
+// }
+// }
+
+// ////////////////////////////////////////////
+func (b *Collection) GetStringIDInsertOne(ctx context.Context, doc interface{}) (string, error) {
+ result, err := b.coll.InsertOne(ctx, doc)
+ if err != nil {
+ return "", err
+ }
+ if id, ok := result.InsertedID.(primitive.ObjectID); ok {
+ return id.Hex(), nil
+ }
+ return "", err
+}
+
+// InsertOne 插入单条信息
+func (b *Collection) GetObjectIDInsertOne(ctx context.Context, document interface{}, opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) {
+ return b.coll.InsertOne(ctx, document, opts...)
+}
+
+// UpdateOneNew 或者修改或者插入一条数据
+func (b *Collection) UpdateOneNew(ctx context.Context, query bson.M, update bson.M) (*mongo.UpdateResult, error) {
+ return b.coll.UpdateOne(ctx, query, update, options.Update().SetUpsert(true))
+}
+
+func (b *Collection) Max(ctx context.Context, fieldName string, filter bson.M) (int64, error) {
+ //b.coll.Aggregate(ctx, )
+ type Temp struct {
+ Max int64 `bson:"max"`
+ }
+ var temp []Temp
+ query := []bson.M{
+ {"$group": bson.M{
+ "_id": nil,
+ "max": bson.M{"$max": "$" + fieldName},
+ },
+ },
+ }
+ if err := b.Aggregate(ctx, &temp, query); err != nil || len(temp) <= 0 {
+ return 0, err
+ }
+
+ return temp[0].Max, nil
+}
+
+// func (b *Collection) setUpdatedAt(doc bson.M, now time.Time) {
+// set, exists := doc["$set"]
+// if !exists {
+// doc["$set"] = bson.M{"updatedAt": now}
+// return
+// }
+
+// if res, ok := set.(bson.M); ok {
+// res["updatedAt"] = now
+// }
+// }
+
+func (b *Collection) FindOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
+ setReturn := false
+ for _, opt := range opts {
+ if opt != nil && opt.ReturnDocument != nil {
+ setReturn = true
+ break
+ }
+ }
+ if !setReturn {
+ after := options.After
+ opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+ }
+ // b.coll.setUpdatedAt(update, time.Now())
+ result := b.coll.FindOneAndUpdate(ctx, query, update, opts...)
+ err := result.Decode(bind)
+ return err
+}
+
+// //
+// func (b *Collection) IfNotExistThenInsertAndUpdate(ctx context.Context, query bson.M, insert bson.M, update bson.M) error {
+// // func (b *MongoTool) FindOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
+// err := b.FindOneAndUpdate(ctx, nil, query, update)
+// if err != nil {
+// _, err = b.InsertOne(ctx, insert)
+// }
+// return err
+// }
+
+func (b *Collection) WatchOpLog(ctx context.Context, pipeline []bson.M,
+ opt ...*options.ChangeStreamOptions) (*mongo.ChangeStream, error) {
+ changeStream, err := b.coll.Watch(ctx, pipeline, opt...)
+ if err != nil {
+ return nil, err
+ }
+ return changeStream, nil
+}
diff --git a/pub/mongos/codec.go b/pub/mongos/codec.go
new file mode 100644
index 0000000..9d9fb30
--- /dev/null
+++ b/pub/mongos/codec.go
@@ -0,0 +1,119 @@
+package mongos
+
+import (
+ "fmt"
+ "github.com/shopspring/decimal"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/bsoncodec"
+ "go.mongodb.org/mongo-driver/bson/bsonrw"
+ "go.mongodb.org/mongo-driver/bson/bsontype"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "reflect"
+ "time"
+)
+
+var Registry = func() *bsoncodec.Registry {
+ builder := bson.NewRegistryBuilder()
+ builder.RegisterDecoder(reflect.TypeOf(time.Time{}), &localTimeDecoder{})
+ builder.RegisterDecoder(reflect.TypeOf(decimal.Decimal{}), &decimalDecoder{})
+ builder.RegisterEncoder(reflect.TypeOf(decimal.Decimal{}), &decimalEncoder{})
+ builder.RegisterDefaultDecoder(reflect.Float32, &floatDecoder{})
+ builder.RegisterDefaultDecoder(reflect.Float64, &floatDecoder{})
+ return builder.Build()
+}()
+
+type floatDecoder struct {
+}
+
+func (dvd *floatDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
+ var f float64
+ var err error
+ switch vr.Type() {
+ case bsontype.Int32:
+ i32, err := vr.ReadInt32()
+ if err != nil {
+ return err
+ }
+ f = float64(i32)
+ case bsontype.Int64:
+ i64, err := vr.ReadInt64()
+ if err != nil {
+ return err
+ }
+ f = float64(i64)
+ case bsontype.Double:
+ f, err = vr.ReadDouble()
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("cannot decode %v into a float32 or float64 type", vr.Type())
+ }
+ val.SetFloat(f)
+ return nil
+}
+
+type localTimeDecoder struct{}
+
+func (*localTimeDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
+ dvd := bsoncodec.DefaultValueDecoders{}
+ err := dvd.TimeDecodeValue(dc, vr, val)
+ if err != nil {
+ return err
+ }
+ t := val.Interface().(time.Time)
+ val.Set(reflect.ValueOf(t.Local()))
+ return nil
+}
+
+type decimalDecoder struct{}
+
+func (*decimalDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
+ if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) {
+ return bsoncodec.ValueDecoderError{Name: "DecimalDecodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val}
+ }
+ var (
+ num bool
+ )
+ if vr.Type() == bson.TypeInt32 || vr.Type() == bson.TypeInt64 {
+ vr.ReadInt32()
+ vr.ReadInt64()
+ num = true
+ }
+ if num {
+ d := decimal.NewFromFloat(0.0)
+ val.Set(reflect.ValueOf(d))
+ return nil
+ }
+
+ mongodecimal, err := vr.ReadDecimal128()
+ if err != nil {
+ return err
+ }
+ d, err := decimal.NewFromString(mongodecimal.String())
+ if err != nil {
+ return err
+ }
+ val.Set(reflect.ValueOf(d))
+ return nil
+}
+
+//
+
+type decimalEncoder struct{}
+
+func (*decimalEncoder) EncodeValue(ctx bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
+ if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) {
+ return bsoncodec.ValueDecoderError{Name: "DecimalEncodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val}
+ }
+
+ if d, ok := val.Interface().(decimal.Decimal); ok {
+ mongodecimal, err := primitive.ParseDecimal128(d.StringFixed(2))
+ if err != nil {
+ return err
+ }
+ val = reflect.ValueOf(mongodecimal)
+ }
+ dve := bsoncodec.DefaultValueEncoders{}
+ return dve.Decimal128EncodeValue(ctx, vw, val)
+}
diff --git a/pub/mongos/generate_id.go b/pub/mongos/generate_id.go
new file mode 100644
index 0000000..f0f9487
--- /dev/null
+++ b/pub/mongos/generate_id.go
@@ -0,0 +1,110 @@
+package mongos
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/models/utils"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var Lid struct {
+ User *lid
+ MwAcc *lid
+ MwRole *lid
+ MwOpLog *lid
+ MwAccToken *lid
+ SystemMsg *lid
+ AdvitiseInfo *lid
+ AdvitiseBind *lid
+ AdvitiseLocation *lid
+ App *lid
+ VideoTag *lid
+ VideoCategory *lid
+ VideoInfo *lid
+ VideoFavorite *lid
+ VideoLike *lid
+ TransCategory *lid
+ Translation *lid
+ VideoCreator *lid
+ Comment *lid
+ HtmlText *lid
+ ContentTag *lid
+ Actress *lid
+ HotNav *lid
+ HomeBanner *lid
+ Announcement *lid
+ Playlet *lid
+ PlayletChapter *lid
+ PlayletCategory *lid
+ PlayletHomeConfig *lid
+ PlayletLike *lid
+ PlayletFavorite *lid
+ PlayletTheme *lid
+ PlayletTag *lid
+}
+
+func StartLid() {
+
+ coll := Shop.Coll(utils.TN.GenerateId)
+ initIndex(coll)
+
+ Lid.User = createLid(coll, "user", 10)
+ Lid.MwAcc = createLid(coll, "MwAcc", 1)
+ Lid.MwRole = createLid(coll, "MwRole", 1)
+ Lid.MwOpLog = createLid(coll, "MwOpLog", 10)
+ Lid.MwAccToken = createLid(coll, "MwAccToken", 10)
+ Lid.SystemMsg = createLid(coll, "SystemMsg", 10)
+ Lid.AdvitiseInfo = createLid(coll, "AdvitiseInfo", 10)
+ Lid.AdvitiseBind = createLid(coll, "AdvitiseBind", 10)
+ Lid.AdvitiseLocation = createLid(coll, "AdvitiseLocation", 10)
+ Lid.App = createLid(coll, "App", 10)
+ Lid.VideoTag = createLid(coll, "VideoTag", 10)
+ Lid.VideoCategory = createLid(coll, "VideoCategory", 10)
+ Lid.VideoInfo = createLid(coll, "VideoInfo", 10)
+ Lid.VideoFavorite = createLid(coll, "VideoFavorite", 10)
+ Lid.VideoLike = createLid(coll, "VideoLike", 10)
+ Lid.TransCategory = createLid(coll, "TransCategory", 10)
+ Lid.Translation = createLid(coll, "TranslationData", 10)
+ Lid.VideoCreator = createLid(coll, "VideoCreator", 10)
+ Lid.Comment = createLid(coll, "Comment", 10)
+ Lid.HtmlText = createLid(coll, "HtmlText", 10)
+ Lid.ContentTag = createLid(coll, "contentTag", 10)
+ Lid.Actress = createLid(coll, "Actress", 10)
+ Lid.HotNav = createLid(coll, "HotNav", 10)
+ Lid.HomeBanner = createLid(coll, "homeBanner", 10)
+ Lid.Announcement = createLid(coll, "announcement", 10)
+ Lid.Playlet = createLid(coll, "playlet", 10)
+ Lid.PlayletChapter = createLid(coll, "playletChapter", 10)
+ Lid.PlayletCategory = createLid(coll, "playletCategory", 10)
+ Lid.PlayletHomeConfig = createLid(coll, "playletHomeConfig", 10)
+ Lid.PlayletLike = createLid(coll, "playletLike", 10)
+ Lid.PlayletFavorite = createLid(coll, "playletFavorite", 10)
+ Lid.PlayletTheme = createLid(coll, "playletTheme", 10)
+ Lid.PlayletTag = createLid(coll, "playletTag", 10)
+}
+
+// initIndex 初始化索引
+func initIndex(coll *Collection) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: utils.FN.GenerateId.BusinessId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := coll.CreateIndex(context.Background(), indexes); err != nil {
+ panic(fmt.Sprintf("(user:initIndex) Create Index user failed! err:%+v:", err))
+ }
+}
+
+func createLid(db *Collection, businessId string, len int) *lid {
+ lid, err := newLid(db, businessId, len)
+ if err != nil {
+ panic(err)
+ }
+ return lid
+}
diff --git a/pub/mongos/lid.go b/pub/mongos/lid.go
new file mode 100644
index 0000000..f2dd3da
--- /dev/null
+++ b/pub/mongos/lid.go
@@ -0,0 +1,102 @@
+package mongos
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/models/utils"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+const notFindMsg = "mongo: no documents in result"
+
+type lid struct {
+ db *Collection // 数据库连接
+ businessId string // 业务id
+ ch chan int64 // id缓冲池
+ min, max int64 // id段最小值,最大值
+}
+
+// newLid 创建一个lid,db:数据库连接;businessId:业务id;len:缓冲池大小
+func newLid(db *Collection, businessId string, len int) (*lid, error) {
+ lid := lid{
+ db: db,
+ businessId: businessId,
+ ch: make(chan int64, len),
+ }
+ go lid.productId()
+ return &lid, nil
+}
+
+// Get 获取自增id
+func (l *lid) Get() int64 {
+ return <-l.ch
+}
+
+// productId 生产id
+func (l *lid) productId() {
+ l.reset()
+ for {
+ if l.min >= l.max {
+ l.reset()
+
+ }
+
+ l.min++
+ l.ch <- l.min
+ }
+}
+
+// reset 不断尝试从数据库获取,直到成功
+func (l *lid) reset() {
+ for {
+ err := l.getFromDB()
+ if err == nil {
+ return
+ }
+ time.Sleep(time.Second)
+ continue
+ }
+}
+
+func (l *lid) getFromDB() error {
+ var (
+ max_id int64
+ step int64
+ //idModel IdModel
+ idModel utils.GenerateId
+ )
+ businessIdfilter := bson.M{utils.FN.GenerateId.BusinessId: l.businessId}
+ txErr := Shop.Transaction(context.Background(), func(sctx mongo.SessionContext) (err error) {
+ err = l.db.FindOneEx(sctx, &idModel, businessIdfilter)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ idModel.MaxId = 10000
+ _, err = l.db.InsertOne(sctx,
+ bson.M{
+ utils.FN.GenerateId.BusinessId: l.businessId,
+ utils.FN.GenerateId.MaxId: 0,
+ utils.FN.GenerateId.Step: 10,
+ utils.FN.GenerateId.Desc: "id自增器",
+ utils.FN.GenerateId.CreateAt: time.Now(),
+ utils.FN.GenerateId.UpdateAt: time.Now()})
+ if err != nil {
+ return
+ }
+ } else {
+ return
+ }
+ }
+ max_id = idModel.MaxId
+ step = idModel.Step
+ _, err = l.db.UpdateOne(sctx, businessIdfilter, bson.M{"$set": bson.M{utils.FN.GenerateId.MaxId: max_id + step}})
+ if err != nil {
+ return
+ }
+ l.min = max_id
+ l.max = max_id + step
+ return
+ })
+ return txErr
+}
diff --git a/pub/mongos/manager.go b/pub/mongos/manager.go
new file mode 100644
index 0000000..ddec16e
--- /dev/null
+++ b/pub/mongos/manager.go
@@ -0,0 +1,168 @@
+package mongos
+
+import (
+ "context"
+ "errors"
+ "time"
+ "videoweb/pub/common/log"
+
+ "go.mongodb.org/mongo-driver/mongo/readconcern"
+ "go.mongodb.org/mongo-driver/mongo/readpref"
+ "go.mongodb.org/mongo-driver/mongo/writeconcern"
+ "go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
+
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// var (
+// connPools *mongo.Client
+// dataBase *mongo.Database
+// )
+
+var (
+ //业务库
+ Shop *Manager
+ //日志库
+ ShopLog *Manager
+)
+
+type Manager struct {
+ client *mongo.Client
+ //dataBase *mongo.Database
+ url string
+ db string
+}
+
+func NewManager(url string) *Manager {
+ return &Manager{url: url}
+}
+
+func (m *Manager) Database() *mongo.Database {
+ return m.client.Database(m.db)
+}
+
+func (m *Manager) Coll(name string) *Collection {
+ coll := m.Database().Collection(name)
+ return &Collection{name: name, coll: coll}
+}
+
+func (m *Manager) Init() error {
+ cs, err := connstring.Parse(m.url)
+ if err != nil {
+ log.Error("db connect parse err", log.E(err))
+ return err
+ }
+ if cs.Database == "" {
+ log.Error("db connect database MUST contains")
+ return errors.New("database MUST contains")
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ client, err := mongo.Connect(ctx, options.Client().ApplyURI(m.url))
+ if err != nil {
+ log.Error("[warn] connect mongo db err", log.E(err))
+ return err
+ }
+ if err = client.Ping(ctx, nil); err != nil {
+ log.Error("[warn] ping mongo db err", log.E(err))
+ return err
+ }
+
+ //connPools = client
+ m.client = client
+ m.db = cs.Database
+
+ log.Info("mongos:Init ok!", log.Any("dbName", m.db))
+ return nil
+}
+
+func (m *Manager) DisConnect() {
+ if m.client != nil {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := m.client.Disconnect(ctx); err != nil {
+ log.Error("db disconnect err", log.E(err))
+ }
+
+ m.client = nil
+ }
+}
+
+// Transaction 开启事务处理包裹处理,里面处理的全是利用的事务的ctx 切记里面的context必须要使用同一个,因为里面有重试机制,循环调用该函数
+func (m *Manager) Transaction(ctx context.Context, fn func(sctx mongo.SessionContext) error) error {
+ opts := options.Session().SetDefaultReadPreference(readpref.Primary())
+ handler := func(sctx mongo.SessionContext) error {
+ lHandler := func(ctx mongo.SessionContext) (err error) {
+ tOpts := options.Transaction()
+ tOpts.SetReadConcern(readconcern.Snapshot())
+ tOpts.SetWriteConcern(writeconcern.New(writeconcern.WMajority()))
+ err = sctx.StartTransaction(tOpts)
+ if err != nil {
+ return
+ }
+ defer func() {
+ if rErr := recover(); rErr != nil {
+ log.Info("caught panic during transaction, aborting...", log.Any("", rErr))
+ aErr := sctx.AbortTransaction(sctx)
+ if aErr != nil {
+ log.Info("中止事务错误", log.E(aErr))
+ }
+ }
+ }()
+ err = fn(sctx)
+ if err != nil {
+ aErr := sctx.AbortTransaction(sctx)
+ if aErr != nil {
+ log.Info("中止事务错误", log.E(aErr))
+ }
+ log.Info("事务内错误", log.E(err))
+ return
+ }
+ return commitWithRetry(sctx)
+ }
+ return runTransactionWithRetry(sctx, lHandler)
+ }
+ return m.client.UseSessionWithOptions(ctx, opts, handler)
+}
+
+func runTransactionWithRetry(sctx mongo.SessionContext, txnFn func(mongo.SessionContext) error) error {
+ var err error
+ for i := int64(0); i < 100; i++ {
+ err = txnFn(sctx) // Performs transaction.
+ if err == nil {
+ return nil
+ }
+ // If transient error, retry the whole transaction
+ if cmdErr, ok := err.(mongo.CommandError); ok && cmdErr.HasErrorLabel("TransientTransactionError") {
+ continue
+ }
+ return err
+ }
+ return err
+}
+
+// commitWithRetry is an example function demonstrating transaction retry logic.
+func commitWithRetry(sctx mongo.SessionContext) error {
+ for {
+ err := sctx.CommitTransaction(sctx)
+ switch e := err.(type) {
+ case nil:
+ return nil
+ case mongo.CommandError:
+ // Can retry commit
+ if e.HasErrorLabel("UnknownTransactionCommitResult") {
+ continue
+ }
+ return e
+ default:
+ return e
+ }
+ }
+}
+
+// func Transaction(ctx context.Context, fn func(sctx mongo.SessionContext) error) error {
+// return connMgr.Transaction(ctx, fn)
+// }
diff --git a/pub/pubdef/comment.go b/pub/pubdef/comment.go
new file mode 100644
index 0000000..a33d935
--- /dev/null
+++ b/pub/pubdef/comment.go
@@ -0,0 +1,27 @@
+package pubdef
+
+// CommentObjectType 评论对象类型
+type CommentObjectType int64
+
+const (
+ CommentObjectTypeVideo CommentObjectType = 0 // 视频
+ CommentObjectTypePlaylet CommentObjectType = 4 // 短剧
+ CommentObjectTypePlayletChapter CommentObjectType = 11 // 短剧剧集
+)
+
+var commentObjectTypeNameMap = map[CommentObjectType]string{
+ CommentObjectTypeVideo: "视频",
+ CommentObjectTypePlaylet: "短剧",
+ CommentObjectTypePlayletChapter: "短剧剧集",
+}
+
+// CommentObjectTypeName 返回对象类型对应的展示名称
+func CommentObjectTypeName(objectType CommentObjectType) string {
+ return commentObjectTypeNameMap[objectType]
+}
+
+// ValidCommentObjectType 判断评论对象类型是否合法
+func ValidCommentObjectType(objectType CommentObjectType) bool {
+ _, ok := commentObjectTypeNameMap[objectType]
+ return ok
+}
diff --git a/pub/pubdef/flow.go b/pub/pubdef/flow.go
new file mode 100644
index 0000000..642293d
--- /dev/null
+++ b/pub/pubdef/flow.go
@@ -0,0 +1,36 @@
+package pubdef
+
+type Flow struct {
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 真实观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Comments int64 `json:"comments" bson:"comments"` // 评论条数
+ Likes int64 `json:"likes" bson:"likes"` // 真实点赞数(收藏总量)
+ WeekWatchs int64 `json:"weekWatchs" bson:"weekWatchs"` // 本周观看次数
+ MonthWatchs int64 `json:"monthWatchs" bson:"monthWatchs"` // 本月观看次数
+ YearWatchs int64 `json:"yearWatchs" bson:"yearWatchs"` // 本年观看次数
+ TodayWatchs int64 `json:"todayWatchs" bson:"todayWatchs"` // 当日观看次数
+ WeekLikes int64 `json:"weekLikes" bson:"weekLikes"` // 近7日收藏量
+ MonthLikes int64 `json:"monthLikes" bson:"monthLikes"` // 近30日收藏量
+ YearLikes int64 `json:"yearLikes" bson:"yearLikes"` // 近365日收藏量
+}
+
+type FakeFlow struct {
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 假观看次数
+ Likes int64 `json:"likes" bson:"likes"` // 假点赞数
+}
+
+type InformationStatus int32
+
+const (
+ InformationStatusWaiting InformationStatus = 0 // 初始化
+ InformationStatusVerifyOK InformationStatus = 1 // 上架
+ InformationStatusVerifyCancel InformationStatus = 2 // 下架
+)
+
+type InformationUpdateStatus int32
+
+const (
+ InformationUpdateStatusWaiting InformationUpdateStatus = 0 // 待发布
+ InformationUpdateStatusLoading InformationUpdateStatus = 1 // 连载中
+ InformationUpdateStatusEnd InformationUpdateStatus = 2 // 已完结
+)
diff --git a/pub/pubdef/media.go b/pub/pubdef/media.go
new file mode 100644
index 0000000..9de5a91
--- /dev/null
+++ b/pub/pubdef/media.go
@@ -0,0 +1,8 @@
+package pubdef
+
+type ContentType int64
+
+const (
+ ContentType_Video ContentType = 1 // 视频
+ ContentType_Collection ContentType = 2 // 合集
+)
diff --git a/pub/pubdef/pubdef.go b/pub/pubdef/pubdef.go
new file mode 100644
index 0000000..7b87cd7
--- /dev/null
+++ b/pub/pubdef/pubdef.go
@@ -0,0 +1,83 @@
+package pubdef
+
+import "fmt"
+
+type Page struct {
+ PageNum int64 `form:"pageNum" json:"pageNum,omitempty"`
+ PageSize int64 `form:"pageSize" json:"pageSize,omitempty"`
+}
+
+const (
+ DateISO = "2006-01-02"
+ TimeISO = "2006-01-02 15:04:05"
+ AppId = 1046 // 91pron
+ AppName = "海角社区"
+ DefaultChannelCode = "hjsq"
+
+ // CompanyPlatform 是内容中台授权的产品名,对应 company_platform 字段
+ CompanyPlatform = "海角社区"
+ // seoPushEnabled 对外推送总开关。需要推送时改为 true 后重新发版。
+ // false 时不登记任务,开启后再按当前页面地址生成并推送。
+ // 真正提交还要求 runMod=prod,测试环境即使打开开关也只 dry-run。
+ seoPushEnabled = false
+)
+
+func ReportAppId() string {
+ return fmt.Sprintf("JHA-%d", AppId)
+}
+
+// Enabled 业务开关是否打开(不含环境判断)。
+func Enabled() bool {
+ return seoPushEnabled
+}
+
+const PageSize int64 = 40
+
+func GetTotalPage(total int64, page int64) int64 {
+ totalPage := total / PageSize
+ if total%PageSize > 0 {
+ totalPage++
+ }
+ return totalPage
+}
+
+type SeoValue struct {
+ Title string `json:"title" bson:"webTitle"` // 网站SEO标题
+ Keyworld []string `json:"keyworld" bson:"webKeyworld"` // 网站SEO关键字
+ Description string `json:"description" bson:"webDescription"` // 网站SEO描述
+}
+
+// DisplayMode 分类展示方式
+type DisplayMode int64
+
+const (
+ DisplayModeDefault DisplayMode = 0 // 默认(上图下标题多列)
+ DisplayModeCardCarousel DisplayMode = 1 // 卡片层叠轮播
+ DisplayModeAvatarList DisplayMode = 2 // 头像列表
+ DisplayModeImageText DisplayMode = 3 // 图文列表(左图右标题多列)
+ DisplayModeBigCover DisplayMode = 4 // 大封面列表(一大图两列)
+)
+
+// CategoryDisplayPosition 分类显示位置
+type CategoryDisplayPosition int64
+
+const (
+ CategoryDisplayPositionTabOnly CategoryDisplayPosition = 0 // 仅TAB
+ CategoryDisplayPositionHomeOnly CategoryDisplayPosition = 1 // 仅首页
+ CategoryDisplayPositionBoth CategoryDisplayPosition = 2 // TAB和首页都显示
+)
+
+// CategorySortType 首页分类内容排序方式。
+type CategorySortType int64
+
+const (
+ CategorySortTypeAddedTimeDesc CategorySortType = 0 // 上架时间降序
+ CategorySortTypeWatchDesc CategorySortType = 1 // 观看次数降序
+ CategorySortTypeLikeDesc CategorySortType = 2 // 点赞(收藏)次数降序
+ CategorySortTypeAddedTimeAsc CategorySortType = 3 // 上架时间升序
+ CategorySortTypeWatchAsc CategorySortType = 4 // 观看次数升序
+ CategorySortTypeLikeAsc CategorySortType = 5 // 点赞(收藏)次数升序
+ CategorySortTypeUpdateTimeDesc CategorySortType = 6 // 更新时间降序
+ CategorySortTypeUpdateTimeAsc CategorySortType = 7 // 更新时间升序
+ CategorySortTypeSortType CategorySortType = 8 // 自定义排序
+)
diff --git a/pub/pubdef/translation.go b/pub/pubdef/translation.go
new file mode 100644
index 0000000..2cbc905
--- /dev/null
+++ b/pub/pubdef/translation.go
@@ -0,0 +1,26 @@
+package pubdef
+
+// TranslationType 导航类型
+type TranslationType int64
+
+const (
+ TranslationTypeUrl TranslationType = 1 // URL模式
+ TranslationTypeSub TranslationType = 2 // 二级导航
+ TranslationTypeBottom TranslationType = 3 // 底部导航
+)
+
+const (
+ TranslationPath_Video = "/video" // 视频首页
+ TranslationPath_VideoAll = "/video/all" // 全部视频
+ TranslationPath_VideoRank = "/video/rank" // 视频排行榜
+ TranslationPath_ActressRank = "/actress/rank" // 女优排行榜
+ TranslationPath_VideoMore = "/video/more/%d" // 视频分类
+)
+
+// FixedNavItem 固定导航路径对应的名称(与路径常量一一对应,供各端统一使用)
+const (
+ TranslationName_Video = "视频首页"
+ TranslationName_VideoAll = "全部视频"
+ TranslationName_VideoRank = "视频排行榜"
+ TranslationName_ActressRank = "女优排行榜"
+)
diff --git a/pub/pubdef/user.go b/pub/pubdef/user.go
new file mode 100644
index 0000000..f78382c
--- /dev/null
+++ b/pub/pubdef/user.go
@@ -0,0 +1,70 @@
+package pubdef
+
+type UserStatus int32
+
+const (
+ UserStatusNormal UserStatus = 0 // 正常
+ UserStatusDisable UserStatus = 1 // 封禁
+ UserStatusSlient UserStatus = 2 // 禁言
+)
+
+var UserStatusMap = map[UserStatus]string{
+ UserStatusNormal: "正常",
+ UserStatusDisable: "封禁",
+ UserStatusSlient: "禁止登陆",
+}
+
+type LoginType int32
+
+const (
+ LoginTypeDevice LoginType = 0 + iota // 设备登陆
+ LoginTypeMobile // 手机登陆
+ LoginTypeQrCode // 二维码登陆
+ LoginTypeAccount // 账号登陆
+)
+
+// SysType 客户端系统/平台类型
+type SysType string
+
+const (
+ ST_None SysType = ""
+ ST_IOS SysType = "ios"
+ ST_Android SysType = "android"
+ ST_H5 SysType = "h5"
+ ST_PC SysType = "pc"
+ ST_H5IOS SysType = "h5_ios"
+ ST_H5Android SysType = "h5_android"
+ ST_H5PC SysType = "h5_pc"
+ ST_Light SysType = "light"
+ ST_TG SysType = "tg"
+ ST_Web SysType = "web"
+)
+
+func (s SysType) String() string { return string(s) }
+
+// IsH5 判断是否为 H5/Web 端
+func (s SysType) IsH5() bool {
+ return s == ST_H5IOS || s == ST_H5Android || s == ST_H5PC ||
+ s == ST_H5 || s == ST_Light || s == ST_TG || s == ST_Web
+}
+
+// IsIOS 判断是否为 iOS 原生
+func (s SysType) IsIOS() bool { return s == ST_IOS }
+
+// IsAndroid 判断是否为 Android 原生
+func (s SysType) IsAndroid() bool { return s == ST_Android }
+
+// IsWeb 判断是否为 Web 端
+func (s SysType) IsWeb() bool { return s == ST_Web }
+
+// SysTypeFromString 将字符串转换为 SysType
+func SysTypeFromString(s string) SysType {
+ switch SysType(s) {
+ case ST_IOS, ST_Android, ST_H5, ST_PC,
+ ST_H5IOS, ST_H5Android, ST_H5PC,
+ ST_Light, ST_TG, ST_Web:
+ return SysType(s)
+ default:
+ return ST_None
+ }
+}
diff --git a/pub/redis/List.go b/pub/redis/List.go
new file mode 100644
index 0000000..db85e71
--- /dev/null
+++ b/pub/redis/List.go
@@ -0,0 +1,95 @@
+package redis
+
+import (
+ "github.com/go-redis/redis"
+ "time"
+)
+
+// List represents a redis list.
+type List struct {
+ key string
+ client *redis.Client
+}
+
+// NewList creates a new redis list instance.
+func NewList(client *redis.Client, key string) *List {
+ return &List{
+ key: key,
+ client: client,
+ }
+}
+
+// Key returns the key of the list.
+func (l *List) Key() string {
+ return l.key
+}
+
+// Expire sets the expiration time of the list.
+func (l *List) Expire(expiration time.Duration) (bool, error) {
+ return l.client.Expire(l.key, expiration).Result()
+}
+
+// Delete deletes the list.
+func (l *List) Delete() (int64, error) {
+ return l.client.Del(l.key).Result()
+}
+
+// Len returns the length of the list.
+func (l *List) Len() (int64, error) {
+ return l.client.LLen(l.key).Result()
+}
+
+// Push adds a new element to the tail of the list.
+func (l *List) Push(value interface{}) error {
+ return l.client.RPush(l.key, value).Err()
+}
+
+// Pop removes and returns the first element of the list.
+func (l *List) Pop() (string, error) {
+ return l.client.LPop(l.key).Result()
+}
+
+// PushFront adds a new element to the head of the list.
+func (l *List) PushFront(value interface{}) error {
+ return l.client.LPush(l.key, value).Err()
+}
+
+// PopBack removes and returns the last element of the list.
+func (l *List) PopBack() (string, error) {
+ return l.client.RPop(l.key).Result()
+}
+
+// Index returns the element at the specified index.
+func (l *List) Index(index int64) (string, error) {
+ return l.client.LIndex(l.key, index).Result()
+}
+
+// Range returns a range of elements from the list.
+func (l *List) Range(start, stop int64) ([]string, error) {
+ return l.client.LRange(l.key, start, stop).Result()
+}
+
+// Trim removes elements from the list.
+func (l *List) Trim(start, stop int64) error {
+ return l.client.LTrim(l.key, start, stop).Err()
+}
+
+// Set sets the element at the specified index.
+func (l *List) Set(index int64, value interface{}) error {
+ return l.client.LSet(l.key, index, value).Err()
+}
+
+// InsertBefore inserts a new element before the specified element.
+func (l *List) InsertBefore(pivot, value interface{}) error {
+ return l.client.LInsertBefore(l.key, pivot, value).Err()
+}
+
+// InsertAfter inserts a new element after the specified element.
+func (l *List) InsertAfter(pivot, value interface{}) error {
+ return l.client.LInsertAfter(l.key, pivot, value).Err()
+}
+
+// Remove removes all occurrences of the specified element.
+func (l *List) Remove(count int64, value interface{}) (int64, error) {
+ return l.client.LRem(l.key, count, value).Result()
+}
diff --git a/pub/redis/channeldef.go b/pub/redis/channeldef.go
new file mode 100644
index 0000000..f76ed5d
--- /dev/null
+++ b/pub/redis/channeldef.go
@@ -0,0 +1,75 @@
+package redis
+
+import "time"
+
+// RCType redis channel type
+type RCType string
+
+const (
+ // config
+ RCT_cfg_all RCType = "config.*"
+ // tabkind 配置改变
+ RCT_cfg_tabkind_change RCType = "config.tabkindchange"
+ // mediatag 配置改变
+ RCT_cfg_mediatag_change RCType = "config.mediatagchange"
+ // mediacat 配置改变
+ RCT_cfg_mediacat_change RCType = "config.mediacatchange"
+ // mediatopic 配置改变
+ RCT_cfg_mediatopic_change RCType = "config.mediatopicchange"
+ // mediacolumn 配置改变
+ RCT_cfg_mediacolumn_change RCType = "config.mediacolumnchange"
+ // syscfg 系统配置改变
+ RCT_cfg_syscfg_change RCType = "config.syscfgchange"
+
+ // 广告
+ RCT_ad_all RCType = "ad.*"
+ // 广告配置改变
+ RCT_ad_cfgchange RCType = "ad.cfgchange"
+
+ // 系统通知
+ RCT_blt_all RCType = "blt.*"
+ // 系统通知
+ RCT_blt_cfgchange RCType = "blt.cfgchange"
+
+ //media
+ RCT_media_all RCType = "media.*"
+ //media 信息改变
+ RCT_media_change RCType = "media.change"
+ //media 信息添加
+ RCT_media_add RCType = "media.add"
+ //media 信息删除
+ RCT_media_del RCType = "media.del"
+ //media 相关排行榜改变
+ RCT_media_topchange RCType = "media.topchange"
+
+ //user
+ RCT_user_all RCType = "user.*"
+ //user 信息改变
+ RCT_user_change RCType = "user.change"
+ //user 信息添加
+ RCT_user_add RCType = "user.add"
+ //user 信息删除
+ RCT_user_del RCType = "user.del"
+ //user 相关排行榜改变
+ RCT_user_topchange RCType = "user.topchange"
+
+ //视频推荐
+ RCT_recommend_all RCType = "recommend.*"
+ //博主推荐
+ RCT_recommend_blogger_all RCType = "recommend.bloggerALL"
+ // 视频每日推荐
+ RCT_recommend_media_all RCType = "recommend.mediaAll"
+
+ //tg消息
+ RCT_tgmsg_all RCType = "tgmsg.*"
+ RCT_tgmsg_send_msg RCType = "tgmsg.sendmsg"
+
+ //vipcard
+ RCT_vipcard_all RCType = "vipcard.*"
+ RCT_vipcard_change RCType = "vipcard.change"
+
+ NameSource = "name:source" //姓名池
+ NameSourceExpire = 2 * 24 * time.Hour
+ CommunicateServerChannel = "comm:lld#server"
+ CommunicateClientChannel = "comm:lld#client"
+)
diff --git a/pub/redis/hash.go b/pub/redis/hash.go
new file mode 100644
index 0000000..c0f5d0c
--- /dev/null
+++ b/pub/redis/hash.go
@@ -0,0 +1,112 @@
+package redis
+
+import (
+ "time"
+
+ "github.com/go-redis/redis"
+)
+
+//key: {name:value, name:value, name:value}
+
+type Hash struct {
+ key string
+ client *redis.Client
+}
+
+func NewHash(client *redis.Client, key string) *Hash {
+ return &Hash{
+ key: key,
+ client: client,
+ }
+}
+
+func (s *Hash) Key() string {
+ return s.key
+}
+
+// Expire 设置集合过期时间
+func (s *Hash) Expire(expiration time.Duration) (bool, error) {
+ return s.client.Expire(s.key, expiration).Result()
+}
+
+// Delete 删除一个或多个 Name
+func (s *Hash) Delete(names ...string) (int64, error) {
+ return s.client.HDel(s.key, names...).Result()
+}
+
+// Exists 检测Name是否在哈希表中
+func (s *Hash) Exists(name string) (bool, error) {
+ return s.client.HExists(s.key, name).Result()
+}
+
+// NameValues 获取所有哈希表数据
+func (s *Hash) NameValues() (map[string]string, error) {
+ return s.client.HGetAll(s.key).Result()
+}
+
+// Names 获取所有哈希表的 Name
+func (s *Hash) Names() ([]string, error) {
+ return s.client.HKeys(s.key).Result()
+}
+
+// Values 获取所有哈希表的 Total
+func (s *Hash) Values() ([]string, error) {
+ return s.client.HVals(s.key).Result()
+}
+
+// SetNX 当name不存在时才向哈希表中设置一对Name:Total
+func (s *Hash) SetNX(name string, value interface{}) (bool, error) {
+ return s.client.HSetNX(s.key, name, value).Result()
+}
+
+// Set 向哈希表中设置一对Name:Total
+func (s *Hash) Set(name string, value interface{}) (bool, error) {
+ return s.client.HSet(s.key, name, value).Result()
+}
+
+// Get 获取哈希表中Name对应的Value
+func (s *Hash) Get(name string) (string, error) {
+ return s.client.HGet(s.key, name).Result()
+}
+
+// GetResult 获取哈希表中Name对应的Value
+func (s *Hash) GetResult(name string) *redis.StringCmd {
+ return s.client.HGet(s.key, name)
+}
+
+// IncInt 返回累加之后的新值
+func (s *Hash) IncInt(name string, value int64) (int64, error) {
+ return s.client.HIncrBy(s.key, name, value).Result()
+}
+
+// IncFloat 返回累加之后的新值
+func (s *Hash) IncFloat(name string, value float64) (float64, error) {
+ return s.client.HIncrByFloat(s.key, name, value).Result()
+}
+
+// Len 返回哈希表的大小
+func (s *Hash) Len() (int64, error) {
+ return s.client.HLen(s.key).Result()
+}
+
+// Scan 扫描哈希表数据
+func (s *Hash) Scan(match string, piece int64, callback func(name, value string)) error {
+ proc := func(l []string) {
+ for i := 0; i+1 < len(l); i += 2 {
+ callback(l[i], l[i+1])
+ }
+ }
+ lst, cur, err := s.client.HScan(s.key, 0, match, piece).Result()
+ if err != nil {
+ return err
+ }
+ proc(lst)
+ for cur > 0 {
+ lst, cur, err = s.client.HScan(s.key, cur, match, piece).Result()
+ if err != nil {
+ return err
+ }
+ proc(lst)
+ }
+ return nil
+}
diff --git a/pub/redis/manager.go b/pub/redis/manager.go
new file mode 100644
index 0000000..79b1d3e
--- /dev/null
+++ b/pub/redis/manager.go
@@ -0,0 +1,177 @@
+package redis
+
+import (
+ "fmt"
+ "regexp"
+ "sync"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/go-redis/redis"
+)
+
+type SubscribeCallback func(channel RCType, message string)
+
+var (
+ redisCli *redis.Client
+ opts Options
+
+ //订阅参数
+ pubsub *redis.PubSub
+ subCBMap map[RCType]SubscribeCallback
+ cbMapLock sync.RWMutex
+)
+
+func Client() *redis.Client {
+ return redisCli
+}
+
+func Disconnect() {
+ if redisCli != nil {
+ if pubsub != nil {
+ pubsub.Close()
+ }
+ redisCli.Close()
+ redisCli = nil
+ }
+}
+
+func Init(opts Options) error {
+ c := redis.NewClient(
+ &redis.Options{
+ Addr: opts.Addr,
+ Password: opts.Psw,
+ DB: opts.Db,
+ },
+ )
+
+ if err := c.Ping().Err(); err != nil {
+ log.Error(fmt.Sprintf("[error] ping redis err %+v\n", err))
+ return err
+ }
+ redisCli = c
+
+ pubsub = nil
+ if opts.Subscribe {
+ subCBMap = make(map[RCType]SubscribeCallback)
+ pubsub = redisCli.Subscribe()
+ //接受发布消息
+ go ReceiveMessage()
+ }
+
+ log.Info("redis:Init Ok!")
+ return nil
+}
+
+// OnMessage 订阅消息回调
+func OnMessage(channel RCType, message string) {
+ cbMapLock.RLock()
+ defer cbMapLock.RUnlock()
+ flag := false
+ for k, v := range subCBMap {
+ reg := regexp.MustCompile(string(k))
+ //log.Info("test", log.Any("k", k), log.Any("channelchannel", channel))
+ if b := reg.MatchString(string(channel)); b {
+ v(channel, message)
+ flag = true
+ }
+ }
+ if !flag {
+ log.Warn("(redis:OnMessage) callback doesn't exist!", log.Any("channel", channel))
+ }
+}
+
+// ReceiveMessage 接受发布消息
+func ReceiveMessage() {
+ for {
+ msg, err := pubsub.Receive()
+ if err != nil {
+ fmt.Println("test1", err)
+ return
+ }
+
+ switch msg := msg.(type) {
+ case *redis.Subscription:
+ // Ignore.
+ log.Info("(redis:ReceiveMessage) subscribe success!", log.Any("channel", msg.Channel))
+ case *redis.Pong:
+ // Ignore.
+ fmt.Println("Pong")
+ case *redis.Message:
+ channel := msg.Channel
+ message := msg.Payload
+ OnMessage(RCType(channel), message)
+ default:
+ log.Error("(redis:ReceiveMessage) msg!", log.Any("msg", msg))
+ return
+ }
+ }
+}
+
+// Subscribe 订阅一个消息
+func Subscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:Subscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.Subscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:Subscribe) Subscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ subCBMap[channel] = cb
+ return nil
+}
+
+// UnSubscribe 取消订阅一个消息
+func UnSubscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:UnSubscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.Unsubscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:UnSubscribe) UnSubscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ delete(subCBMap, channel)
+ return nil
+}
+
+// PSubscribe 模式匹配消息
+func PSubscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:PSubscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.PSubscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:PSubscribe) PSubscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ subCBMap[channel] = cb
+ return nil
+}
+
+// PUnsubscribe 模式取消匹配消息
+func PUnsubscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:PUnsubscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.PUnsubscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:PUnsubscribe) PUnsubscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ delete(subCBMap, channel)
+ return nil
+}
diff --git a/pub/redis/options.go b/pub/redis/options.go
new file mode 100644
index 0000000..fed9708
--- /dev/null
+++ b/pub/redis/options.go
@@ -0,0 +1,9 @@
+package redis
+
+// Redis 配置项
+type Options struct {
+ Addr string `json:"addr"` // 地址
+ Psw string `json:"psw"` // 密码
+ Db int `json:"db"` // 库
+ Subscribe bool `json:"subscribe"` // 是否有订阅功能
+}
diff --git a/pub/redis/redis.go b/pub/redis/redis.go
new file mode 100644
index 0000000..d504209
--- /dev/null
+++ b/pub/redis/redis.go
@@ -0,0 +1,275 @@
+package redis
+
+import (
+ "errors"
+ "fmt"
+ "time"
+ sErr "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+
+ goredis "github.com/go-redis/redis"
+)
+
+type ZMember = goredis.Z
+
+// Set 设置键值对
+func Set(key string, value interface{}, expireTime time.Duration) (err error) {
+ if err = redisCli.Set(key, value, expireTime).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// Incr 增加1
+func Incr(key string, expireTime time.Duration) (val int64, err error) {
+ val, err = redisCli.Incr(key).Result()
+ if err != nil {
+ return
+ }
+ duration := redisCli.TTL(key).Val()
+ //log.Info("Incr Test", log.Any("duration", duration))
+ if duration.Seconds() == -1 {
+ redisCli.Expire(key, expireTime)
+ }
+ return
+}
+
+func SAdd(key string, members ...interface{}) (err error) {
+ _, err = redisCli.SAdd(key, members...).Result()
+ return
+}
+
+// 无序集合操作 随机删除集合中某一元素
+func SPop(key string, members ...interface{}) (string, error) {
+ val, err := redisCli.SPop(key).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("SPop failed %+v:", err))
+ }
+ return val, err
+}
+
+func SRem(key string, members ...interface{}) (err error) {
+ if err = redisCli.SRem(key, members...).Err(); err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// ScanSlice 返回集合成员
+func SScanSlice(key string, container interface{}) (err error) {
+ err = redisCli.SMembers(key).ScanSlice(container)
+ return err
+}
+
+// SMembers
+func SMembers(key string) (list []string, err error) {
+ result := redisCli.SMembers(key)
+ if result.Err() != nil {
+ err = sErr.MakeWithErr(sErr.RedisGet, result.Err())
+ return
+ }
+ list = result.Val()
+ return
+}
+
+// RPush
+func RPush(key string, value string) error {
+ if key == "" {
+ return errors.New("key should not empty")
+ }
+ if err := redisCli.RPush(key, value).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisRPush, err)
+ }
+ return nil
+}
+
+// RPush
+func RPushValues(key string, value ...interface{}) error {
+ if key == "" {
+ return errors.New("key should not empty")
+ }
+ if err := redisCli.RPush(key, value...).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisRPush, err)
+ }
+ return nil
+}
+
+// LPop
+func LPop(key string) (*string, error) {
+ if key == "" {
+ return nil, errors.New("key should not empty")
+ }
+
+ cmd := redisCli.LPop(key)
+ if cmd.Err() != nil {
+ return nil, sErr.MakeWithErr(sErr.RedisLPop, cmd.Err())
+ }
+
+ data, err := cmd.Result()
+ if err != nil {
+ if err == goredis.Nil {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+// Get 根据键 获取值
+func Get(key string) (*string, error) {
+ if key == "" {
+ return nil, errors.New("key should not empty")
+ }
+ data, err := redisCli.Get(key).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ return nil, nil
+ }
+ return nil, err
+ } else {
+ return &data, nil
+ }
+}
+
+// Get 根据键 获取值
+func GetBytes(key string) (bytes []byte, err error) {
+ bytes, err = redisCli.Get(key).Bytes()
+ if err != nil {
+ return nil, sErr.MakeWithErr(sErr.RedisGet, err)
+ }
+ return
+}
+
+// Del 删除
+func Del(keys ...string) (err error) {
+ if err = redisCli.Del(keys...).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisDel, err)
+ }
+ return
+}
+
+// Exists keys exists?
+func Exists(keys ...string) (has bool, err error) {
+ intcmd := redisCli.Exists(keys...)
+ if err = intcmd.Err(); err != nil {
+ return
+ }
+ has = intcmd.Val() > 0
+ return
+}
+
+func Keys(pattern string) (keys []string, err error) {
+ list := redisCli.Keys(pattern)
+ keys = list.Val()
+ err = list.Err()
+ return
+}
+
+// Setnx 设置键值对
+func Setnx(key string, value interface{}, expirTime time.Duration) (err error) {
+ if err = redisCli.SetNX(key, value, expirTime).Err(); err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// Setnx 设置键值对 正确的封装
+func Setnx_NewOK(key string, value interface{}, expirTime time.Duration) (v bool, err error) {
+ cmd := redisCli.SetNX(key, value, expirTime)
+ return cmd.Val(), cmd.Err()
+}
+
+func Expire(key string, expirTime time.Duration) (err error) {
+ if err = redisCli.Expire(key, expirTime).Err(); err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// Redis `SRANDMEMBER key count` command.
+func SRandMemberN(key string, count int64) (list []string, err error) {
+ result := redisCli.SRandMemberN(key, count)
+ if result.Err() != nil {
+ return
+ }
+ list = result.Val()
+ return
+}
+
+// 有序集合操作 添加多个有序集合
+func ZAdd(key string, z ...ZMember) (int64, error) {
+ count, err := redisCli.ZAdd(key, z...).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZAdd failed %+v:", err))
+ }
+ return count, err
+}
+
+// 有序集合 数量
+func ZCard(key string) (int64, error) {
+ count, err := redisCli.ZCard(key).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZCard failed %+v:", err))
+ }
+ return count, err
+}
+
+// 返回区间成员
+func ZRange(key string, start, end int64) ([]string, error) {
+ data, err := redisCli.ZRange(key, start, end).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZRange failed %+v:", err))
+ }
+ return data, err
+}
+
+// 按照元素排序 删除指定排名的元素
+func ZRemRangeByRank(key string, start, end int64) (int64, error) {
+ count, err := redisCli.ZRemRangeByRank(key, start, end).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZRemRangeByRank failed %+v:", err))
+ }
+ return count, err
+}
+
+// 按照元素排序 删除按指定的分数区间的的元素
+func ZRemRangeByScore(key, start, end string) (int64, error) {
+ count, err := redisCli.ZRemRangeByScore(key, start, end).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZRemRangeByScore failed %+v:", err))
+ }
+ return count, err
+}
+
+// Publish 发布消息
+func Publish(channel RCType, message string) error {
+ if err := redisCli.Publish(string(channel), message).Err(); err != nil {
+ log.Error(fmt.Sprintf("Publish failed %+v:", err))
+ return err
+ }
+ return nil
+}
+
+func SendMsgToTg(message string) error {
+ return Publish(RCT_tgmsg_send_msg, message)
+}
+
+// SCard 发布消息
+func SCard(key string) (int64, error) {
+ count, err := redisCli.SCard(key).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("SCard failed %+v:", err))
+ }
+
+ return count, err
+}
+
+// 获取key的剩余生存时间
+func TTL(key string) time.Duration {
+ durCmd := redisCli.TTL(key)
+ if durCmd != nil {
+ return durCmd.Val()
+ }
+ return 0
+}
diff --git a/pub/redis/zset.go b/pub/redis/zset.go
new file mode 100644
index 0000000..c3d1e7c
--- /dev/null
+++ b/pub/redis/zset.go
@@ -0,0 +1,136 @@
+package redis
+
+import (
+ "github.com/go-redis/redis"
+ "strconv"
+ "time"
+)
+
+type Z = redis.Z
+
+type ZSet struct {
+ key string
+ client *redis.Client
+}
+
+func NewZSet(client *redis.Client, key string) *ZSet {
+ return &ZSet{
+ key: key,
+ client: client,
+ }
+}
+
+func (s *ZSet) Key() string {
+ return s.key
+}
+
+// Expire 设置集合过期时间
+func (s *ZSet) Expire(expiration time.Duration) (bool, error) {
+ return s.client.Expire(s.key, expiration).Result()
+}
+
+// Add 添加一个或多个成员到有序集合
+func (s *ZSet) Add(members ...Z) (int64, error) {
+ return s.client.ZAdd(s.key, members...).Result()
+}
+
+// Count 返回有序集合在指定分数范围内的成员数量
+func (s *ZSet) Count(min, max float64) (int64, error) {
+ return s.client.ZCount(s.key, strconv.FormatFloat(min, 'f', -1, 64), strconv.FormatFloat(max, 'f', -1, 64)).Result()
+}
+
+// Score 获取成员的分数
+func (s *ZSet) Score(member string) (float64, error) {
+ return s.client.ZScore(s.key, member).Result()
+}
+
+// Rank 获取成员的排名(从小到大,0开始)
+func (s *ZSet) Rank(member string) (int64, error) {
+ return s.client.ZRank(s.key, member).Result()
+}
+
+// RevRank 获取成员的排名(从大到小,0开始)
+func (s *ZSet) RevRank(member string) (int64, error) {
+ return s.client.ZRevRank(s.key, member).Result()
+}
+
+// Range 返回指定排名范围的成员
+func (s *ZSet) Range(start, stop int64) ([]string, error) {
+ return s.client.ZRange(s.key, start, stop).Result()
+}
+
+// RangeWithScores 返回指定排名范围的成员和分数
+func (s *ZSet) RangeWithScores(start, stop int64) ([]Z, error) {
+ return s.client.ZRangeWithScores(s.key, start, stop).Result()
+}
+
+// RangeByScore 返回指定分数范围的成员
+func (s *ZSet) RangeByScore(min, max float64) ([]string, error) {
+ opt := redis.ZRangeBy{
+ Min: strconv.FormatFloat(min, 'f', -1, 64),
+ Max: strconv.FormatFloat(max, 'f', -1, 64),
+ }
+ return s.client.ZRangeByScore(s.key, opt).Result()
+}
+
+// RangeByScoreWithScores 返回指定分数范围的成员和分数
+func (s *ZSet) RangeByScoreWithScores(min, max float64) ([]Z, error) {
+ opt := redis.ZRangeBy{
+ Min: strconv.FormatFloat(min, 'f', -1, 64),
+ Max: strconv.FormatFloat(max, 'f', -1, 64),
+ }
+ return s.client.ZRangeByScoreWithScores(s.key, opt).Result()
+}
+
+// Remove 删除一个或多个成员
+func (s *ZSet) Remove(members ...string) (int64, error) {
+ return s.client.ZRem(s.key, members).Result()
+}
+
+// RemoveRangeByRank 删除指定排名范围的成员
+func (s *ZSet) RemoveRangeByRank(start, stop int64) (int64, error) {
+ return s.client.ZRemRangeByRank(s.key, start, stop).Result()
+}
+
+// RemoveRangeByScore 删除指定分数范围的成员
+func (s *ZSet) RemoveRangeByScore(min, max float64) (int64, error) {
+ return s.client.ZRemRangeByScore(s.key, strconv.FormatFloat(min, 'f', -1, 64), strconv.FormatFloat(max, 'f', -1, 64)).Result()
+}
+
+// Len 返回有序集合的成员数
+func (s *ZSet) Len() (int64, error) {
+ return s.client.ZCard(s.key).Result()
+}
+
+// IncScore 为成员的分数加上增量
+func (s *ZSet) IncScore(member string, increment float64) (float64, error) {
+ return s.client.ZIncrBy(s.key, increment, member).Result()
+}
+
+// Scan 扫描有序集合
+func (s *ZSet) Scan(match string, count int64, callback func(member string, score float64) error) error {
+ cursor := uint64(0)
+ for {
+ keys, newCursor, err := s.client.ZScan(s.key, cursor, match, count).Result()
+ if err != nil {
+ return err
+ }
+
+ for i := 0; i < len(keys); i += 2 {
+ score, err := strconv.ParseFloat(keys[i+1], 64)
+ if err != nil {
+ return err
+ }
+ err = callback(keys[i], score)
+ if err != nil {
+ return err
+ }
+ }
+
+ if newCursor == 0 {
+ break
+ }
+ cursor = newCursor
+ }
+ return nil
+}
diff --git a/pub/seo/googleindex/googleindex.go b/pub/seo/googleindex/googleindex.go
new file mode 100644
index 0000000..49cf609
--- /dev/null
+++ b/pub/seo/googleindex/googleindex.go
@@ -0,0 +1,187 @@
+// Package googleindex 对接 Google Indexing API,在文章发布/更新/删除时推送具体页面 URL。
+// 需要在 Google Cloud Console 创建服务账号并开启 Indexing API,
+// 同时在 Search Console 中把该服务账号邮箱添加为站点「所有者」。
+package googleindex
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+
+ "google.golang.org/api/googleapi"
+ "google.golang.org/api/indexing/v3"
+ "google.golang.org/api/option"
+)
+
+// 推送动作类型
+const (
+ TypeUpdated = "URL_UPDATED" // 新增或更新
+ TypeDeleted = "URL_DELETED" // 已删除
+)
+
+// defaultDailyQuota Google Indexing API 默认每天 200 个 URL 请求
+const defaultDailyQuota = int64(200)
+
+// ErrQuotaExceeded 配额已用尽,可能来自本地计数拦截,也可能是 Google 返回的 429。
+// 两者都只代表「现在不能推」,调用方应保留任务等下次,而不是计为失败重试。
+var ErrQuotaExceeded = errors.New("googleindex: daily quota exceeded")
+
+// ErrNotEnabled 未启用或未初始化
+var ErrNotEnabled = errors.New("googleindex: not enabled")
+
+// Client Indexing API 客户端,内置每日配额保护
+type Client struct {
+ svc *indexing.Service
+ quota *quotaGuard
+}
+
+var (
+ defaultClient *Client
+ defaultMu sync.RWMutex
+)
+
+// Init 初始化全局客户端,未启用时不报错,仅记录日志
+func Init(ctx context.Context) {
+ client, err := NewClient(ctx)
+ if err != nil {
+ log.Error("googleindex: init failed", log.E(err))
+ return
+ }
+ defaultMu.Lock()
+ defaultClient = client
+ defaultMu.Unlock()
+ log.Info("googleindex: ready", log.Any("dailyQuota", client.quota.limit))
+}
+
+// NewClient 依据配置创建客户端
+func NewClient(ctx context.Context) (*Client, error) {
+ credentials, err := loadCredentials()
+ if err != nil {
+ return nil, err
+ }
+ svc, err := indexing.NewService(ctx, option.WithCredentialsJSON(credentials))
+ if err != nil {
+ return nil, fmt.Errorf("create indexing service failed: %w", err)
+ }
+ limit := defaultDailyQuota
+ return &Client{svc: svc, quota: "aGuard{limit: limit}}, nil
+}
+
+func loadCredentials() ([]byte, error) {
+ credentialsFile := "composed-anvil-470512-r8-fd674ac0d7dc.json"
+ b, err := os.ReadFile(credentialsFile)
+ if err != nil {
+ return nil, fmt.Errorf("read credentials file %s failed: %w", credentialsFile, err)
+ }
+ return b, nil
+}
+
+// Publish 使用全局客户端推送单个 URL
+func Publish(ctx context.Context, pageURL, notifyType string) (*indexing.PublishUrlNotificationResponse, error) {
+ defaultMu.RLock()
+ client := defaultClient
+ defaultMu.RUnlock()
+ if client == nil {
+ return nil, ErrNotEnabled
+ }
+ return client.Publish(ctx, pageURL, notifyType)
+}
+
+// Publish 推送单个 URL,notifyType 取 TypeUpdated / TypeDeleted
+func (c *Client) Publish(ctx context.Context, pageURL, notifyType string) (*indexing.PublishUrlNotificationResponse, error) {
+ if strings.TrimSpace(pageURL) == "" {
+ return nil, errors.New("googleindex: empty url")
+ }
+ if notifyType != TypeUpdated && notifyType != TypeDeleted {
+ return nil, fmt.Errorf("googleindex: invalid notify type %s", notifyType)
+ }
+ if !c.quota.acquire() {
+ return nil, ErrQuotaExceeded
+ }
+
+ resp, err := c.svc.UrlNotifications.Publish(&indexing.UrlNotification{
+ Url: pageURL,
+ Type: notifyType,
+ }).Context(ctx).Do()
+ if err != nil {
+ // Google 已明确告知配额用尽,说明本地计数比实际用量宽松:
+ // 本地日界按服务器时区计算、且计数在进程重启后归零,都可能早于 Google 放行。
+ // 此时把本地计数拉满,避免后续请求继续无效地打过去。
+ if isQuotaError(err) {
+ c.quota.exhaust()
+ return resp, fmt.Errorf("%w: %v", ErrQuotaExceeded, err)
+ }
+ // 请求已消耗配额但失败,退回本地计数,避免本地计数与实际用量偏差过大
+ c.quota.release()
+ return resp, err
+ }
+ return resp, nil
+}
+
+// isQuotaError 判断错误是否为配额或频率限制。
+// 这类错误只代表「现在不能推」,调用方应保留任务等下次,而不是计为失败重试。
+// 注意 403 还用于站点所有权不足等需要人工介入的场景,因此只在 reason 明确是配额时才算。
+func isQuotaError(err error) bool {
+ var gerr *googleapi.Error
+ if !errors.As(err, &gerr) {
+ return false
+ }
+ if gerr.Code == http.StatusTooManyRequests {
+ return true
+ }
+ for _, item := range gerr.Errors {
+ switch item.Reason {
+ case "rateLimitExceeded", "quotaExceeded", "userRateLimitExceeded":
+ return true
+ }
+ }
+ return false
+}
+
+// quotaGuard 按自然日计数的配额保护。
+// 计数仅存于内存,进程重启会归零;真正超配额时 Google 会返回 429,由调用方按失败重试处理。
+type quotaGuard struct {
+ mu sync.Mutex
+ limit int64
+ day string
+ used int64
+}
+
+func (q *quotaGuard) acquire() bool {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ today := time.Now().Format("2006-01-02")
+ if q.day != today {
+ q.day = today
+ q.used = 0
+ }
+ if q.used >= q.limit {
+ return false
+ }
+ q.used++
+ return true
+}
+
+func (q *quotaGuard) release() {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ if q.used > 0 {
+ q.used--
+ }
+}
+
+// exhaust 把当天计数置满,用于 Google 已返回配额超限时同步本地状态。
+// 代价是本地要等到下一个自然日才放行,可能晚于 Google 实际的配额重置时刻;
+// 换来的是不再持续发送必定失败的请求。
+func (q *quotaGuard) exhaust() {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ q.day = time.Now().Format("2006-01-02")
+ q.used = q.limit
+}
diff --git a/pub/seo/indexnow/submitter.go b/pub/seo/indexnow/submitter.go
new file mode 100644
index 0000000..1804fe6
--- /dev/null
+++ b/pub/seo/indexnow/submitter.go
@@ -0,0 +1,246 @@
+// Package indexnow 实现 IndexNow 协议提交,推送到官方入口后由 Bing / Yandex 等共享数据。
+// 调用方是 web 进程的 seopush,在文章发布 / 更新 / 删除时推送对应页面地址。
+package indexnow
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+)
+
+// DefaultKey IndexNow 默认密钥,与站点根目录下的 {key}.txt 验证文件对应
+const DefaultKey = "6d41643810d540ce924edb982c3d3171"
+
+// DefaultSearchEngine 官方入口,会自动分发给 Bing、Yandex 等搜索引擎
+const DefaultSearchEngine = "https://api.indexnow.org"
+
+// IndexNowRequest 提交请求结构体
+type IndexNowRequest struct {
+ Host string `json:"host"`
+ Key string `json:"key"`
+ KeyLocation string `json:"keyLocation,omitempty"`
+ URLList []string `json:"urlList"`
+}
+
+// IndexNowConfig 配置结构体
+type IndexNowConfig struct {
+ Key string
+ Host string
+ KeyLocation string
+ SearchEngines []string
+ RetryCount int
+ RetryDelay time.Duration
+ // BatchInterval 多批次之间的间隔,避免触发频率限制;单批提交不生效
+ BatchInterval time.Duration
+}
+
+// BuildConfig 依据站点地址推导 IndexNow 配置
+func BuildConfig(webAddr string) (*IndexNowConfig, error) {
+ host, keyLocation, err := hostFromWebAddr(webAddr, DefaultKey)
+ if err != nil {
+ return nil, err
+ }
+ return &IndexNowConfig{
+ Key: DefaultKey,
+ Host: host,
+ KeyLocation: keyLocation,
+ SearchEngines: []string{DefaultSearchEngine},
+ RetryCount: 3,
+ RetryDelay: 2 * time.Second,
+ BatchInterval: 60 * time.Second,
+ }, nil
+}
+
+func hostFromWebAddr(webAddr, key string) (host, keyLocation string, err error) {
+ normalized := webAddr
+ if !strings.HasPrefix(normalized, "http://") && !strings.HasPrefix(normalized, "https://") {
+ normalized = "https://" + normalized
+ }
+ u, parseErr := url.Parse(normalized)
+ if parseErr != nil {
+ return "", "", parseErr
+ }
+ host = u.Hostname()
+ if host == "" {
+ return "", "", fmt.Errorf("invalid webAddr: %s", webAddr)
+ }
+ base := strings.TrimSuffix(normalized, "/")
+ keyLocation = fmt.Sprintf("%s/%s.txt", base, key)
+ return host, keyLocation, nil
+}
+
+// BatchIndexNowSubmitter 批量提交器
+type BatchIndexNowSubmitter struct {
+ config *IndexNowConfig
+ client *http.Client
+}
+
+// NewBatchIndexNowSubmitter 创建新的批量提交器
+func NewBatchIndexNowSubmitter(config *IndexNowConfig) *BatchIndexNowSubmitter {
+ if config.RetryCount == 0 {
+ config.RetryCount = 3
+ }
+ if config.RetryDelay == 0 {
+ config.RetryDelay = 1 * time.Second
+ }
+ if config.BatchInterval == 0 {
+ config.BatchInterval = 60 * time.Second
+ }
+
+ return &BatchIndexNowSubmitter{
+ config: config,
+ client: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }
+}
+
+// SubmitBatch 批量提交URL,返回成功提交的 URL 列表
+func (s *BatchIndexNowSubmitter) SubmitBatch(urls []string) *BatchResult {
+ result := &BatchResult{
+ TotalURLs: len(urls),
+ SuccessCount: 0,
+ FailCount: 0,
+ Errors: make([]string, 0),
+ SuccessURLs: make([]string, 0),
+ StatusCodes: make([]int, 0),
+ }
+
+ if len(urls) == 0 {
+ return result
+ }
+
+ // 分批处理,每批最多100个URL(避免单次请求过大)
+ const batchSize = 100
+ batches := s.splitIntoBatches(urls, batchSize)
+ for batchIndex, batchURLs := range batches {
+ codes, err := s.submitWithRetry(batchURLs)
+ result.StatusCodes = append(result.StatusCodes, codes...)
+ if err != nil {
+ result.FailCount += len(batchURLs)
+ result.Errors = append(result.Errors,
+ fmt.Sprintf("批次 %d 提交失败: %v", batchIndex+1, err))
+ } else {
+ result.SuccessCount += len(batchURLs)
+ result.SuccessURLs = append(result.SuccessURLs, batchURLs...)
+ }
+ if batchIndex < len(batches)-1 {
+ time.Sleep(s.config.BatchInterval)
+ }
+ }
+ return result
+}
+
+// submitWithRetry 带重试的提交,返回各引擎 HTTP 状态码
+func (s *BatchIndexNowSubmitter) submitWithRetry(urls []string) (statusCodes []int, err error) {
+ var lastError error
+ var lastCodes []int
+
+ for attempt := 1; attempt <= s.config.RetryCount; attempt++ {
+ codes, submitErr := s.submitToAllEngines(urls)
+ lastCodes = codes
+ if submitErr != nil {
+ lastError = submitErr
+ if attempt < s.config.RetryCount {
+ log.Warn("indexnow: submit retry",
+ log.Any("attempt", attempt),
+ log.Any("delay", s.config.RetryDelay),
+ log.Any("statusCodes", codes),
+ log.E(submitErr))
+ time.Sleep(s.config.RetryDelay)
+ continue
+ }
+ } else {
+ return codes, nil
+ }
+ }
+
+ return lastCodes, fmt.Errorf("经过 %d 次重试后仍然失败: %v", s.config.RetryCount, lastError)
+}
+
+// submitToAllEngines 向所有搜索引擎提交,返回各引擎 HTTP 状态码
+func (s *BatchIndexNowSubmitter) submitToAllEngines(urls []string) (statusCodes []int, err error) {
+ requestBody := IndexNowRequest{
+ Host: s.config.Host,
+ Key: s.config.Key,
+ KeyLocation: s.config.KeyLocation,
+ URLList: urls,
+ }
+
+ jsonData, err := json.Marshal(requestBody)
+ if err != nil {
+ return nil, fmt.Errorf("JSON编码失败: %v", err)
+ }
+
+ var errors []string
+ statusCodes = make([]int, 0, len(s.config.SearchEngines))
+ for _, engine := range s.config.SearchEngines {
+ endpoint := fmt.Sprintf("%s/indexnow", engine)
+
+ req, reqErr := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData))
+ if reqErr != nil {
+ errors = append(errors, fmt.Sprintf("创建请求失败: %v", reqErr))
+ continue
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, doErr := s.client.Do(req)
+ if doErr != nil {
+ errors = append(errors, fmt.Sprintf("请求 %s 失败: %v", engine, doErr))
+ continue
+ }
+
+ statusCodes = append(statusCodes, resp.StatusCode)
+ log.Info("indexnow: api response",
+ log.Any("engine", engine),
+ log.Any("statusCode", resp.StatusCode),
+ log.Any("urlCount", len(urls)))
+
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
+ errors = append(errors, fmt.Sprintf("搜索引擎 %s 返回错误状态码: %d", engine, resp.StatusCode))
+ }
+ resp.Body.Close()
+ }
+
+ if len(errors) > 0 {
+ return statusCodes, fmt.Errorf("%v", errors)
+ }
+
+ return statusCodes, nil
+}
+
+// splitIntoBatches 将URL列表分割成多个批次
+func (s *BatchIndexNowSubmitter) splitIntoBatches(urls []string, batchSize int) [][]string {
+ var batches [][]string
+
+ for i := 0; i < len(urls); i += batchSize {
+ end := i + batchSize
+ if end > len(urls) {
+ end = len(urls)
+ }
+ batches = append(batches, urls[i:end])
+ }
+
+ return batches
+}
+
+// BatchResult 批量提交结果
+type BatchResult struct {
+ TotalURLs int
+ SuccessCount int
+ FailCount int
+ Errors []string
+ SuccessURLs []string
+ StatusCodes []int // 各引擎 HTTP 状态码(按 SearchEngines 顺序,含重试后最后一次)
+}
+
+// String 结果字符串表示
+func (r *BatchResult) String() string {
+ return fmt.Sprintf("提交完成: 总数=%d, 成功=%d, 失败=%d, 错误数=%d, statusCodes=%v",
+ r.TotalURLs, r.SuccessCount, r.FailCount, len(r.Errors), r.StatusCodes)
+}
diff --git a/pub/seo/sitemapdirty/dirty.go b/pub/seo/sitemapdirty/dirty.go
new file mode 100644
index 0000000..b90e6f5
--- /dev/null
+++ b/pub/seo/sitemapdirty/dirty.go
@@ -0,0 +1,44 @@
+// Package sitemapdirty 在 web 与 app 之间传递「站点内容已变更」的标记。
+// web 进程负责文章的发布/更新/删除,而 sitemap 文件由 app 进程生成并对外提供,
+// 两者独立部署且不互相调用,因此借数据库中的一条记录做跨进程通知。
+package sitemapdirty
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/syncfilemod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func recordFilter() bson.M {
+ return bson.M{syncfilemod.FN.SyncRecord.Job: syncfilemod.SitemapDirtyJob}
+}
+
+// Mark 标记内容已变更,app 侧会在下一个检查周期重新生成 sitemap
+func Mark(ctx context.Context) {
+ now := time.Now()
+ update := bson.M{
+ "$set": bson.M{
+ syncfilemod.FN.SyncRecord.RecordTime: now,
+ syncfilemod.FN.SyncRecord.UpdatedAt: now,
+ },
+ "$setOnInsert": bson.M{
+ syncfilemod.FN.SyncRecord.Job: syncfilemod.SitemapDirtyJob,
+ syncfilemod.FN.SyncRecord.CreatedAt: now,
+ },
+ }
+ if serr := syncfilemod.UpsertOneSyncRecord(ctx, recordFilter(), update); serr != nil {
+ log.Warn("sitemapdirty: mark failed", log.E(serr))
+ }
+}
+
+// LastDirtyAt 读取最近一次内容变更时间,无记录时返回零值
+func LastDirtyAt(ctx context.Context) time.Time {
+ record, serr := syncfilemod.FindOneSyncRecord(ctx, recordFilter())
+ if serr != nil || record == nil {
+ return time.Time{}
+ }
+ return record.RecordTime
+}
diff --git a/pub/storage/proxy/object.go b/pub/storage/proxy/object.go
new file mode 100644
index 0000000..d56c3c4
--- /dev/null
+++ b/pub/storage/proxy/object.go
@@ -0,0 +1,532 @@
+package proxy
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+ "videoweb/pub/common/http"
+ "videoweb/pub/common/log"
+)
+
+const (
+ apiDomain = "http://resourcets.bbaazz.me/file/upload"
+)
+
+var (
+ header = map[string]string{"Content-Type": "application/json"}
+)
+
+const (
+ Success = 200
+)
+
+func UploadImg(fileName string, data []byte, domain string) (pathStr string, err error) {
+ r := objreq{
+ Name: fileName,
+ Data: base64.StdEncoding.EncodeToString(data),
+ }
+ jsonbytes, err := json.Marshal(r)
+ if err != nil {
+ return
+ }
+ url := ImageUploadAddressRepair(domain, BucketImageAddressV2)
+
+ resp, err := http.Post(url, header, bytes.NewReader(jsonbytes))
+ if err != nil {
+ return
+ }
+
+ var or objresp
+ if err = json.Unmarshal(resp.Data, &or); err != nil {
+ return
+ }
+ if or.Code != Success {
+ err = fmt.Errorf("上传失败:%s", or.Msg)
+ return
+ }
+ pathStr = or.Object.Name
+ return
+}
+
+func UploadBase64Img(fileName string, data string, domain string) (pathStr string, err error) {
+ r := objreq{
+ Name: fileName,
+ Data: data,
+ }
+ jsonbytes, err := json.Marshal(r)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = apiDomain
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/upload") {
+ url = domain + "/file/upload"
+ }
+ resp, err := http.Post(url, header, bytes.NewReader(jsonbytes))
+ if err != nil {
+ return
+ }
+
+ var or objresp
+ if err = json.Unmarshal(resp.Data, &or); err != nil {
+ return
+ }
+ if or.Code != Success {
+ err = fmt.Errorf("上传失败:%s", or.Msg)
+ return
+ }
+ pathStr = or.Object.Name
+ if strings.Contains(domain, "resourceimg.bbaazz.me") &&
+ !strings.HasPrefix(pathStr, "v2/") {
+ pathStr = "v2/" + pathStr
+ }
+ return
+}
+
+func fileServer() (url string) {
+ return "http://resourcets.bbaazz.me"
+}
+
+func PostOk(baseInfo *ShardOk, domain string) (result *VideoOkResult, err error) {
+ result = &VideoOkResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ url := VideoOkAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(baseInfo)
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ fmt.Println(string(fr.Data))
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(result)
+ return nil, err
+ }
+ fmt.Println(result)
+ return
+}
+func PostOkV2(baseInfo *ShardOk, domain string) (result *VideoOkResult, err error) {
+ result = &VideoOkResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ url := VideoOkAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(paramJson)
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(result)
+ return nil, err
+ }
+ fmt.Println(result)
+ return
+}
+func QueryMedia(baseInfo *MediaQueryeReq, domain string) (result *VideoQueryResult, err error) {
+ result = &VideoQueryResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/query") {
+ url = domain + "/file/web/media/query"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+
+ return
+}
+
+func QueryAdvitiseLocation(baseInfo *AppAdvitiseInfoReq, domain string) (result *AdvitiseLocationQuerysResult, err error) {
+ result = &AdvitiseLocationQuerysResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/app/advitise/loca") {
+ url = domain + "/media/web/app/advitise/loca"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+ return
+}
+func QueryMedias(baseInfo *MediaQueryesReq, domain string) (result *VideoQuerysResult, err error) {
+ result = &VideoQuerysResult{
+ Data: make([]MediaResp, 0),
+ }
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/task/querys") {
+ url = domain + "/media/web/task/querys"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+ return
+}
+func SyncMedia(baseInfo *MediaTaskSyncReq, domain string) (result *VideoQuerysResult, err error) {
+ result = &VideoQuerysResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/task/sync") {
+ url = domain + "/media/web/task/sync"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+
+ return
+}
+
+func UploadVideo(domain string, body *ShardBody) (result *Result, err error) {
+ if body == nil {
+ return nil, errors.New("no video to upload")
+ }
+ url := VideoBodyAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(url)
+
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Post(url, header, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println("resp-->", string(resp.Data))
+ result = new(Result)
+ if err = json.Unmarshal(resp.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func UploadVideoV2(domain string, body *ShardBodyV2) (result *Result, err error) {
+ if body == nil {
+ return nil, errors.New("no video to upload")
+ }
+ url := VideoBodyAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(url)
+
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Post(url, header, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println("resp-->", string(resp.Data))
+ result = new(Result)
+ if err = json.Unmarshal(resp.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func PostSearch(param *MediaSearchReq, domain string) (result *MediaSearchResult, err error) {
+ result = &MediaSearchResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/search") {
+ url = domain + "/file/web/media/search"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ //log.Info("proxy:test", log.Any("url", url), log.Any("result", result))
+ return
+}
+
+func PostUpdate(param *MediaUpdateReq, domain string) (result *MediaUpdateResult, err error) {
+ result = &MediaUpdateResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/update") {
+ url = domain + "/file/web/media/update"
+ }
+ fr, perr := http.Post(url, header, bytes.NewReader(paramJson))
+ if perr != nil {
+ log.Error("PostUpdate Unmarshal", log.E(perr))
+ err = perr
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ log.Error("PostUpdate Unmarshal", log.E(err), log.Any("data", string(fr.Data)))
+ return nil, err
+ }
+ return
+}
+
+func PostDelete(param *MediaDelReq, domain string) (result *MediaDelResult, err error) {
+ result = &MediaDelResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/delete") {
+ url = domain + "/file/web/media/delete"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func PostUpdatePreTime(param *MediaUpdatePreTime, domain string) (result *MediaDelResult, err error) {
+ result = &MediaDelResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/update/prefile") {
+ url = domain + "/file/web/media/update/prefile"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func UploadVideoV2New(domain string, body *ShardBodyV2) (result *Result, err error) {
+ if body == nil {
+ return nil, errors.New("no video to upload")
+ }
+ if len(domain) == 0 {
+ domain = fileServer()
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/proxy/videoBodyV2") {
+ url = domain + "/file/web/proxy/videoBodyV2"
+ }
+ fmt.Println(url)
+
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Post(url, header, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println("resp-->", string(resp.Data))
+ result = new(Result)
+ if err = json.Unmarshal(resp.Data, result); err != nil {
+ return nil, err
+ }
+ // if strings.Contains(domain, "resourcets.bbaazz.me") && !strings.HasPrefix(result.Data.VideoUri, "v2/") {
+ // result.Data.VideoUri = "v2/" + result.Data.VideoUri
+ // }
+ fmt.Println("result-->", result)
+ if result.Code != 200 {
+ err = fmt.Errorf("result code err")
+ }
+ return
+}
+
+// SyncMediaFile 将相对路径 m3u8 登记到 media 切片服
+func SyncMediaFile(req *MediaFileSyncReq, domain string) (result *MediaAPIResult, err error) {
+ result = &MediaAPIResult{}
+ paramJson, err := json.Marshal(req)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/mediafile/sync") {
+ url = domain + "/media/web/mediafile/sync"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+// AddMediaTask 提交 media 加水印切片任务
+func AddMediaTask(req *MediaTaskAddReq, domain string) (result *MediaAPIResult, err error) {
+ result = &MediaAPIResult{}
+ paramJson, err := json.Marshal(req)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/task/add") {
+ url = domain + "/media/web/task/add"
+ }
+ reqHeader := map[string]string{
+ "Content-Type": "application/json",
+ "acc-id": "1",
+ "acc-name": "playlet-watermark",
+ "app-id": fmt.Sprintf("%d", req.AppId),
+ }
+ fr, err := http.Post(url, reqHeader, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+// UpdateMediaWatermark 按已有 media 任务重新加水印切片
+func UpdateMediaWatermark(req *MediaTaskUpdateWatermarkReq, domain string) (result *MediaAPIResult, err error) {
+ result = &MediaAPIResult{}
+ paramJson, err := json.Marshal(req)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/task/update/watermark") {
+ url = domain + "/media/web/task/update/watermark"
+ }
+ reqHeader := map[string]string{
+ "Content-Type": "application/json",
+ "acc-id": "1",
+ "acc-name": "playlet-watermark",
+ "app-id": fmt.Sprintf("%d", req.AppId),
+ }
+ fr, err := http.Post(url, reqHeader, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+// QueryMediaSource 按原片 ID 查询 media source 状态
+func QueryMediaSource(sourceID string, appId int64, domain string) (*MediaSourceListItem, error) {
+ req := MediaSourceListReq{
+ Id: sourceID,
+ AppId: appId,
+ WebAppId: appId,
+ PageNum: 1,
+ PageSize: 1,
+ }
+ paramJSON, err := json.Marshal(req)
+ if err != nil {
+ return nil, err
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/source/list") {
+ url = domain + "/media/web/source/list"
+ }
+ reqHeader := map[string]string{
+ "Content-Type": "application/json",
+ "acc-id": "1",
+ "acc-name": "playlet-watermark",
+ "app-id": fmt.Sprintf("%d", appId),
+ }
+ fr, err := http.Post(url, reqHeader, bytes.NewReader(paramJSON))
+ if err != nil {
+ return nil, err
+ }
+ result := &MediaSourceListResult{}
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ if result.Code != Success || len(result.Data.List) == 0 {
+ return nil, nil
+ }
+ return &result.Data.List[0], nil
+}
diff --git a/pub/storage/proxy/object_test.go b/pub/storage/proxy/object_test.go
new file mode 100644
index 0000000..943b369
--- /dev/null
+++ b/pub/storage/proxy/object_test.go
@@ -0,0 +1 @@
+package proxy
diff --git a/pub/storage/proxy/test.html b/pub/storage/proxy/test.html
new file mode 100644
index 0000000..a7de28b
--- /dev/null
+++ b/pub/storage/proxy/test.html
@@ -0,0 +1,3 @@
+
+ 51吃瓜国内最新地址 https://51cga33.com
51吃瓜海外永久地址 https://51cg1.com
播放异常? 请刷新 或 使用51吃瓜App
苹果App白屏 请等待1分钟/切换网络/重新下载
发邮件获取 最新网址 👇长按复制保存👇
51cgfun@pm.me
曲阜师范大学学生【何鑫】的私密自拍视频流出,尺度炸裂,令人瞠目!这位清纯外表的学妹私下化身小母狗,为完成神秘“主人”的任务大秀骚浪本性!视频中,她身着宽松裸体背带裤,半遮半掩的布料勾勒出白嫩胴体,饱满小胸和粉嫩美穴若隐若现。




何鑫裸体自拍玩穴
她站在宿舍镜前,挑逗地拉下背带裤,露出湿漉漉的私处,手指大胆深入玩穴,湿滑水声混杂娇喘,镜头特写捕捉到她揉搓嫩穴的每一个细节,令人血脉喷张!



何鑫足交伺候主人
何鑫还完成足交任务,赤裸坐在床边,用白嫩玉足轻抚主人粗大肉棒,脚趾灵活挑逗,油亮光泽的脚掌摩擦到主人爽到低吼!
关键词:#曲阜师范大学何鑫 #何鑫裸体自拍 #何鑫对镜玩穴自慰 #何鑫足交伺候主人 #何鑫不雅自拍视频 #曲阜师范大学 #何鑫 #自慰 #足交 #51吃瓜 #51吃瓜网 #黑料 #吃瓜
想看更多精彩完整版,下载51吃瓜APP一爽到底~
51吃瓜APP下载地址:
最新地址 https://url46.co/1222
安卓下载备用地址一: https://url46.co/1237
安卓下载备用地址二: https://url46.co/1238
iOS下载备用地址 https://url46.co/1239
+
\ No newline at end of file
diff --git a/pub/storage/proxy/types.go b/pub/storage/proxy/types.go
new file mode 100644
index 0000000..6c78ce0
--- /dev/null
+++ b/pub/storage/proxy/types.go
@@ -0,0 +1,537 @@
+package proxy
+
+import (
+ "strings"
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type objreq struct {
+ Name string `json:"fileName"`
+ Data string `json:"fileData"`
+}
+
+type Obj struct {
+ Domain string `json:"domain"`
+ Name string `json:"fileName"`
+}
+
+type objresp struct {
+ Code int `json:"code"`
+ Object Obj `json:"data"`
+ Msg string `json:"msg"`
+}
+
+type ShardBody struct {
+ TaskId string `json:"taskId,omitempty"` // 文档md5
+ Pos int `json:"pos,omitempty"` // 位置
+ FileData string `json:"fileData,omitempty"` // 数据
+ Type string `json:"type,omitempty"`
+ TotalPos int `json:"totalPos,omitempty"` // 分片总数
+}
+
+type ShardBodyV2 struct {
+ TaskId string `json:"taskId,omitempty"` // 文档md5
+ Pos int `json:"pos,omitempty"` // 位置
+ FileData string `json:"fileData,omitempty"` // 数据
+ Type string `json:"type,omitempty"`
+ TotalPos int `json:"totalPos,omitempty"`
+ FileName string `json:"fileName,omitempty"`
+ Publish string `json:"publish,omitempty"`
+ AppId int `json:"appId,omitempty"`
+ MP4Folder string `json:"mp4Folder"` // 保存mp4的文件夹名,为空表示不需要保存mp4
+}
+
+type Data struct {
+ Id string `json:"id,omitempty"`
+ VideoUri string `json:"videoUri,omitempty"`
+}
+
+type Result struct {
+ Code int `json:"code,omitempty"` //
+ Data Data `json:"data,omitempty"` //
+ Msg string `json:"msg,omitempty"` //
+}
+
+type VideoOkResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data string `json:"data,omitempty"`
+}
+type ShardOk struct {
+ TaskId string `json:"taskId,omitempty"` // 文档md5
+ Id string `json:"id,omitempty"` // Id
+ Title *string `json:"title,omitempty"` // 标题
+ CoverImg *string `json:"coverImg,omitempty"` // 封面
+}
+
+type MediaTaskSyncReq struct {
+ AppId int64 `bson:"appId" json:"appId"` // APPID
+ LastUpdateTime time.Time `bson:"lastUpdateTime" json:"lastUpdateTime"` // 最后更新时间
+ PageSize int64 `bson:"pageSize" json:"pageSize"`
+}
+
+type MediaQueryeReq struct {
+ Id string `json:"id"`
+ TaskId string `json:"taskId"`
+}
+type MediaQueryesReq struct {
+ Ids []string `json:"ids,omitempty"`
+ MediaSourceIds []string `json:"mediaSourceIds,omitempty"`
+ AppId int64 `json:"appId,omitempty"`
+}
+type VideoQueryResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaResp `json:"data,omitempty"`
+}
+type VideoQuerysResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data []MediaResp `json:"data,omitempty"`
+}
+type MediaSyncReq struct {
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+ Param *SyncParam `json:"param"`
+}
+type SyncParam struct {
+ Status string `bson:"status,omitempty" json:"status,omitempty"` // 当前状态
+ FirstTag string `json:"firstTag,omitempty"` // 一级标签
+ Publish string `json:"publish,omitempty"` // 视频发布者
+}
+type VideoSyncResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaSearchResp `json:"data,omitempty"`
+}
+
+// type MediaSearchResp struct {
+// Total int64 `json:"total"`
+// List []*MediaSync `json:"list"`
+// }
+
+type MediaResp struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"id"`
+ MediaSourceId primitive.ObjectID `bson:"mediaSourceId,omitempty" json:"mediaSourceId"` // 原始Id
+ VideoTime int32 `bson:"videoTime,omitempty" json:"videoTime"` // 影片长度
+ Size int64 `bson:"size,omitempty" json:"size"` // 文件size,单位byte
+ AppId int64 `bson:"appId,omitempty" json:"appId"` // 应用列表
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ VideoCover string `bson:"videoCover,omitempty" json:"videoCover"` // 视频封面
+ Title string `bson:"title,omitempty" json:"title"` // 标题
+ Publish string `bson:"publish,omitempty" json:"publish"` // 视频发布者
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+
+ Nodes []videoinformationmod.Node `bson:"nodes" json:"nodes"` // 节点
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ Status string `bson:"status,omitempty" json:"status"` // 当前状态
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ TaskStartTime time.Time `bson:"taskStartTime,omitempty" json:"taskStartTime"` // 任务开始时间
+ SourceNameFs string `bson:"sourceNameFs,omitempty" json:"sourceNameFs"` // 远程源文件
+ IsDelLocalDir bool `bson:"isDelLocalDir,omitempty" json:"isDelLocalDir"` // 是否删除本机文件
+ WatermarkImage string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
+ WatermarkMedia string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件
+ DownloadRetry int `bson:"downloadRetry,omitempty" json:"downloadRetry"` // 下载重试次数
+ WatermarkRetry int `bson:"watermarkRetry" json:"watermarkRetry"` // 水印重试次数
+ CompressRetry int `bson:"compressRetry" json:"compressRetry"` // 压缩重试次数
+ SegmentRetry int `bson:"segmentRetry" json:"segmentRetry"` // 切片重试次数
+ LinodeRetry int `bson:"linodeRetry" json:"linodeRetry"` // 上传重试次数
+
+ // 切片相关信息
+ Filename string `bson:"filename,omitempty" json:"filename"` // 文件名称
+ FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs"` // H.264 m3u8 路径
+ PreMp4FileNameFs string `bson:"preMp4FileNameFs,omitempty" json:"preMp4FileNameFs"` // 预览MP4文件
+ UnencryptedPreMp4 string `bson:"unencryptedPreMp4,omitempty" json:"unencryptedPreMp4"` // 不加密的预览MP4
+ TempFieldNameFs string `bson:"tempFieldNameFs,omitempty" json:"tempFieldNameFs"` // 临时存储
+ Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀
+ TaskID string `bson:"taskId,omitempty" json:"taskId"` // 项目组传来的唯一标示
+ Width int64 `bson:"width,omitempty" json:"width"` // 分辨率宽
+ Height int64 `bson:"height,omitempty" json:"height"` // 分辨率高
+ FrameRate string `bson:"frameRate,omitempty" json:"frameRate"` // 视频贞数
+ SyBitRate string `bson:"syBitRate,omitempty" json:"syBitRate"` // 音频bit率
+ VidBitRate string `bson:"vidBitRate,omitempty" json:"vidBitRate"` // 视频bit率
+ CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updatedAt"` // 更新时间
+ ProxyUpload *int32 `bson:"proxyUpload,omitempty" json:"proxyUpload"` // 代理上传
+ Retry int `bson:"retry" json:"retry"` // 重试次数 默认为零 最大重试3次
+ NotifySignal bool `bson:"notifySignal" json:"notifySignal"` //中止通知的信号 false 可以发送通知,true 中止发送通知
+
+ InfoTsName string `bson:"infoTsName" json:"infoTsName"` // 从该ts中获取视频分辨率等信息
+ SyncFlag int64 `bson:"syncFlag" json:"syncFlag"` // 同步状态
+ SrcKey string `bson:"srcKey,omitempty" json:"srcKey"` // 原始Key
+ MediaType int64 `bson:"mediaType" json:"mediaType"` // 资源类型:视频
+ IsPlaylet bool `bson:"isPlaylet,omitempty" json:"isPlaylet,omitempty"` // 是否短剧视频
+ IssuedAt time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
+ IssuedBrand string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
+ Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+ UploadUserName string `bson:"uploadUserName" json:"uploadUserName,omitempty"` // 上传用户名
+ OptionAccount string `bson:"optionAccount,omitempty" json:"optionAccount"` // 最后操作人
+}
+
+// PlayletKeys 短剧媒资比对用的 ID(任务 id / 源 id / taskId)
+func (info MediaResp) PlayletKeys() []string {
+ keys := make([]string, 0, 3)
+ if !info.Id.IsZero() {
+ keys = append(keys, info.Id.Hex())
+ }
+ if !info.MediaSourceId.IsZero() {
+ keys = append(keys, info.MediaSourceId.Hex())
+ }
+ if taskID := strings.TrimSpace(info.TaskID); taskID != "" {
+ keys = append(keys, taskID)
+ }
+ return keys
+}
+
+func (info *MediaResp) ToVideoInfo() *videoinformationmod.VideoInformation {
+ syncKey := info.Id.Hex()
+ if !info.MediaSourceId.IsZero() {
+ syncKey = info.MediaSourceId.Hex()
+ }
+ rsl := &videoinformationmod.VideoInformation{
+ Id: mongos.Lid.VideoInfo.Get(),
+ Title: info.Title,
+ AddedTime: time.Time{},
+ Keyworld: info.Keyworld,
+ FirstTag: info.FirstTag,
+ SecondTags: info.SecondTags,
+ CoverImg: info.CoverImg,
+ Desc: info.Desc,
+ Remark: info.Remark,
+ MediaType: videoinformationmod.MediaType_Video,
+ Status: pubdef.InformationStatusWaiting,
+ Tags: make([]int64, 0),
+ Real: pubdef.Flow{},
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ IssuedBrand: info.IssuedBrand,
+ IssuedAt: info.IssuedAt,
+ Bango: info.Bango,
+ OptionAccount: info.OptionAccount,
+ UploadUserName: info.UploadUserName,
+ UnencryptedImage: info.UnencryptedImage,
+ SyncId: syncKey,
+ }
+ rsl.Nodes = make([]videoinformationmod.Node, 0)
+ if info.Nodes != nil && len(info.Nodes) > 0 {
+ for _, node := range info.Nodes {
+ node.Videos = make([]videoinformationmod.VideoInfo, 0)
+ rsl.Nodes = append(rsl.Nodes, node)
+ }
+ }
+ if len(info.FieldNameFs) > 0 {
+
+ rsl.Video = videoinformationmod.VideoInfo{
+ TaskId: info.TaskID,
+ FileId: syncKey,
+ Url: info.FieldNameFs,
+ Cover: info.VideoCover,
+ Height: info.Height,
+ Width: info.Width,
+ VideoTime: info.VideoTime,
+ PreMp4FileNameFs: info.PreMp4FileNameFs,
+ UnencryptedPreMp4: info.UnencryptedPreMp4,
+ }
+ if !strings.HasSuffix(rsl.Video.Url, ".m3u8") {
+ rsl.Video.Url += ".m3u8"
+ }
+ }
+ return rsl
+}
+
+type MediaType int
+
+const (
+ Preview MediaType = 1
+ Normal MediaType = 2
+)
+
+type FailReason string
+
+const (
+ FailReasonNone FailReason = ""
+ FailReasonUploadTimeoutError FailReason = "UploadTimeoutError" // 上传超时
+ FailReasonMergeError FailReason = "MergeError" // 合并错误
+ FailReasonCompressError FailReason = "CompressError" // 压缩错误
+ FailReasonSegmentError FailReason = "SegmentError" // 分片错误
+ FailReasonLinodeError FailReason = "LinodeError" // Linode错误
+)
+
+type BaseInfo struct {
+ ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
+ SrcId int64 `bson:"srcId,omitempty" json:"srcId"` // 原始ID
+ Title string `bson:"title,omitempty" json:"title"` // 标题
+ CheckSum string `bson:"checkSum,omitempty" json:"checkSum"` // md5校验码
+ VideoTime int32 `bson:"videoTime,omitempty" json:"videoTime"` // 影片长度
+ Publish string `json:"publish,omitempty" bson:"publish"` // 视频发布者
+ Type string `bson:"type,omitempty" json:"type"` // 长视频/短视频
+ Preview MediaType `bson:"preview,omitempty" json:"preview"` // 预览普通视频:1:预览 2:正片
+ Country string `bson:"country,omitempty" json:"country"` // 国家 (国产、日本、欧美.... 前端固定)
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ PreFileName string `bson:"preFileName,omitempty" json:"preFileName"` // 预览文件
+ Status string `bson:"status,omitempty" json:"status"` // 当前状态
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Size int64 `bson:"size,omitempty" json:"size"` // 文件size,单位byte
+ Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+ Directors []string `bson:"directors,omitempty" json:"directors"` // 导演
+ Actors []string `bson:"actors,omitempty" json:"actors"` // 演员
+ Studio string `bson:"studio,omitempty" json:"studio"` // 工作室/所有者
+ AddedTime time.Time `bson:"addedTime,omitempty" json:"addedTime"` // 上架时间
+ AppIds []int64 `bson:"appids,omitempty" json:"appids,omitempty"` // 应用列表
+
+ // 切片相关信息
+ Filename string `bson:"filename,omitempty" json:"filename"` // 文件名称
+ FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs"` // 文件名称
+ Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀
+ TaskID string `bson:"taskId,omitempty" json:"taskId"` // 项目组传来的唯一标示
+ Width int `bson:"width,omitempty" json:"width"` // 分辨率宽
+ Height int `bson:"height,omitempty" json:"height"` // 分辨率高
+ Ratio float64 `bson:"ratio,omitempty" json:"ratio"` // 宽高比
+ FrameRate string `bson:"frameRate,omitempty" json:"frameRate"` // 视频贞数
+ SyBitRate string `bson:"syBitRate,omitempty" json:"syBitRate"` // 音频bit率
+ VidBitRate string `bson:"vidBitRate,omitempty" json:"vidBitRate"` // 视频bit率
+ CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updatedAt"` // 更新时间
+ ProxyUpload *int32 `bson:"proxyUpload,omitempty" json:"proxyUpload"` // 代理上传
+
+ IsAdd bool `bson:"isAdd,omitempty" json:"isAdd"` // 是否加入到本地
+ Retry int `bson:"retry" json:"retry"` // 重试次数 默认为零 最大重试3次
+ NotifySignal bool `bson:"notifySignal" json:"notifySignal"` //中止通知的信号 false 可以发送通知,true 中止发送通知
+ ShowLocation int64 `bson:"showLocation" json:"showLocation"` // 显示位置
+ Remark string `bson:"remark,omitempty" json:"remark"`
+
+ MergeRetry int `bson:"mergeRetry" json:"mergeRetry"` //合并重试次数
+ CompressRetry int `bson:"compressRetry" json:"compressRetry"` //压缩重试次数
+ SegmentRetry int `bson:"segmentRetry" json:"segmentRetry"` //切片重试次数
+ LinodeRetry int `bson:"linodeRetry" json:"linodeRetry"` //上传linode重试次数
+ FailReason FailReason `bson:"failReason" json:"failReason"` //失败原因
+ DeleteDisk bool `bson:"deleteDisk" json:"deleteDisk"` //是否删除了对于磁盘文件
+ InfoTsName string `bson:"infoTsName" json:"infoTsName"` //从该ts中获取视频分辨率等信息
+ UploadedPoses []int64 `bson:"uploadedPoses" json:"uploadedPoses"` //上传过的pos列表
+}
+
+type MediaUpdate struct {
+ ID string `json:"id,omitempty"`
+ Title *string `bson:"title,omitempty" json:"title,omitempty"` // 标题
+ AddedTime *time.Time `bson:"addedTime,omitempty" json:"addedTime,omitempty"` // 上架时间
+ Publish *string `json:"publish,omitempty" bson:"publish,omitempty"` // 视频发布者
+ Country *string `bson:"country,omitempty" json:"country,omitempty"` // 国家 (国产、日本、欧美....)
+ Preview *MediaType `bson:"preview,omitempty" json:"preview,omitempty"` // 预览普通视频:1:预览 2:正片
+ Type *string `bson:"type,omitempty" json:"type,omitempty"` // 长视频/短视频
+ FirstTag *string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
+ SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
+ Bango *string `bson:"bango,omitempty" json:"bango,omitempty"` // 番号,唯一
+ AppIds *[]int64 `bson:"appids,omitempty" json:"appids,omitempty"` // 应用列表
+ Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
+ PreFileName *string `bson:"preFileName,omitempty" json:"preFileName,omitempty"` // 预览文件
+ CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
+ UnencryptedImage *string `bson:"unencryptedImage,omitempty" json:"unencryptedImage,omitempty"` // 不加密封面
+ CreatedAt *time.Time `bson:"createdAt,omitempty" json:"createdAt,omitempty"` // 创建时间
+ UpdatedAt *time.Time `bson:"updatedAt,omitempty" json:"updatedAt,omitempty"` // 更新时间
+ ShowLocation *int64 `bson:"showLocation,omitempty" json:"showLocation"` // 显示位置
+ MoveInfo *MoveInfo `bson:"moveInfo,omitempty" json:"moveInfo,omitempty"` // 电影自动更新
+}
+type MoveInfo struct {
+ Score float64 `json:"score" bson:"score"` // 评分 暂无评分=0
+ SerialCount int64 `json:"serialCount" bson:"serialCount"` // 总集数 serialCount
+ SerialIndex int64 `json:"serialIndex" bson:"serialIndex"` // 集数 lastName
+ Likes int64 `json:"likes" bson:"likes"` // 点赞数量 favoriteCount
+ Watchs int64 `json:"watchs" bson:"watchs"` // 点赞数量 view
+ UpdateDate time.Time `json:"updateDate" bson:"updateDate"` // 更新时间 languagesplaylist 短剧和剧集有更新 电影
+ MoveTag []string `json:"moveTag" bson:"moveTag"` // 标签:cidMapper + post_Year + regional
+}
+type VideoSearch struct {
+ ID string `json:"id,omitempty"`
+ Title *string `json:"title,omitempty"` // 标题
+ TaskID *string `json:"taskId,omitempty"` // 项目组传来的唯一标示
+ Publish *string `bson:"publish,omitempty"` // 视频发布者
+ Type *string `json:"type"` // 视频:长视频/短视频--时间阈值
+ Preview *int32 `json:"preview"` // 预览:1:预览 2:普通视频
+ Country *string `json:"country"` // 国家 (国产、日本、欧美.... 前端固定)
+ Appid *int64 `json:"appid"` // 应用ID
+ FirstTag *string `json:"firstTag,omitempty"` // 一级标签
+ SecondTags *string `json:"secondTags,omitempty"` // 二级标签标签
+ Status *string `json:"status,omitempty"` // 当前状态
+ Directors *[]string `json:"director,omitempty"` // 导演
+ Actors *[]string `json:"actors,omitempty"` // 演员
+ Studio *string `json:"studio,omitempty"` // 工作室
+ Bango *string `json:"bango,omitempty"` // 番号,唯一
+ Via *string `json:"via,omitempty"` // 来源
+ Remark *string `json:"remark,omitempty"` // 备注
+ AddedTime *string `json:"addedTime,omitempty"` // 上架时间
+ UploadTime *string `json:"uploadTime,omitempty"` // 上传时间
+ Start *time.Time `json:"start"` // 开始时间
+ End *time.Time `json:"end"` // 结束时间
+ ShowLocation *int64 `json:"showLocation"` // 显示位置
+}
+
+type MediaSearchReq struct {
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+ Param VideoSearch `json:"param"`
+}
+
+// MediaSearchReq 媒体搜索请求返回
+type MediaSearchResp struct {
+ Total int64 `json:"total"`
+ List []*BaseInfo `json:"list"`
+}
+
+type MediaSearchResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaSearchResp `json:"data,omitempty"`
+}
+
+type MediaDelReq struct {
+ ID string `json:"id,omitempty"`
+}
+
+// MediasDelResp 请求批量删除媒体回复
+type MediaDelResp struct {
+}
+
+type MediaUpdatePreTime struct {
+ ID string `json:"id,omitempty"`
+ PreFileTime int64 `bson:"preFileTime,omitempty" json:"preFileTime"` // 预览文件(开始时间)
+}
+
+type MediaDelResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ // Data MediaDelResp `json:"data,omitempty"`
+}
+
+type MediaUpdateReq struct {
+ //Ids []string `json:"id"`
+ MediaUpdate
+}
+
+// MediasUpdateResp 请求批量更新媒体回复
+type MediaUpdateResp struct {
+}
+
+type MediaUpdateResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaUpdateResp `json:"data,omitempty"`
+}
+
+type AppAdvitiseInfoReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+type AdvitiseInfo struct {
+ Id string `json:"id" bson:"_id"` // 广告id
+ Location int64 `json:"location" bson:"location"` // 位置id
+ Title string `json:"title" bson:"title"` // 广告标题
+ Images string `json:"images" bson:"images"` // 视频banner
+ Link string `json:"link" bson:"link"` // 链接
+}
+
+type AdvitiseLocationQuerysResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data AdvitiseLocationDataResp `json:"data,omitempty"`
+}
+type AdvitiseLocationDataResp struct {
+ List []AdvitiseLocationData `json:"list" bson:"list"` // 数据
+}
+
+type AdvitiseLocationData struct {
+ LocationId int64 `json:"locationId" bson:"locationId"` // 位置ID
+ LocationName string `json:"locationName" bson:"locationName"` // 位置名称
+ AdvitiseSize string `json:"advitiseSize" bson:"advitiseSize"` // 封面尺寸
+ ImgSizeDesc string `json:"imgSizeDesc" bson:"imgSizeDesc"` // 该位置图片描述
+ AdvitiseData []AdvitiseInfo `json:"advitiseData" bson:"advitiseData"` // 广告数据
+}
+
+// MediaFileSyncItem mediafile/sync 单条媒资项
+type MediaFileSyncItem struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ TaskID string `json:"taskId"`
+ FieldNameFs string `json:"fieldNameFs"`
+ CoverImg string `json:"coverImg"`
+ Width int64 `json:"width"`
+ Height int64 `json:"height"`
+ VideoTime int64 `json:"videoTime"`
+ Size int64 `json:"size"`
+ MediaFileVersion string `json:"mediaFileVersion"`
+ IsPlaylet bool `json:"isPlaylet"`
+}
+
+// MediaFileSyncReq 同步媒资库 m3u8 到 media 切片服
+type MediaFileSyncReq struct {
+ AppId int64 `json:"appId"`
+ AppKey string `json:"appKey"`
+ VideoList []MediaFileSyncItem `json:"videoList"`
+}
+
+// MediaTaskAddReq 提交 media 加水印切片任务
+type MediaTaskAddReq struct {
+ MediaSourceId string `json:"mediaSourceId"`
+ AppId int64 `json:"appId"`
+ AppKey string `json:"appKey"`
+ Title string `json:"title"`
+ WatermarkImage string `json:"watermarkImage"`
+ WatermarkMedia string `json:"watermarkMedia"`
+ WatermarkMediaTop string `json:"watermarkMediaTop"`
+ WatermarkMediaTopRight string `json:"watermarkMediaTopRight"`
+ WatermarkMediaBottomLeft string `json:"watermarkMediaBottomLeft"`
+}
+
+// MediaTaskUpdateWatermarkReq 按已有切片任务重新加水印
+type MediaTaskUpdateWatermarkReq struct {
+ Id string `json:"id"`
+ AppId int64 `json:"appId"`
+ AppKey string `json:"appKey"`
+ WatermarkImage *string `json:"watermarkImage,omitempty"`
+ WatermarkMedia *string `json:"watermarkMedia,omitempty"`
+ WatermarkMediaTop *string `json:"watermarkMediaTop,omitempty"`
+ WatermarkMediaTopRight *string `json:"watermarkMediaTopRight,omitempty"`
+ WatermarkMediaBottomLeft *string `json:"watermarkMediaBottomLeft,omitempty"`
+}
+
+// MediaAPIResult media 通用响应
+type MediaAPIResult struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Tip string `json:"tip"`
+}
+
+const MediaSourceCompletedFail = "CompletedFail"
+
+// MediaSourceListReq 按 ID 查询 media 原片状态
+type MediaSourceListReq struct {
+ Id string `json:"id"`
+ AppId int64 `json:"appId,omitempty"`
+ WebAppId int64 `json:"webAppId,omitempty"`
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+}
+
+type MediaSourceListItem struct {
+ Status string `json:"status"`
+ FailReason string `json:"failReason"`
+}
+
+type MediaSourceListResult struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Tip string `json:"tip"`
+ Data struct {
+ Total int64 `json:"total"`
+ List []MediaSourceListItem `json:"list"`
+ } `json:"data"`
+}
diff --git a/pub/storage/proxy/utils.go b/pub/storage/proxy/utils.go
new file mode 100644
index 0000000..761e69d
--- /dev/null
+++ b/pub/storage/proxy/utils.go
@@ -0,0 +1,141 @@
+package proxy
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ // 视频桶地址
+ BucketVideoAddressV1 = "http://ts.bbaazz.me"
+ // 图片桶地址
+ BucketImageAddressV1 = "http://img.bbaazz.me"
+ BucketImageAddressV2 = "http://tong3yuanimg.bbaazz.me"
+
+ // 上传路径
+ ImageUploadPathV1 = "/file/upload"
+ ImageUploadPathV2 = "/file/upload" // "/file/web/media/uploadImg"
+ VideoBodyPathV1 = "/file/web/proxy/videoBody"
+ VideoBodyPathV2 = "/file/web/proxy/videoBodyV2"
+ VideoOkPathV1 = "/file/web/proxy/videoOk"
+ VideoOkPathV2 = "/file/web/proxy/videoOkv2"
+)
+
+var (
+ BucketVideoAddressV2 = "http://139.162.27.29:19996" //正式
+ BucketVideoAddress = "http://tong3yuants.bbaazz.me"
+)
+
+func InitBucketVideoAddress(address, addressV2 string) {
+ BucketVideoAddress = address
+ BucketVideoAddressV2 = addressV2
+}
+
+func DomainAddressRepair(domain string, defaultBucketAddress ...string) string {
+ if domain == "" {
+ if len(defaultBucketAddress) > 0 && defaultBucketAddress[0] != "" {
+ domain = defaultBucketAddress[0]
+ } else {
+ domain = BucketVideoAddressV2
+ }
+ }
+ return domain
+}
+
+func ImageUploadAddressRepair(domain string, defaultBucketAddress ...string) string {
+ domain = DomainAddressRepair(domain, defaultBucketAddress...)
+ if !strings.Contains(domain, ImageUploadPathV1) && !strings.Contains(domain, ImageUploadPathV2) {
+ if strings.HasPrefix(domain, BucketImageAddressV1) {
+ return domain + ImageUploadPathV1
+ } else if strings.HasPrefix(domain, BucketImageAddressV2) {
+ return domain + ImageUploadPathV2
+ }
+ return domain + ImageUploadPathV2
+ }
+ return domain
+}
+
+func VideoBodyAddressRepair(domain string, defaultBucketAddress ...string) string {
+ domain = DomainAddressRepair(domain, defaultBucketAddress...)
+ if strings.HasPrefix(domain, BucketVideoAddressV1) {
+ if !strings.Contains(domain, VideoBodyPathV1) {
+ return domain + VideoBodyPathV1
+ }
+ return domain
+ } else if strings.HasPrefix(domain, BucketVideoAddressV2) {
+ if !strings.Contains(domain, VideoBodyPathV2) {
+ return domain + VideoBodyPathV2
+ }
+ return domain
+ } else { // 未知存储桶,默认v2
+ if !strings.Contains(domain, VideoBodyPathV2) {
+ return domain + VideoBodyPathV2
+ }
+ return domain
+ }
+}
+
+func VideoOkAddressRepair(domain string, defaultBucketAddress ...string) string {
+ domain = DomainAddressRepair(domain, defaultBucketAddress...)
+ if strings.HasPrefix(domain, BucketVideoAddressV1) {
+ if !strings.Contains(domain, VideoOkPathV1) {
+ return domain + VideoOkPathV1
+ }
+ return domain
+ } else if strings.HasPrefix(domain, BucketVideoAddressV2) {
+ if !strings.Contains(domain, VideoOkPathV2) {
+ return domain + VideoOkPathV2
+ }
+ return domain
+ } else { // 未知存储桶,默认v2
+ if !strings.Contains(domain, VideoOkPathV2) {
+ return domain + VideoOkPathV2
+ }
+ return domain
+ }
+}
+
+func GetExistsUrlPathBucketAddress(path string) string {
+ if strings.HasPrefix(path, "v1/") {
+ return BucketVideoAddressV1
+ } else if strings.HasPrefix(path, "v2/") {
+ return BucketVideoAddressV2
+ } else if strings.HasPrefix(path, "v3/") {
+ return BucketVideoAddressV2
+ } else {
+ return BucketVideoAddressV1 // 欲漫涩、微博、快看禁漫 默认用桶2,其他的app,默认用桶1
+ }
+}
+
+func AppendV2PrefixToUrlPath(name string) string {
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ name = fmt.Sprintf("v2/%s", name)
+ }
+ return name
+}
+
+func AppendV1PrefixToUrlPath(name string) string {
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ name = fmt.Sprintf("v1/%s", name)
+ }
+ return name
+}
+
+func AppendsMediaS3Version(name string) string {
+ // 默认使用V1
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ return "/v2"
+ }
+ return ""
+}
+func AppendsMediaS3VersionV1(name string) string {
+ // 默认使用V1
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ return "/v1"
+ }
+ return ""
+}
diff --git a/pub/storage/proxy/utils_test.go b/pub/storage/proxy/utils_test.go
new file mode 100644
index 0000000..4bcabed
--- /dev/null
+++ b/pub/storage/proxy/utils_test.go
@@ -0,0 +1,157 @@
+package proxy
+
+import "testing"
+
+func TestImageUploadAddressRepair(t *testing.T) {
+ type args struct {
+ domain string
+ defaultBucketAddress []string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{
+ domain: "http://aaa",
+ defaultBucketAddress: []string{},
+ },
+ want: "http://aaa/file/upload",
+ },
+ {
+ name: "b",
+ args: args{
+ domain: "http://ts.bbaazz.me",
+ defaultBucketAddress: []string{},
+ },
+ want: "http://ts.bbaazz.me/file/upload",
+ },
+ {
+ name: "c",
+ args: args{
+ domain: "http://resourcets.bbaazz.me",
+ defaultBucketAddress: []string{},
+ },
+ want: "http://resourcets.bbaazz.me/file/upload",
+ },
+ {
+ name: "d",
+ args: args{
+ domain: "",
+ defaultBucketAddress: []string{BucketImageAddressV1},
+ },
+ want: "http://ts.bbaazz.me/file/upload",
+ },
+ {
+ name: "e",
+ args: args{
+ domain: "",
+ defaultBucketAddress: []string{BucketImageAddressV2},
+ },
+ want: "http://resourcets.bbaazz.me/file/upload",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ImageUploadAddressRepair(tt.args.domain, tt.args.defaultBucketAddress...); got != tt.want {
+ t.Errorf("ImageUploadAddressRepair() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestVideoBodyAddressRepair(t *testing.T) {
+ type args struct {
+ domain string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{domain: "http://aaa"},
+ want: "http://aaa/file/web/proxy/videoBodyV2",
+ },
+ {
+ name: "b",
+ args: args{domain: "http://ts.bbaazz.me"},
+ want: "http://ts.bbaazz.me/file/web/proxy/videoBody",
+ },
+ {
+ name: "c",
+ args: args{domain: "http://resourcets.bbaazz.me"},
+ want: "http://resourcets.bbaazz.me/file/web/proxy/videoBodyV2",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := VideoBodyAddressRepair(tt.args.domain); got != tt.want {
+ t.Errorf("VideoBodyAddressRepair() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestVideoOkAddressRepair(t *testing.T) {
+ type args struct {
+ domain string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{domain: "http://aaa"},
+ want: "http://aaa/file/web/proxy/videoOkv2",
+ },
+ {
+ name: "b",
+ args: args{domain: "http://ts.bbaazz.me"},
+ want: "http://ts.bbaazz.me/file/web/proxy/videoOk",
+ },
+ {
+ name: "c",
+ args: args{domain: "http://resourcets.bbaazz.me"},
+ want: "http://resourcets.bbaazz.me/file/web/proxy/videoOkv2",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := VideoOkAddressRepair(tt.args.domain); got != tt.want {
+ t.Errorf("VideoOkAddressRepair() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestAppendV2PrefixToUrlPath(t *testing.T) {
+ type args struct {
+ name string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{
+ name: "abc/aaa",
+ },
+ want: "v2/abc/aaa",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := AppendV2PrefixToUrlPath(tt.args.name); got != tt.want {
+ t.Errorf("AppendV2PrefixToUrlPath() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pub/storage/upload.go b/pub/storage/upload.go
new file mode 100644
index 0000000..bf20bf9
--- /dev/null
+++ b/pub/storage/upload.go
@@ -0,0 +1,275 @@
+package storage
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "videoweb/pub/storage/proxy"
+
+ "strconv"
+)
+
+var (
+// buffer = bytes.Buffer{}
+// mtx sync.Mutex
+)
+
+// web后台上传使用, form-data方式上传
+func UploadImage(ctx context.Context, f *multipart.FileHeader, domain string) (path string, err error) {
+ if f == nil {
+ return "", errors.New("file is nil!")
+ }
+ var file multipart.File
+ if file, err = f.Open(); err != nil {
+ return
+ }
+
+ defer file.Close()
+
+ //mtx.Lock()
+ //defer mtx.Unlock()
+ buffer := bytes.Buffer{}
+ if _, err = buffer.ReadFrom(file); err != nil {
+ return
+ }
+ return proxy.UploadImg(f.Filename, buffer.Bytes(), domain)
+}
+
+func UploadVideoBody(ctx context.Context, form *multipart.Form, domain string) (*proxy.Result, error) {
+ if form == nil {
+ return nil, errors.New("multipart from is nil")
+ }
+ sb, err := makeShardBodyByForm(form)
+ if err != nil {
+ return nil, err
+ }
+ return proxy.UploadVideo(domain, sb)
+}
+
+func UploadVideoBodyV2(ctx context.Context, form *multipart.Form,
+ domain string, appIdIn int, publish string) (*proxy.Result, error) {
+ if form == nil {
+ return nil, errors.New("multipart from is nil")
+ }
+ sb, err := makeShardBodyByFormV2(form, appIdIn, publish)
+ if err != nil {
+ return nil, err
+ }
+ return proxy.UploadVideoV2(domain, sb)
+}
+
+func VideoBody(ctx context.Context, taskId, typ, domain string, pos int, data []byte) (*proxy.Result, error) {
+ body := &proxy.ShardBody{
+ TaskId: taskId,
+ Pos: pos,
+ FileData: base64.StdEncoding.EncodeToString(data),
+ Type: typ,
+ }
+ if typ == "1" {
+ body.Type = "AV"
+ } else if typ == "2" {
+ body.Type = "SP"
+ }
+ return proxy.UploadVideo(domain, body)
+}
+
+func UploadVideoOK(ctx context.Context, in *proxy.ShardOk, domain string) (*proxy.VideoOkResult, error) {
+ return proxy.PostOk(in, domain)
+}
+func UploadVideoOKV2(ctx context.Context, in *proxy.ShardOk, domain string) (*proxy.VideoOkResult, error) {
+ return proxy.PostOkV2(in, domain)
+}
+
+func QueryMedia(ctx context.Context, in *proxy.MediaQueryeReq, domain string) (*proxy.VideoQueryResult, error) {
+ return proxy.QueryMedia(in, domain)
+}
+func QueryMedias(ctx context.Context, in *proxy.MediaQueryesReq, domain string) (*proxy.VideoQuerysResult, error) {
+ return proxy.QueryMedias(in, domain)
+}
+func QueryAdvitiseLocationList(ctx context.Context, in *proxy.AppAdvitiseInfoReq, domain string) (*proxy.AdvitiseLocationQuerysResult, error) {
+ return proxy.QueryAdvitiseLocation(in, domain)
+}
+
+func SyncMedia(in *proxy.MediaTaskSyncReq, domain string) (result *proxy.VideoQuerysResult, err error) {
+ return proxy.SyncMedia(in, domain)
+}
+func makeShardBodyByForm(form *multipart.Form) (*proxy.ShardBody, error) {
+ poss := form.Value["pos"]
+ taskIds := form.Value["taskId"]
+ types := form.Value["type"]
+ fs := form.File["file"]
+ totalPoss := form.Value["totalChunks"]
+
+ var (
+ pos, total int
+ taskId string
+ typ string
+ f *multipart.FileHeader
+ err error
+ )
+
+ if len(fs) == 0 || fs[0] == nil {
+ return nil, errors.New("no file")
+ }
+ f = fs[0]
+
+ if len(poss) == 0 || poss[0] == "" {
+ return nil, errors.New("no pos")
+ }
+ if pos, err = strconv.Atoi(poss[0]); err != nil {
+ return nil, fmt.Errorf("invalid pos")
+ }
+ if len(totalPoss) == 0 || totalPoss[0] == "" {
+ return nil, errors.New("no totalPos")
+ }
+ if total, err = strconv.Atoi(totalPoss[0]); err != nil {
+ return nil, fmt.Errorf("invalid totalPos")
+ }
+
+ if len(taskIds) == 0 || taskIds[0] == "" {
+ return nil, errors.New("no taskId")
+ }
+ taskId = taskIds[0]
+
+ if len(types) == 0 || types[0] == "" {
+ return nil, errors.New("no type")
+ }
+ if types[0] == "1" {
+ typ = "AV"
+ } else if types[0] == "2" {
+ typ = "SP"
+ } else if types[0] == "3" {
+ typ = "AI"
+ } else {
+ typ = types[0]
+ }
+
+ file, ne := f.Open()
+ if ne != nil {
+ return nil, ne
+ }
+ defer file.Close()
+ //mtx.Lock()
+ //defer mtx.Unlock()
+ buffer := bytes.Buffer{}
+ if _, err = buffer.ReadFrom(file); err != nil {
+ return nil, err
+ }
+
+ resp := &proxy.ShardBody{
+ TaskId: taskId,
+ Pos: pos,
+ FileData: base64.StdEncoding.EncodeToString(buffer.Bytes()),
+ Type: typ,
+ TotalPos: total,
+ }
+ return resp, nil
+}
+
+func makeShardBodyByFormV2(form *multipart.Form, appIdIn int, publishIn string) (
+ *proxy.ShardBodyV2, error) {
+ taskIds := form.Value["taskId"]
+ types := form.Value["type"]
+ poss := form.Value["pos"]
+ fs := form.File["file"]
+ totalPoss := form.Value["totalChunks"]
+ fileNames := form.Value["filename"]
+ appIds := form.Value["appId"]
+ publishs := form.Value["publish"]
+
+ var (
+ pos, total int
+ taskId string
+ f *multipart.FileHeader
+ fileName string
+ typ string
+ publish string
+ appId int
+ err error
+ )
+
+ if len(fs) == 0 || fs[0] == nil {
+ return nil, errors.New("no file")
+ }
+ f = fs[0]
+
+ if len(poss) == 0 || poss[0] == "" {
+ return nil, errors.New("no pos")
+ }
+ if pos, err = strconv.Atoi(poss[0]); err != nil {
+ return nil, fmt.Errorf("invalid pos")
+ }
+ if len(totalPoss) == 0 || totalPoss[0] == "" {
+ return nil, errors.New("no totalPos")
+ }
+ if total, err = strconv.Atoi(totalPoss[0]); err != nil {
+ return nil, fmt.Errorf("invalid totalPos")
+ }
+
+ if len(taskIds) == 0 || taskIds[0] == "" {
+ return nil, errors.New("no taskId")
+ }
+ taskId = taskIds[0]
+
+ if len(types) == 0 || types[0] == "" {
+ return nil, errors.New("no type")
+ }
+ if types[0] == "1" {
+ typ = "AV"
+ } else if types[0] == "2" {
+ typ = "SP"
+ } else if types[0] == "3" {
+ typ = "AI"
+ } else {
+ typ = types[0]
+ }
+
+ if len(fileNames) == 0 || fileNames[0] == "" {
+ return nil, errors.New("no fileName")
+ }
+ fileName = fileNames[0]
+
+ if len(publishs) == 0 || publishs[0] == "" {
+ return nil, errors.New("no publish")
+ }
+ publish = publishs[0]
+ if publish == "" {
+ publish = publishIn
+ }
+
+ if len(appIds) == 0 || appIds[0] == "" {
+ return nil, errors.New("no appId")
+ }
+ if appId, err = strconv.Atoi(appIds[0]); err != nil {
+ return nil, fmt.Errorf("invalid appId")
+ }
+
+ if appId == 0 {
+ appId = appIdIn
+ }
+
+ file, ne := f.Open()
+ if ne != nil {
+ return nil, ne
+ }
+ defer file.Close()
+ buffer := bytes.Buffer{}
+ if _, err = buffer.ReadFrom(file); err != nil {
+ return nil, err
+ }
+
+ resp := &proxy.ShardBodyV2{
+ TaskId: taskId,
+ Pos: pos,
+ FileData: base64.StdEncoding.EncodeToString(buffer.Bytes()),
+ Type: typ,
+ TotalPos: total,
+ FileName: fileName,
+ AppId: appId,
+ Publish: publish,
+ }
+ return resp, nil
+}
diff --git a/read.md b/read.md
new file mode 100644
index 0000000..e69de29
diff --git a/web/Makefile b/web/Makefile
new file mode 100644
index 0000000..c1a4fea
--- /dev/null
+++ b/web/Makefile
@@ -0,0 +1,32 @@
+
+GOPATH:=$(shell go env GOPATH)
+
+GIT_BRANCH:=$(shell git symbolic-ref --short -q HEAD)
+GIT_VERSION:=$(shell git log --pretty=oneline -p -1 | head -n 1 | awk '{print $$1}')
+BUILD_DATE:=$(shell date '+%Y-%m-%d %H:%M:%S %z')
+BUILD_IP:=$(shell ifconfig | grep inet | grep -v grep | grep -v "inet6" | grep -v "127.0.0.1" | awk '{print $$2}')
+VERSON_PATH:=miao/pub/srvversion
+
+WEB_BUILD_FLAG:=-ldflags "-X '$(VERSON_PATH).GIT_BRANCH=$(GIT_BRANCH)' -X '$(VERSON_PATH).GIT_VERSION=$(GIT_VERSION)' -X '$(VERSON_PATH).BUILD_DATE=$(BUILD_DATE)' -X '$(VERSON_PATH).BUILD_IP=$(BUILD_IP)'"
+
+
+.PHONY: test deploy
+deploy:
+ go clean
+ rm -f web_mdcom_web
+ env GOOS=linux GOARCH=amd64 go build $(WEB_BUILD_FLAG) -o web_mdcom_web
+ tar czf web_mdcom_web.tar.gz web_mdcom_web
+ rm -f web_mdcom_web
+test:
+ go clean
+ rm -f web_mdcom_web
+ GOOS=linux GOARCH=amd64 go build $(WEB_BUILD_FLAG) -o web_mdcom_web
+ tar czf web_mdcom_web.tar.gz web_mdcom_web docs
+ rm -f web_mdcom_web
+
+localtest:
+ @echo "test, GOPATH=$(GOPATH)"
+ go build $(WEB_BUILD_FLAG) -o web
+
+swagger:
+ swag init --parseDependency --parseDepth 4
\ No newline at end of file
diff --git a/web/api/proto/content_tag.go b/web/api/proto/content_tag.go
new file mode 100644
index 0000000..20b371d
--- /dev/null
+++ b/web/api/proto/content_tag.go
@@ -0,0 +1,92 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/pubdef"
+)
+
+// ContentTagListReq web端统一标签分页列表请求
+type ContentTagListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"` // 标签ID
+ Name string `form:"name" json:"name"` // 标签名称,支持模糊查询
+ Type *contenttagmod.ContentTagType `form:"type" json:"type"` // 内容类型 1-视频 5-短剧
+ Status *bool `form:"status" json:"status"` // 状态 true-启用 false-停用
+}
+
+// ContentTagListResp web端统一标签列表项
+type ContentTagListResp struct {
+ Id int64 `json:"id"` // ID
+ Name string `json:"name"` // 标签名称
+ Slug string `json:"slug"` // slug
+ Types []contenttagmod.ContentTagType `json:"types"` // 支持类型 1-视频 5-短剧
+ TypeNames []string `json:"typeNames"` // 支持类型名称
+ Status bool `json:"status"` // 状态 true-启用 false-停用
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ VideoCount int64 `json:"videoCount"` // 绑定视频数量
+ PlayletCount int64 `json:"playletCount"` // 绑定短剧数量
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ CreatedById int64 `json:"createdById"` // 创建人ID
+ CreatedByName string `json:"createdByName"` // 创建人名称
+ UpdatedByName string `json:"updatedByName"` // 修改人名称
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 修改时间
+}
+
+// ContentTagListPageResp web端统一标签分页列表响应
+type ContentTagListPageResp struct {
+ response.BasePageResp
+}
+
+// ContentTagOptionReq web端统一标签下拉列表请求
+type ContentTagOptionReq struct {
+ Type *contenttagmod.ContentTagType `form:"type" json:"type"` // 内容类型 1-视频 5-短剧,不传返回全部启用标签
+}
+
+// ContentTagOptionResp web端统一标签下拉列表响应
+type ContentTagOptionResp struct {
+ List []*ContentTagOptionItem `json:"list"` // 标签下拉列表
+}
+
+// ContentTagOptionItem web端统一标签下拉项
+type ContentTagOptionItem struct {
+ Id int64 `json:"id"` // ID
+ Name string `json:"name"` // 标签名称
+ Types []contenttagmod.ContentTagType `json:"types"` // 支持类型 1-视频 5-短剧
+ TypeNames []string `json:"typeNames"` // 支持类型名称
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ BindCount int64 `json:"bindCount"` // 当前查询类型的绑定数量,不传type时返回0
+}
+
+// ContentTagAddReq web端新增统一标签请求
+type ContentTagAddReq struct {
+ Name string `json:"name" binding:"required"` // 标签名称
+ Slug string `json:"slug"` // slug,不传则按 SEO 关键词或名称自动生成
+ Types []contenttagmod.ContentTagType `json:"types" binding:"required"` // 支持类型 1-视频 5-短剧
+ Status *bool `json:"status" binding:"required"` // 状态 true-启用 false-停用
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// ContentTagAddResp web端新增统一标签响应
+type ContentTagAddResp struct {
+ Id int64 `json:"id"` // ID
+}
+
+// ContentTagUpdateReq web端更新统一标签请求
+type ContentTagUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // ID
+ Name *string `json:"name"` // 标签名称
+ Slug *string `json:"slug"` // slug,历史数据为空时更新会自动生成
+ Types *[]contenttagmod.ContentTagType `json:"types"` // 支持类型 1-视频 5-短剧
+ Status *bool `json:"status"` // 状态 true-启用 false-停用
+ Rank *int64 `json:"rank"` // 排序,越大越靠前
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// ContentTagDelReq web端删除统一标签请求
+type ContentTagDelReq struct {
+ Id int64 `json:"id" binding:"required"` // ID
+}
diff --git a/web/api/proto/home_banner.go b/web/api/proto/home_banner.go
new file mode 100644
index 0000000..fefcf95
--- /dev/null
+++ b/web/api/proto/home_banner.go
@@ -0,0 +1,67 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+)
+
+// HomeBannerListReq web端首页 banner 分页列表请求
+type HomeBannerListReq struct {
+ PageNum int64 `form:"pageNum" json:"pageNum"` // 页码,默认1
+ PageSize int64 `form:"pageSize" json:"pageSize"` // 每页数量,默认10,最大100
+ Id *int64 `form:"id" json:"id"` // 首页 banner ID
+ Name string `form:"name" json:"name"` // 名称,支持模糊查询
+ Status *bool `form:"status" json:"status"` // 状态 true-启用 false-停用
+}
+
+// HomeBannerListResp web端首页 banner 列表项
+type HomeBannerListResp struct {
+ Id int64 `json:"id"` // ID
+ Name string `json:"name"` // 名称
+ Image string `json:"image"` // 图片
+ VerticalImage string `json:"verticalImage"` // 竖版图片
+ Url string `json:"url"` // URL
+ Rank int64 `json:"rank"` // 排序
+ Status bool `json:"status"` // 状态 true-启用 false-停用
+ CreatedById int64 `json:"createdById"` // 创建人ID
+ CreatedByName string `json:"createdByName"` // 创建人名称
+ UpdatedByName string `json:"updatedByName"` // 修改人名称
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 修改时间
+}
+
+// HomeBannerListPageResp web端首页 banner 分页列表响应
+type HomeBannerListPageResp struct {
+ response.BasePageResp
+}
+
+// HomeBannerAddReq web端新增首页 banner 请求
+type HomeBannerAddReq struct {
+ Name string `json:"name" binding:"required"` // 名称
+ Image string `json:"image" binding:"required"` // 图片
+ VerticalImage string `json:"verticalImage"` // 竖版图片
+ Url string `json:"url"` // URL
+ Rank int64 `json:"rank"` // 排序
+ Status *bool `json:"status" binding:"required"` // 状态 true-启用 false-停用
+}
+
+// HomeBannerAddResp web端新增首页 banner 响应
+type HomeBannerAddResp struct {
+ Id int64 `json:"id"` // ID
+}
+
+// HomeBannerUpdateReq web端更新首页 banner 请求
+type HomeBannerUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // ID
+ Name *string `json:"name"` // 名称
+ Image *string `json:"image"` // 图片
+ VerticalImage *string `json:"verticalImage"` // 竖版图片
+ Url *string `json:"url"` // URL
+ Rank *int64 `json:"rank"` // 排序
+ Status *bool `json:"status"` // 状态 true-启用 false-停用
+}
+
+// HomeBannerDelReq web端删除首页 banner 请求
+type HomeBannerDelReq struct {
+ Id int64 `json:"id" binding:"required"` // ID
+}
diff --git a/web/api/proto/playlet.go b/web/api/proto/playlet.go
new file mode 100644
index 0000000..70c2518
--- /dev/null
+++ b/web/api/proto/playlet.go
@@ -0,0 +1,355 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletListReq web端短剧分页列表请求
+type PlayletListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"` // 短剧ID
+ CategoryId *int64 `form:"categoryId" json:"categoryId"` // 分类ID
+ TagId *int64 `form:"tagId" json:"tagId"` // 标签ID
+ ThemeId *int64 `form:"themeId" json:"themeId"` // 题材ID
+ ContentType *playletmod.ContentType `form:"contentType" json:"contentType"` // 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ PayType *playletmod.PayType `form:"payType" json:"payType"` // 支付方式,0-免费、1-金币、2-VIP
+ Status *playletmod.Status `form:"status" json:"status"` // 上架状态,0-下架、1-上架
+ IsFeatured *bool `form:"isFeatured" json:"isFeatured"` // 是否爆剧,true-是、false-否
+ IsOriginal *bool `form:"isOriginal" json:"isOriginal"` // 是否原创:true只查原创,false只查非原创,不传查全部
+ ShowOriginal *bool `form:"showOriginal" json:"showOriginal"` // 页面是否展示原创标识 不传查全部
+ IsRecommend *bool `form:"isRecommend" json:"isRecommend"` // 是否推荐,true-推荐、false-不推荐
+ ShowOnBanner *bool `form:"showOnBanner" json:"showOnBanner"` // 是否显示在首页banner,true-显示、false-不显示
+ Title string `form:"title" json:"title"` // 短剧标题,支持模糊查询
+ OperatorName string `form:"operatorName" json:"operatorName"` // 最后操作人名称,支持模糊查询
+}
+
+// PlayletListResp web端短剧列表项
+type PlayletListResp struct {
+ Id int64 `json:"id"` // 短剧ID
+ Status playletmod.Status `json:"status"` // 上架状态,0-下架、1-上架
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧,true-是、false-否
+ IsOriginal bool `json:"isOriginal"` // 是否原创:后台新增为true,媒资导入为false
+ ShowOriginal bool `json:"showOriginal"` // 页面是否展示原创标识
+ MediaBusiness string `json:"mediaBusiness"` // 媒资业务来源,按媒资标题前缀记录,如 CGDJ;空表示未标记
+ IsRecommend bool `json:"isRecommend"` // 是否推荐,true-推荐、false-不推荐
+ ShowOnBanner bool `json:"showOnBanner"` // 是否显示在首页banner,true-显示、false-不显示
+ Banner string `json:"banner"` // banner图
+ ReviewAt time.Time `json:"reviewAt"` // 审核时间
+ OnlineAt time.Time `json:"onlineAt"` // 上架时间
+ Title string `json:"title"` // 标题
+ Slug string `json:"slug"` // slug
+ Desc string `json:"desc"` // 简介
+ Anchor string `json:"anchor"` // 作者/演员
+ Cover string `json:"cover"` // 封面原图
+ CompressedCover string `json:"compressedCover"` // 压缩封面
+ Preview string `json:"preview"` // 预告
+ CategoryIds []int64 `json:"categoryIds"` // 分类ID集合
+ CategoryBinds []PlayletCategoryBind `json:"categoryBinds"` // 各分类下的排序
+ CategoryRank int64 `json:"categoryRank"` // 当前筛选分类下的排序
+ CategoryNames []string `json:"categoryNames"` // 分类名称集合
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合
+ ThemeNames []string `json:"themeNames"` // 题材名称集合
+ TagIds []int64 `json:"tagIds"` // 标签ID集合
+ TagNames []string `json:"tagNames"` // 标签名称集合
+ OldTags []string `json:"oldTags"` // 媒资库原标签名称
+ ContentType playletmod.ContentType `json:"contentType"` // 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ PayType playletmod.PayType `json:"payType"` // 支付方式,0-免费、1-金币、2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时生效
+ TotalEpisode int64 `json:"totalEpisode"` // 总集数
+ FreeEpisodeCount int64 `json:"freeEpisodeCount"` // 官网开放集数 1至1000表示开放第1集到第N集 -1表示开放全集 历史未配置按2集返回
+ UpdateStatus int `json:"updateStatus"` // 连载状态,1-连载中、2-已完结
+ TopRank int64 `json:"topRank"` // 置顶排序
+ RecommendRank int64 `json:"recommendRank"` // 推荐排序
+ CountComment int64 `json:"countComment"` // 评论数
+ CountBuy int64 `json:"countBuy"` // 购买数
+ CountBrowse int64 `json:"countBrowse"` // 浏览数
+ VirtualCountBrowse int64 `json:"virtualCountBrowse"` // 后台虚拟观看数
+ DisplayCountBrowse int64 `json:"displayCountBrowse"` // 展示观看数
+ HeatValue int64 `json:"heatValue"` // 热度值
+ VirtualHeatValue int64 `json:"virtualHeatValue"` // 后台虚拟热度值
+ DisplayHeatValue int64 `json:"displayHeatValue"` // 展示热度值
+ CountCollect int64 `json:"countCollect"` // 收藏数
+ CountLike int64 `json:"countLike"` // 真实点赞数
+ VirtualCountLike int64 `json:"virtualCountLike"` // 虚拟点赞数
+ DisplayCountLike int64 `json:"displayCountLike"` // 展示点赞数=真实点赞数+虚拟点赞数
+ CountDisLike int64 `json:"countDisLike"` // 不喜欢数
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ OperatorId int64 `json:"operatorId"` // 最后操作人ID
+ OperatorName string `json:"operatorName"` // 最后操作人名称
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 更新时间
+}
+
+// PlayletListPageResp web端短剧分页列表响应
+type PlayletListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletCategoryBind 短剧在指定分类下的排序
+type PlayletCategoryBind struct {
+ Id int64 `json:"id"` // 分类ID
+ Rank int64 `json:"rank"` // 排序值,越大越靠前,不填为0
+}
+
+// PlayletDelReq 删除短剧请求
+type PlayletDelReq struct {
+ Ids []int64 `json:"ids" binding:"required"` // 短剧ID集合
+}
+
+// PlayletAddReq web端新增短剧请求
+type PlayletAddReq struct {
+ CategoryIds []int64 `json:"categoryIds"` // 分类ID集合
+ CategoryBinds []PlayletCategoryBind `json:"categoryBinds"` // 各分类下的排序,优先于categoryIds
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合
+ Title string `json:"title" binding:"required"` // 标题
+ Slug string `json:"slug"` // slug,不传则按 SEO 关键词或标题自动生成
+ Desc string `json:"desc"` // 简介
+ Anchor string `json:"anchor"` // 作者/演员
+ Cover string `json:"cover"` // 封面原图
+ Preview string `json:"preview"` // 预告
+ TagIds []int64 `json:"tagIds"` // 标签ID集合
+ ContentType playletmod.ContentType `json:"contentType"` // 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ PayType playletmod.PayType `json:"payType"` // 支付方式,0-免费、1-金币、2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时生效
+ TotalEpisode int64 `json:"totalEpisode"` // 总集数
+ FreeEpisodeCount *int64 `json:"freeEpisodeCount"` // 官网开放集数 不传默认2集 1至1000表示开放第1集到第N集 -1表示开放全集 0无效
+ TopRank int64 `json:"topRank"` // 置顶排序
+ RecommendRank int64 `json:"recommendRank"` // 推荐排序
+ Status playletmod.Status `json:"status"` // 上架状态,0-下架、1-上架
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧,true-是、false-否
+ ShowOriginal bool `json:"showOriginal"` // 页面是否展示原创标识
+ IsRecommend bool `json:"isRecommend"` // 是否推荐,true-推荐、false-不推荐
+ ShowOnBanner bool `json:"showOnBanner"` // 是否显示在首页banner
+ Banner string `json:"banner"` // banner图
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletAddResp web端新增短剧响应
+type PlayletAddResp struct {
+ Id int64 `json:"id"` // 短剧ID
+}
+
+// PlayletUpdateReq web端更新短剧请求
+type PlayletUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // 短剧ID
+ Status *playletmod.Status `json:"status"` // 上架状态,0-下架、1-上架
+ IsFeatured *bool `json:"isFeatured"` // 是否爆剧,true-是、false-否
+ ShowOriginal *bool `json:"showOriginal"` // 页面是否展示原创标识 不传保持原值
+ IsRecommend *bool `json:"isRecommend"` // 是否推荐,true-推荐、false-不推荐
+ ShowOnBanner *bool `json:"showOnBanner"` // 是否显示在首页banner
+ Banner *string `json:"banner"` // banner图
+ CategoryIds *[]int64 `json:"categoryIds"` // 分类ID集合,传空数组表示清空分类
+ CategoryBinds *[]PlayletCategoryBind `json:"categoryBinds"` // 各分类下的排序,优先于categoryIds
+ ThemeIds *[]int64 `json:"themeIds"` // 题材ID集合
+ Title *string `json:"title"` // 标题
+ Slug *string `json:"slug"` // slug,历史数据为空时更新会自动生成
+ Desc *string `json:"desc"` // 简介
+ Anchor *string `json:"anchor"` // 作者/演员
+ Cover *string `json:"cover"` // 封面原图
+ Preview *string `json:"preview"` // 预告
+ TagIds *[]int64 `json:"tagIds"` // 标签ID集合,传空数组表示清空标签
+ ContentType *playletmod.ContentType `json:"contentType"` // 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ PayType *playletmod.PayType `json:"payType"` // 支付方式,0-免费、1-金币、2-VIP
+ SingleCoins *int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时生效
+ TotalEpisode *int64 `json:"totalEpisode"` // 总集数
+ FreeEpisodeCount *int64 `json:"freeEpisodeCount"` // 官网可播放范围 不传则不修改 传1至1000时仅开放第1集至指定集数 传-1开放全部剧集 传0报参数错误
+ TopRank *int64 `json:"topRank"` // 置顶排序
+ RecommendRank *int64 `json:"recommendRank"` // 推荐排序
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletPayTypeSetReq web端单/批量设置短剧默认支付方式请求
+type PlayletPayTypeSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ PayType playletmod.PayType `json:"payType"` // 支付方式,0-免费、1-金币、2-VIP
+ SingleCoins *int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时必填且需大于0
+}
+
+// PlayletWatermarkResetReq web端按短剧ID批量重置剧集水印请求
+type PlayletWatermarkResetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条,传多个表示批量
+}
+
+// PlayletChapterListReq web端短剧剧集分页列表请求
+type PlayletChapterListReq struct {
+ pubdef.Page
+ PlayletId *int64 `form:"playletId" json:"playletId"` // 短剧ID
+ Id *int64 `form:"id" json:"id"` // 剧集ID
+ WatermarkStatus *playletchaptermod.WatermarkStatus `form:"watermarkStatus" json:"watermarkStatus"` // 水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败
+}
+
+// PlayletChapterListResp web端短剧剧集列表项
+type PlayletChapterListResp struct {
+ Id int64 `json:"id"` // 剧集ID
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ MediaContentId uint `json:"mediaContentId"` // 媒资中心剧集ID
+ CurrentEpisode int64 `json:"currentEpisode"` // 第几集
+ Title string `json:"title"` // 剧集标题
+ Md5 string `json:"md5"` // 媒体md5
+ Hash string `json:"hash"` // 媒体hash
+ Height int64 `json:"height"` // 视频高度
+ Width int64 `json:"width"` // 视频宽度
+ Ratio float64 `json:"ratio"` // 宽高比
+ VideoUrl string `json:"videoUrl"` // 原片视频地址
+ WatermarkVideoUrl string `json:"watermarkVideoUrl"` // 加水印后的视频地址
+ MediaSize int64 `json:"mediaSize"` // 资源大小
+ MediaTime int64 `json:"mediaTime"` // 影片长度,单位秒
+ CountComment int64 `json:"countComment"` // 评论数
+ CountBuy int64 `json:"countBuy"` // 购买数
+ CountBrowse int64 `json:"countBrowse"` // 浏览数
+ CountCollect int64 `json:"countCollect"` // 收藏数
+ CountLike int64 `json:"countLike"` // 点赞数
+ CountDisLike int64 `json:"countDisLike"` // 不喜欢数
+ PayType playletchaptermod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时生效
+ WatermarkStatus playletchaptermod.WatermarkStatus `json:"watermarkStatus"` // 水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败
+ MediaSourceId string `json:"mediaSourceId"` // media切片服原片ID
+ MediaTaskId string `json:"mediaTaskId"` // media切片服水印任务ID
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 更新时间
+}
+
+// PlayletChapterListPageResp web端短剧剧集分页列表响应
+type PlayletChapterListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletChapterAddReq web端新增短剧剧集请求
+type PlayletChapterAddReq struct {
+ PlayletId int64 `json:"playletId" binding:"required"` // 短剧ID
+ CurrentEpisode int64 `json:"currentEpisode" binding:"required"` // 第几集
+ Title string `json:"title"` // 剧集标题
+ Md5 string `json:"md5"` // 媒体md5
+ Hash string `json:"hash"` // 媒体hash
+ Height int64 `json:"height"` // 视频高度
+ Width int64 `json:"width"` // 视频宽度
+ Ratio float64 `json:"ratio"` // 宽高比
+ VideoUrl string `json:"videoUrl"` // 原片视频地址
+ MediaSize int64 `json:"mediaSize"` // 资源大小
+ MediaTime int64 `json:"mediaTime"` // 影片长度,单位秒
+ PayType playletchaptermod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时生效
+ MediaSourceId string `json:"mediaSourceId"` // media切片服原片ID,上传正片已创建原片时直接沿用
+}
+
+// PlayletChapterAddResp web端新增短剧剧集响应
+type PlayletChapterAddResp struct {
+ Id int64 `json:"id"` // 剧集ID
+}
+
+// PlayletChapterUpdateReq web端编辑短剧剧集请求
+type PlayletChapterUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // 剧集ID
+ CurrentEpisode *int64 `json:"currentEpisode"` // 第几集
+ Title *string `json:"title"` // 剧集标题
+ Md5 *string `json:"md5"` // 媒体md5
+ Hash *string `json:"hash"` // 媒体hash
+ Height *int64 `json:"height"` // 视频高度
+ Width *int64 `json:"width"` // 视频宽度
+ Ratio *float64 `json:"ratio"` // 宽高比
+ VideoUrl *string `json:"videoUrl"` // 原片视频地址
+ MediaSize *int64 `json:"mediaSize"` // 资源大小
+ MediaTime *int64 `json:"mediaTime"` // 影片长度,单位秒
+ PayType *playletchaptermod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins *int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时生效
+}
+
+// PlayletChapterDelReq web端单/批量删除短剧剧集请求
+type PlayletChapterDelReq struct {
+ Ids []int64 `json:"ids"` // 剧集ID集合,传1个表示单条删除,传多个表示批量删除
+}
+
+// PlayletChapterSyncReq web端手动同步短剧剧集请求
+type PlayletChapterSyncReq struct {
+ PlayletIds []int64 `json:"playletIds" binding:"required"` // 短剧ID集合,传1个表示单条同步,传多个表示批量同步
+}
+
+// PlayletChapterSyncResult web端手动同步短剧剧集结果项
+type PlayletChapterSyncResult struct {
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ NewChapterCount int `json:"newChapterCount"` // 本次新增剧集数量
+ Success bool `json:"success"` // 是否同步成功
+ Reason string `json:"reason"` // 失败原因
+}
+
+// PlayletChapterSyncResp web端手动同步短剧剧集响应
+type PlayletChapterSyncResp struct {
+ Total int `json:"total"` // 同步总数
+ SuccessCount int `json:"successCount"` // 成功数量
+ FailedCount int `json:"failedCount"` // 失败数量
+ List []*PlayletChapterSyncResult `json:"list"` // 同步结果集合
+}
+
+// PlayletChapterPayTypeSetReq web端单/批量设置短剧剧集支付方式请求
+type PlayletChapterPayTypeSetReq struct {
+ Ids []int64 `json:"ids"` // 剧集ID集合,传1个表示单条设置,传多个表示批量设置
+ PayType playletchaptermod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins *int64 `json:"singleCoins"` // 单集金币,支付方式为1-金币时必填且需大于0
+}
+
+// PlayletStatusSetReq web端单/批量设置短剧上下架请求
+type PlayletStatusSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ Status playletmod.Status `json:"status"` // 上架状态,0-下架、1-上架
+}
+
+// PlayletRecommendSetReq web端单/批量设置短剧推荐请求
+type PlayletRecommendSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ IsRecommend bool `json:"isRecommend"` // 是否推荐,true-推荐、false-不推荐
+}
+
+// PlayletFeaturedSetReq web端单/批量设置短剧是否爆剧请求
+type PlayletFeaturedSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧,true-是、false-否
+}
+
+// PlayletBannerSetReq web端单/批量设置短剧是否显示在首页banner请求
+type PlayletBannerSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ ShowOnBanner bool `json:"showOnBanner"` // 是否显示在首页banner,true-显示、false-不显示
+}
+
+// PlayletCategorySetReq web端单/批量设置短剧分类请求
+type PlayletCategorySetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ CategoryIds *[]int64 `json:"categoryIds"` // 分类ID集合,可设置多个分类,传空数组表示清空分类,不传表示参数错误
+}
+
+// PlayletTagSetReq web端单/批量设置短剧标签请求
+type PlayletTagSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ TagIds *[]int64 `json:"tagIds"` // 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
+}
+
+// PlayletThemeSetReq web端单/批量设置短剧题材请求
+type PlayletThemeSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合,可设置多个题材
+}
+
+// PlayletVirtualLikeSetReq web端单/批量设置短剧虚拟点赞数请求
+type PlayletVirtualLikeSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ MinVirtualCountLike int64 `json:"minVirtualCountLike"` // 虚拟点赞数最小值,必须大于等于0
+ MaxVirtualCountLike int64 `json:"maxVirtualCountLike"` // 虚拟点赞数最大值,必须大于等于最小值
+}
+
+// PlayletVirtualBrowseSetReq web端单/批量设置短剧虚拟观看数请求
+type PlayletVirtualBrowseSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合
+ MinVirtualCountBrowse int64 `json:"minVirtualCountBrowse"` // 虚拟观看数最小值
+ MaxVirtualCountBrowse int64 `json:"maxVirtualCountBrowse"` // 虚拟观看数最大值
+}
+
+// PlayletVirtualHeatSetReq web端单/批量设置短剧虚拟热度请求
+type PlayletVirtualHeatSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合
+ MinVirtualHeatValue int64 `json:"minVirtualHeatValue"` // 虚拟热度最小值
+ MaxVirtualHeatValue int64 `json:"maxVirtualHeatValue"` // 虚拟热度最大值
+}
diff --git a/web/api/proto/playlet_category.go b/web/api/proto/playlet_category.go
new file mode 100644
index 0000000..7a776f6
--- /dev/null
+++ b/web/api/proto/playlet_category.go
@@ -0,0 +1,67 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletCategoryListReq web端分类分页列表请求
+type PlayletCategoryListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"` // 分类ID
+ Name string `form:"name" json:"name"` // 分类名称,支持模糊查询
+ Status *bool `form:"status" json:"status"` // 启用状态
+}
+
+// PlayletCategoryListResp web端分类列表项
+type PlayletCategoryListResp struct {
+ Id int64 `json:"id"` // 分类ID
+ Name string `json:"name"` // 分类名称
+ Slug string `json:"slug"` // slug
+ Icon string `json:"icon"` // 分类图标
+ Status bool `json:"status"` // 启用状态
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ Remark string `json:"remark"` // 备注
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 更新时间
+}
+
+// PlayletCategoryListPageResp web端分类分页列表响应
+type PlayletCategoryListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletCategoryAddReq web端新增分类请求
+type PlayletCategoryAddReq struct {
+ Name string `json:"name" binding:"required"` // 分类名称
+ Slug string `json:"slug"` // slug,不传则按 SEO 关键词或名称自动生成
+ Icon string `json:"icon"` // 分类图标
+ Status bool `json:"status"` // 启用状态
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ Remark string `json:"remark"` // 备注
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletCategoryAddResp web端新增分类响应
+type PlayletCategoryAddResp struct {
+ Id int64 `json:"id"` // 分类ID
+}
+
+// PlayletCategoryUpdateReq web端更新分类请求
+type PlayletCategoryUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // 分类ID
+ Name *string `json:"name"` // 分类名称
+ Slug *string `json:"slug"` // slug,历史数据为空时更新会自动生成
+ Icon *string `json:"icon"` // 分类图标
+ Status *bool `json:"status"` // 启用状态
+ Rank *int64 `json:"rank"` // 排序,越大越靠前
+ Remark *string `json:"remark"` // 备注
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletCategoryDelReq web端删除分类请求
+type PlayletCategoryDelReq struct {
+ Id int64 `json:"id" binding:"required"` // 分类ID
+}
diff --git a/web/api/proto/playlet_home_config.go b/web/api/proto/playlet_home_config.go
new file mode 100644
index 0000000..dbabb0b
--- /dev/null
+++ b/web/api/proto/playlet_home_config.go
@@ -0,0 +1,114 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletHomeConfigListReq web端首页配置分页列表请求
+type PlayletHomeConfigListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"`
+ Slug string `form:"slug" json:"slug"` // 页面路径标识
+ Title string `form:"title" json:"title"`
+ Desc string `form:"desc" json:"desc"`
+ Label string `form:"label" json:"label"`
+ Style string `form:"style" json:"style"`
+ ModuleType *int64 `form:"moduleType" json:"moduleType"`
+ DataType *int64 `form:"dataType" json:"dataType"` // 1全部上架短剧 2全部上架视频
+ RecommendType *int64 `form:"recommendType" json:"recommendType"`
+ CategoryId *int64 `form:"categoryId" json:"categoryId"`
+ Enabled *bool `form:"enabled" json:"enabled"`
+}
+
+// PlayletHomeConfigListResp web端首页配置列表项
+type PlayletHomeConfigListResp struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"` // 页面路径标识
+ Title string `json:"title"`
+ Desc string `json:"desc"`
+ Label string `json:"label"`
+ Style string `json:"style"`
+ CardBackgroundImage string `json:"cardBackgroundImage"`
+ ModuleType int64 `json:"moduleType"`
+ DataType int64 `json:"dataType"` // 1全部上架短剧 2全部上架视频
+ RecommendType int64 `json:"recommendType"`
+ CategoryId int64 `json:"categoryId"`
+ TopicIds []int64 `json:"topicIds"` // 专题ID集合 短剧为题材ID 视频为分类ID
+ Rank int64 `json:"rank"`
+ Enabled bool `json:"enabled"`
+ Limit int64 `json:"limit"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+type PlayletHomeConfigListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletHomeTopicOptionsReq 首页专题下拉选项分页请求
+type PlayletHomeTopicOptionsReq struct {
+ pubdef.Page
+ DataType int64 `form:"dataType" json:"dataType" binding:"required"` // 1短剧题材 2视频分类
+ ConfigId int64 `form:"configId" json:"configId"` // 编辑时传当前首页配置ID,用于回显已选项
+}
+
+// PlayletHomeTopicOption 首页专题下拉选项
+type PlayletHomeTopicOption struct {
+ Id int64 `json:"id"` // 题材或视频分类ID
+ Name string `json:"name"` // 题材或视频分类名称
+ Status bool `json:"status"` // 上架状态,已选项下架时仍可回显
+}
+
+// PlayletHomeTopicOptionsResp 首页专题下拉选项分页响应
+type PlayletHomeTopicOptionsResp struct {
+ List []*PlayletHomeTopicOption `json:"list"` // 当前页可选项
+ SelectedList []*PlayletHomeTopicOption `json:"selectedList"` // 当前配置已选项,独立于分页
+ TotalCount int64 `json:"totalCount"` // 可选项总数
+ PageNum int64 `json:"pageNum"` // 当前页码
+ PageSize int64 `json:"pageSize"` // 每页数量
+}
+
+type PlayletHomeConfigAddReq struct {
+ Title string `json:"title" binding:"required"`
+ Slug string `json:"slug"` // 页面路径标识 不传则按标题自动生成
+ Desc string `json:"desc"`
+ Label string `json:"label"`
+ Style string `json:"style" binding:"required"`
+ CardBackgroundImage string `json:"cardBackgroundImage"`
+ ModuleType int64 `json:"moduleType"`
+ DataType int64 `json:"dataType"` // 1全部上架短剧 2全部上架视频
+ RecommendType int64 `json:"recommendType" binding:"required"`
+ CategoryId int64 `json:"categoryId"`
+ TopicIds []int64 `json:"topicIds"` // 专题ID集合 短剧为题材ID 视频为分类ID
+ Rank int64 `json:"rank"`
+ Enabled bool `json:"enabled"`
+ Limit int64 `json:"limit"`
+}
+
+type PlayletHomeConfigAddResp struct {
+ Id int64 `json:"id"`
+}
+
+type PlayletHomeConfigUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Slug *string `json:"slug"` // 不传保持原值 历史空值自动生成
+ Title *string `json:"title"`
+ Desc *string `json:"desc"`
+ Label *string `json:"label"`
+ Style *string `json:"style"`
+ CardBackgroundImage *string `json:"cardBackgroundImage"`
+ ModuleType *int64 `json:"moduleType"`
+ DataType *int64 `json:"dataType"` // 1全部上架短剧 2全部上架视频
+ RecommendType *int64 `json:"recommendType"`
+ CategoryId *int64 `json:"categoryId"`
+ TopicIds *[]int64 `json:"topicIds"` // 不传保持原值 传空数组清空专题
+ Rank *int64 `json:"rank"`
+ Enabled *bool `json:"enabled"`
+ Limit *int64 `json:"limit"`
+}
+
+type PlayletHomeConfigDelReq struct {
+ Ids []int64 `json:"ids" binding:"required"`
+}
diff --git a/web/api/proto/playlet_resource.go b/web/api/proto/playlet_resource.go
new file mode 100644
index 0000000..16d4f7f
--- /dev/null
+++ b/web/api/proto/playlet_resource.go
@@ -0,0 +1,91 @@
+package proto
+
+// PlayletResourceListReq web端短剧媒资列表请求
+type PlayletResourceListReq struct {
+ Title *string `form:"title" json:"title"` // 标题
+ Tag *string `form:"tag" json:"tag"` // 标签
+ MediaType *int `form:"media_type" json:"media_type"` // 媒体类型 1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧
+ SyncType *int `form:"sync_type" json:"sync_type"` // 同步类型 1-全量同步 2-增量同步
+ PageNumber int `form:"pageNumber" json:"pageNumber"` // 页码
+ PageSize int `form:"pageSize" json:"pageSize"` // 个数
+}
+
+// PlayletResourceListItem web端短剧媒资列表项
+type PlayletResourceListItem struct {
+ ID uint `json:"id"`
+ VloggerId uint `json:"vlogger_id"`
+ Title string `json:"title"` // 标题
+ SaleCoin uint `json:"sale_coin"` // 整部售价
+ HorizontalCover string `json:"horizontal_cover"` // 横版封面
+ VerticalCover string `json:"vertical_cover"` // 竖版封面
+ Summary string `json:"summary"` // 简介
+ TotalEpisode uint `json:"total_episode"` // 总集数
+ UpdateStatus int `json:"update_status"` // 更新状态 1-连载中 2-已完结
+ MediaType int `json:"media_type"` // 媒体类型
+ Anchor string `json:"anchor"` // 作者
+ TagsText string `json:"tags_text"` // 标签文字
+ DataVersion int8 `json:"data_version"` // 数据版本
+ TerminalCdo string `json:"terminal_cdo"` // 上传者
+ CreateTime int64 `json:"create_time"` // 创建时间
+ UpdateTime int64 `json:"update_time"` // 修改时间
+ IsAdd bool `json:"isAdd"` // 是否已加入后台数据
+}
+
+// PlayletResourceListResp web端短剧媒资列表响应
+type PlayletResourceListResp struct {
+ List []*PlayletResourceListItem `json:"list"`
+ Total uint `json:"total"`
+}
+
+// PlayletResourceUseReq web端短剧媒资入库请求
+type PlayletResourceUseReq struct {
+ Ids []uint `json:"ids"` // 媒资中心资源ID集合
+}
+
+// PlayletResourceChapterListReq web端短剧媒资剧集列表请求
+type PlayletResourceChapterListReq struct {
+ Id uint `form:"id" json:"id" binding:"required"` // 媒资中心资源ID
+}
+
+// PlayletResourceChapterItem web端短剧媒资剧集项
+type PlayletResourceChapterItem struct {
+ Id uint `json:"id"` // 媒资内容ID
+ EpisodeNumber int `json:"episodeNumber"` // 第几集
+ Title string `json:"title"` // 章节名
+ Cover string `json:"cover"` // 封面
+ VideoUrl string `json:"videoUrl"` // 播放地址
+ Height uint `json:"height"` // 高
+ Width uint `json:"width"` // 宽
+ MediaSize uint `json:"mediaSize"` // 资源大小
+ PlayTime uint `json:"playTime"` // 播放时长(秒)
+ SaleCoin uint `json:"saleCoin"` // 单集售价
+ HashId string `json:"hashId"` // 资源hash
+ CreateTime int64 `json:"createTime"` // 创建时间
+ UpdateTime int64 `json:"updateTime"` // 更新时间
+}
+
+// PlayletResourceChapterListResp web端短剧媒资剧集列表响应
+type PlayletResourceChapterListResp struct {
+ MediaId uint `json:"mediaId"` // 媒资中心资源ID
+ Title string `json:"title"` // 短剧标题
+ List []*PlayletResourceChapterItem `json:"list"` // 剧集列表
+ Total int `json:"total"` // 剧集数量
+}
+
+// PlayletResourceUseSuccess web端短剧媒资入库成功项
+type PlayletResourceUseSuccess struct {
+ MediaId uint `json:"mediaId"`
+ PlayletId int64 `json:"playletId"`
+}
+
+// PlayletResourceUseFailed web端短剧媒资入库失败项
+type PlayletResourceUseFailed struct {
+ MediaId uint `json:"mediaId"`
+ Reason string `json:"reason"`
+}
+
+// PlayletResourceUseResp web端短剧媒资入库响应
+type PlayletResourceUseResp struct {
+ Success []*PlayletResourceUseSuccess `json:"success"`
+ Failed []*PlayletResourceUseFailed `json:"failed"`
+}
diff --git a/web/api/proto/playlet_tag.go b/web/api/proto/playlet_tag.go
new file mode 100644
index 0000000..9df5fcb
--- /dev/null
+++ b/web/api/proto/playlet_tag.go
@@ -0,0 +1,58 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletTagListReq web端标签分页列表请求
+type PlayletTagListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"`
+ Name string `form:"name" json:"name"`
+ Status *bool `form:"status" json:"status"`
+}
+
+// PlayletTagListResp web端标签列表项
+type PlayletTagListResp struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Status bool `json:"status"`
+ Rank int64 `json:"rank"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// PlayletTagListPageResp web端标签分页列表响应
+type PlayletTagListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletTagAddReq web端新增标签请求
+type PlayletTagAddReq struct {
+ Name string `json:"name" binding:"required"`
+ Status bool `json:"status"`
+ Rank int64 `json:"rank"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletTagAddResp web端新增标签响应
+type PlayletTagAddResp struct {
+ Id int64 `json:"id"`
+}
+
+// PlayletTagUpdateReq web端更新标签请求
+type PlayletTagUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Name *string `json:"name"`
+ Status *bool `json:"status"`
+ Rank *int64 `json:"rank"`
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletTagDelReq web端删除标签请求
+type PlayletTagDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/api/proto/playlet_theme.go b/web/api/proto/playlet_theme.go
new file mode 100644
index 0000000..0cd4e93
--- /dev/null
+++ b/web/api/proto/playlet_theme.go
@@ -0,0 +1,64 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletThemeListReq web端题材分页列表请求
+type PlayletThemeListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"` // 题材ID
+ Name string `form:"name" json:"name"` // 题材名称,支持模糊查询
+ Status *bool `form:"status" json:"status"` // 上架状态
+}
+
+// PlayletThemeListResp web端题材列表项
+type PlayletThemeListResp struct {
+ Id int64 `json:"id"` // 题材ID
+ Name string `json:"name"` // 题材名称
+ Slug string `json:"slug"` // slug
+ Icon string `json:"icon"` // 题材图标
+ Status bool `json:"status"` // 上架状态
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 更新时间
+}
+
+// PlayletThemeListPageResp web端题材分页列表响应
+type PlayletThemeListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletThemeAddReq web端新增题材请求
+type PlayletThemeAddReq struct {
+ Name string `json:"name" binding:"required"` // 题材名称
+ Slug string `json:"slug"` // slug,不传则按 SEO 关键词或名称自动生成
+ Icon string `json:"icon"` // 题材图标
+ Status bool `json:"status"` // 上架状态
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletThemeAddResp web端新增题材响应
+type PlayletThemeAddResp struct {
+ Id int64 `json:"id"` // 题材ID
+}
+
+// PlayletThemeUpdateReq web端更新题材请求
+type PlayletThemeUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // 题材ID
+ Name *string `json:"name"` // 题材名称
+ Slug *string `json:"slug"` // slug,历史数据为空时更新会自动生成
+ Icon *string `json:"icon"` // 题材图标
+ Status *bool `json:"status"` // 上架状态
+ Rank *int64 `json:"rank"` // 排序,越大越靠前
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletThemeDelReq web端删除题材请求
+type PlayletThemeDelReq struct {
+ Id int64 `json:"id" binding:"required"` // 题材ID
+}
diff --git a/web/api/proto/slug.go b/web/api/proto/slug.go
new file mode 100644
index 0000000..f3c5a44
--- /dev/null
+++ b/web/api/proto/slug.go
@@ -0,0 +1,21 @@
+package proto
+
+// SlugSyncReq 后台补齐历史空 slug 请求
+type SlugSyncReq struct {
+ MaxSize int64 `json:"maxSize"` // 本次最多处理的空 slug 记录数,默认500,最大2000
+ Type string `json:"type"` // 可选,指定类型;空则按顺序处理全部
+}
+
+// SlugSyncResp 后台补齐历史空 slug 响应
+type SlugSyncResp struct {
+ VideoUpdated int64 `json:"videoUpdated"` // 本次补齐的视频数
+ VideoCategoryUpdated int64 `json:"videoCategoryUpdated"` // 本次补齐的视频分类数
+ ActressUpdated int64 `json:"actressUpdated"` // 本次补齐的女优数
+ VideoCreaterUpdated int64 `json:"videoCreaterUpdated"` // 本次补齐的作者数
+ PlayletUpdated int64 `json:"playletUpdated"` // 本次补齐的短剧数
+ PlayletCategoryUpdated int64 `json:"playletCategoryUpdated"` // 本次补齐的短剧分类数
+ PlayletThemeUpdated int64 `json:"playletThemeUpdated"` // 本次补齐的短剧主题数
+ ContentTagUpdated int64 `json:"contentTagUpdated"` // 本次补齐的统一标签数
+ TotalUpdated int64 `json:"totalUpdated"` // 本次合计补齐数
+ HasMore bool `json:"hasMore"` // 是否还有未处理的历史数据
+}
diff --git a/web/api/web_acc/acc.go b/web/api/web_acc/acc.go
new file mode 100644
index 0000000..444dbb9
--- /dev/null
+++ b/web/api/web_acc/acc.go
@@ -0,0 +1,356 @@
+package web_acc
+
+import (
+ "time"
+ "videoweb/pub/common"
+ "videoweb/pub/common/crypt"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/google"
+ "videoweb/pub/models/mwacc"
+ "videoweb/web/service/srv_acc"
+ "videoweb/web/service/srv_mwacc"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// @Tags 账号
+// @Summary 账号-列表
+// @Description 账号-列表
+// @Accept json
+// @Produce json
+// @Param param body MwAccListReq true "参数列表"
+// @Success 200 {object} MwAccListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/list [GET]
+func AccGetList(c *gin.Context) {
+
+ var req MwAccListReq
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ search := mwacc.MakeSearch(req.KeyWords, req.Status, req.Page)
+ sort := mwacc.MakeSort(req.PageNum, req.PageSize)
+
+ total, data, _ := mwacc.GetAccList(c, search, sort)
+
+ resp := MwAccListResp{
+ TotalCount: total,
+ AccRec: make([]*MwAccRec, 0),
+ }
+
+ for _, v := range data {
+ resp.AccRec = append(resp.AccRec, &MwAccRec{
+ Id: v.Id,
+ UserName: v.Username,
+ Acc: v.Acc,
+ Pwd: v.Pwd,
+ Email: v.Email,
+ RoleId: v.RoleId,
+ RoleName: v.RoleName,
+ // Base32: v.Base32,
+ IsBase32: v.IsBase32,
+ IsEnb: v.IsEnb,
+ FeedBack: v.Feedback,
+ CreatedAt: v.CreatedAt.Unix(),
+ UpdatedAt: v.UpdatedAt.Unix(),
+ LimitIP: v.LimitIP,
+ })
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// @Tags 账号
+// @Summary 账号-添加
+// @Description 账号-添加
+// @Accept json
+// @Produce json
+// @Param param body MwAccAddReq true "参数列表"
+// @Success 200 {object} MwAccAddResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/add [POST]
+func AccAdd(c *gin.Context) {
+ var req MwAccAddReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ accCount, err := mwacc.AccCountByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Acc: req.Acc,
+ })
+ if accCount > 0 || err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "用户已存在")
+ return
+ }
+
+ secret := google.NewGoogleAuth().GetSecret()
+
+ insertDoc := &mwacc.AccInsertDoc{
+ UserName: req.Name,
+ Pwd: crypt.StrToMd5(req.Pwd), //md5V(req.Pwd),
+ Acc: req.Acc,
+ Email: req.Email,
+ RoleId: req.RoleId,
+ Base32: secret,
+ IsBase32: false,
+ IsEnb: req.IsEnb,
+ LimitIP: req.LimitIP,
+ }
+
+ _, _, err = mwacc.AccAdd(c, insertDoc)
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, MwAccAddResp{Result: false})
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, MwAccAddResp{Result: true})
+ return
+}
+
+// @Tags 账号
+// @Summary 账号-删除
+// @Description 账号-删除
+// @Accept json
+// @Produce json
+// @Param param body MwAccDelReq true "参数列表"
+// @Success 200 {object} MwAccDelResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/del [POST]
+func AccDel(c *gin.Context) {
+
+ var req MwAccDelReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ err := mwacc.DelAccByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Id: req.Id,
+ })
+ // 删除对应账号的token
+ _ = mwacc.DeleteOneToken(c, bson.M{
+ mwacc.FN.MwAccToken.AccId: req.Id,
+ })
+
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, MwAccDelResp{Result: true})
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, MwAccDelResp{Result: true})
+ return
+}
+
+// @Tags 账号
+// @Summary 账号-获取信息
+// @Description 账号-获取信息
+// @Accept json
+// @Produce json
+// @Param param body MwAccGetInfoReq true "参数列表"
+// @Success 200 {object} MwAccGetInfoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/getinfo [GET]
+func AccGetInfo(c *gin.Context) {
+ var req MwAccGetInfoReq
+
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ rest, err := mwacc.FindOneAccByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Id: req.Id,
+ })
+ if err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ resq := MwAccGetInfoResp{
+ Id: rest.Id,
+ UserName: rest.UserName,
+ Acc: rest.Acc,
+ Pwd: rest.Pwd,
+ Email: rest.Email,
+ RoleId: rest.RoleId,
+ IsEnb: rest.IsEnb,
+ LimitIP: rest.LimitIP,
+ }
+ common.ServerJSON(c, srvdef.Success, resq)
+ return
+}
+
+// @Tags 账号
+// @Summary 账号-更新
+// @Description 账号-更新
+// @Accept json
+// @Produce json
+// @Param param body MwAccUpReq true "参数列表"
+// @Success 200 {object} MwAccUpResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/up [POST]
+func AccUp(c *gin.Context) {
+ var req MwAccUpReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ _, err := srv_mwacc.AccUp(c, &srv_mwacc.AccUpDoc{
+ Id: req.Id,
+ UserName: req.UserName,
+ Acc: req.Acc,
+ Email: req.Email,
+ RoleId: req.RoleId,
+ IsEnb: req.IsEnb,
+ LimitIP: req.LimitIP,
+ })
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, MwAccUpResp{Result: false})
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, MwAccUpResp{Result: true})
+ return
+}
+
+// @Tags 账号
+// @Summary 账号-获取状态
+// @Description 账号-获取状态
+// @Accept json
+// @Produce json
+// @Param param body MwAccGetStatusReq true "参数列表"
+// @Success 200 {object} MwAccGetStatusResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/getstatus [GET]
+func AccGetStatus(c *gin.Context) {
+ var req MwAccGetStatusReq
+
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ rest, err := mwacc.FindOneAccByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Id: req.Id,
+ })
+
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, err)
+ return
+ }
+
+ resp := MwAccGetStatusResp{
+ Id: rest.Id,
+ UserName: rest.UserName,
+ IsEnb: rest.IsEnb,
+ FeedBack: rest.Feedback,
+ }
+
+ common.ServerJSON(c, srvdef.Success, resp)
+ return
+}
+
+// @Tags 账号
+// @Summary 账号-保存状态
+// @Description 账号-保存状态
+// @Accept json
+// @Produce json
+// @Param param body MwAccSaveStatusReq true "参数列表"
+// @Success 200 {object} MwAccSaveStatusResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/savestatus [POST]
+func AccSaveStatus(c *gin.Context) {
+
+ var req MwAccSaveStatusReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ err := mwacc.UpOneAccByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Id: req.Id,
+ }, bson.M{
+ mwacc.FN.MwAcc.UserName: req.UserName,
+ mwacc.FN.MwAcc.IsEnb: req.IsEnb,
+ mwacc.FN.MwAcc.Feedback: req.FeedBack,
+ mwacc.FN.MwAcc.UpdatedAt: time.Now(),
+ })
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, "")
+ return
+ }
+ // 封禁时,删除token
+ if !req.IsEnb {
+ mwacc.DeleteOneToken(c, bson.M{mwacc.FN.MwAccToken.AccId: req.Id})
+ }
+
+ common.ServerJSON(c, srvdef.Success, MwAccSaveStatusResp{Result: true})
+ return
+}
+
+// @Tags 账号
+// @Summary 账号-获取操作记录
+// @Description 账号-获取操作记录
+// @Accept json
+// @Produce json
+// @Param param body AccOpLogReq true "参数列表"
+// @Success 200 {object} AccOpLogResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/ops [POST]
+func GetAccOpRecords(ctx *gin.Context) {
+ var req AccOpLogReq
+
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ total, data, err := srv_acc.GetOpLogs(ctx, &req.OpLogSearch)
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.Failure, err.Error())
+ return
+ }
+ resp := &AccOpLogResp{total, data}
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 账号
+// @Summary 账号-更新密码
+// @Description 账号-更新密码
+// @Accept json
+// @Produce json
+// @Param param body MwAccUpPwdReq true "参数列表"
+// @Success 200 {object} MwAccUpPwdReq "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/acc/uppwd [POST]
+func AccUpdatePwd(c *gin.Context) {
+ var req MwAccUpPwdReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ _, err := srv_mwacc.AccUpPwd(c, &srv_mwacc.AccUpPwdDoc{
+ Id: req.Id,
+ Pwd: req.Pwd,
+ })
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, MwAccUpPwdResp{Result: false})
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, MwAccUpPwdResp{Result: true})
+ return
+}
diff --git a/web/api/web_acc/login.go b/web/api/web_acc/login.go
new file mode 100644
index 0000000..6918de7
--- /dev/null
+++ b/web/api/web_acc/login.go
@@ -0,0 +1,114 @@
+package web_acc
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/mwacc"
+ "videoweb/web/service/srv_mwacc"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 登录
+// @Summary 登录-登录
+// @Description 登录-登录
+// @Accept json
+// @Produce json
+// @Param param body MainWebLoginReq true "参数列表"
+// @Success 200 {object} MainWebLoginResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/web/login/acc [POST]
+func AccLogin(c *gin.Context) {
+ var req MainWebLoginReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ accInfo, err := srv_mwacc.AccLogin(c, req.Acc, req.Pwd, req.Code)
+ if err != nil || accInfo == nil {
+ common.ServerJSON(c, err.Code, err.Msg)
+ return
+ }
+
+ resp := MainWebLoginResp{}
+
+ if len(accInfo.Qurl) != 0 {
+ resp.Qurl = accInfo.Qurl
+ } else {
+ // 成功保存token
+ if ok, _ := mwacc.SaveToken(c, accInfo.Id, accInfo.Token); ok != true {
+ common.ServerJSON(c, srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg())
+ }
+
+ resp.AccId = accInfo.Id
+ resp.Acc = accInfo.Acc
+ resp.RoleId = accInfo.RoleId
+ resp.Token = accInfo.Token
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+ return
+}
+
+// SLogin doc
+// @Summary 管理员sso登陆
+// @Description 管理员sso登陆
+// @Tags web-单点登陆
+// @Accept mpfd,json
+// @Produce json,html
+// @Param name formData string true "账号"
+// @Param password formData string true "密码"
+// @Success 200 {string} json "{"msg": "管理员信息""}"
+// @Failure 400 {string} json "{"msg": "操作失败"}"
+// @Router /api/web/login/slogin [post]
+func SLogin(c *gin.Context) {
+ var req MainWebLoginReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ accInfo, err := srv_mwacc.AccSLogin(c, req.Acc, req.Pwd)
+ if err != nil || accInfo == nil {
+ common.ServerJSON(c, err.Code, err.Msg)
+ return
+ }
+ resp := MainWebLoginResp{}
+ // 成功保存token
+ if ok, _ := mwacc.SaveToken(c, accInfo.Id, accInfo.Token); ok != true {
+ common.ServerJSON(c, srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg())
+ }
+ resp.AccId = accInfo.Id
+ resp.Acc = accInfo.Acc
+ resp.RoleId = accInfo.RoleId
+ resp.Token = accInfo.Token
+
+ common.ServerJSON(c, srvdef.Success, resp)
+ return
+}
+
+// @Tags 登录
+// @Summary 登录-绑定二维码
+// @Description 登录-绑定二维码
+// @Accept json
+// @Produce json
+// @Param param body BindQurlReq true "参数列表"
+// @Success 200 {object} BindQurlResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/web/login/bind [POST]
+func BindQurl(c *gin.Context) {
+ var req BindQurlReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, "tone AccLogin arg error "+err.Error())
+ return
+ }
+
+ ok, _ := mwacc.BindAccQurl(c, req.Acc)
+ if !ok {
+ common.ServerJSON(c, srvdef.Failure, BindQurlResp{Result: false})
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, BindQurlResp{Result: true})
+ return
+}
diff --git a/web/api/web_acc/protodef.go b/web/api/web_acc/protodef.go
new file mode 100644
index 0000000..1eff43b
--- /dev/null
+++ b/web/api/web_acc/protodef.go
@@ -0,0 +1,479 @@
+package web_acc
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+ "videoweb/web/service/srv_acc"
+)
+
+/////////////////////////登录//////////////////////////
+/******************主后台登录***********************/
+// POST /web/login/acc
+
+// 请求参数
+type MainWebLoginReq struct {
+ // 账户
+ Acc string `json:"acc,omitempty"`
+ // 密码
+ Pwd string `json:"pwd,omitempty"`
+ // 验证码
+ Code string `json:"code,omitempty"`
+}
+
+// 返回参数
+type MainWebLoginResp struct {
+ // 账户id
+ AccId int64 `json:"accId"`
+ // 账户
+ Acc string `json:"acc"`
+ // 角色id
+ RoleId int64 `json:"roleId"`
+ // token
+ Token string `json:"token"`
+ // 二维码
+ Qurl []byte `json:"qurl"`
+}
+
+/******************绑定二维码***********************/
+// POST /web/login/bind
+
+// 请求参数
+type BindQurlReq struct {
+ //账户
+ Acc string `json:"acc,omitempty"`
+}
+
+// 返回参数
+type BindQurlResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************绑定二维码***********************/
+
+/////////////////////////角色//////////////////////////
+// GET /web/role/list
+// 请求参数
+
+// 角色记录
+type MwRoleRec struct {
+ // 角色Id
+ Id int64 `json:"id"`
+ // 角色名称
+ Name string `json:"name"`
+ // 描述
+ Desc string `json:"desc"`
+ // 是否启用 false:禁用 true:启用
+ IsEnb bool `json:"isEnb"`
+ // 已有权限的api
+ ApiMenus []string `json:"apiMenus"`
+ // 创建时间
+ CreatedAt time.Time `json:"createdAt"`
+ // 更新时间
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// 返回参数
+type MwRoleListResp struct {
+ // 总共数量
+ TotalCount int64 `json:"totalCount"`
+ // 当前页配置记录
+ RoleRecs []*MwRoleRec `json:"roleRecs"`
+}
+
+/******************角色添加***********************/
+// POST /web/role/add
+
+// 请求参数
+type MwRoleAddReq struct {
+ // 名称
+ Name string `json:"name"`
+ // 描述
+ Desc string `json:"desc"`
+ // 状态
+ IsEnb bool `json:"isEnb"`
+}
+
+// 返回参数
+type MwRoleAddResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************角色删除***********************/
+// POST /web/role/del
+
+// 请求参数
+type MwRoleDelReq struct {
+ // 角色id
+ Id int64 `json:"id,omitempty"`
+}
+
+// 返回参数
+type MwRoleDelResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************角色获取信息***********************/
+// GET /web/role/getinfo
+
+// 请求参数
+type MwRoleGetInfoReq struct {
+ // 角色id
+ Id int64 `form:"id" json:"id,omitempty"`
+}
+
+// 返回参数
+type MwRoleGetInfoResp struct {
+ RoleRec *MwRoleRec `json:"roleRec"`
+}
+
+/******************角色更新***********************/
+// POST /web/role/up
+
+// 请求参数
+type MwRoleUpReq struct {
+ // 角色id
+ Id int64 `json:"id"`
+ // 名称
+ Name string `json:"name"`
+ // 描述
+ Desc string `json:"desc"`
+ // 状态
+ IsEnb bool `json:"isEnb"`
+}
+
+// 返回参数
+type MwRoleUpResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************角色使能***********************/
+// POST /web/role/enable
+
+// 请求参数
+type MwRoleEnableReq struct {
+ // 角色id
+ Id int64 `json:"id"`
+ // 状态
+ IsEnb bool `json:"isEnb"`
+}
+
+// 返回参数
+type MwRoleEnableResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************角色下拉***********************/
+// GET /web/role/drop
+
+// 请求参数
+
+type MwRoleDrop struct {
+ // 角色Id
+ Id int64 `json:"id,omitempty"`
+ // 角色名称
+ Name string `json:"name,omitempty"`
+}
+
+// 返回参数
+type MwRoleDropResp struct {
+ // 是否成功 true:成功 false:未成功
+ RoleDrop []*MwRoleDrop `json:"roleDrop"`
+}
+
+/******************获取权限***********************/
+// GET /web/role/getauth
+
+type Router_Meta struct {
+ //
+ Title string `json:"title"`
+ //
+ Icon string `json:"icon"`
+ //
+ NoCache bool `json:"noCache"`
+}
+
+type Router_ApiUrl struct {
+ //
+ Url []string `json:"url,omitempty"`
+ //
+ Desc string `json:"desc,omitempty"`
+ //
+ Status bool `json:"status,omitempty"`
+ //
+ Method []string `json:"method,omitempty"`
+}
+
+type Router struct {
+ //
+ Path string `json:"path"`
+ //
+ Component string `json:"component"`
+ //
+ Redirect string `json:"redirect"`
+ //
+ Name string `json:"name"`
+ //
+ IsShow bool `json:"isShow"`
+ //
+ Hidden bool `json:"hidden"`
+ //
+ Meta *Router_Meta `json:"meta"`
+ //
+ Children []*Router `json:"children"`
+ ApiUrl []*Router_ApiUrl `json:"apiUrl"`
+}
+
+// 请求参数
+type MwRoleGetAuthReq struct {
+ // 角色id
+ Id int64 `form:"id" json:"id,omitempty"`
+}
+
+// 返回参数
+type MwRoleGetAuthResp struct {
+ // 角色id
+ Id int64 `json:"id,omitempty"`
+ //
+ Router []*Router `json:"router,omitempty"`
+}
+
+/******************更新权限***********************/
+// POST /web/role/upauth
+
+// 请求参数
+type MwRoleUpAuthReq struct {
+ // 角色id
+ Id int64 `json:"id,omitempty"`
+ Router []*Router `json:"router,omitempty"`
+}
+
+// 返回参数
+type MwRoleUpAuthResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/////////////////////////账号//////////////////////////
+
+type MwAccRec struct {
+ // 账号Id
+ Id int64 `json:"id"`
+ // 用户名
+ UserName string `json:"userName"`
+ // 账号
+ Acc string `json:"acc"`
+ // 密码
+ Pwd string `json:"pwd"`
+ // 邮箱
+ Email string `json:"email"`
+ // 角色Id
+ RoleId int64 `json:"roleId"`
+ // 角色名
+ RoleName string `json:"roleName"`
+ // // 二步签名
+ // Base32 string `json:"base32"`
+ // 是否已签名
+ IsBase32 bool `json:"isBase32"`
+ // 使能
+ IsEnb bool `json:"isEnb"`
+ //禁用原因
+ FeedBack string `json:"feedBack"`
+ // 创建时间
+ CreatedAt int64 `json:"createdAt"`
+ // 更新时间
+ UpdatedAt int64 `json:"updatedAt"`
+ // 允许访问的IP
+ LimitIP []string `json:"limitIP"`
+}
+
+/******************账号列表***********************/
+// GET /web/acc/list
+
+// 请求参数
+type MwAccListReq struct {
+ // 搜索条件
+ KeyWords string `json:"keyWords" form:"keyWords"`
+ //状态 1正常 2 禁用
+ Status int64 `json:"status" form:"status"`
+ // 页码
+ pubdef.Page
+}
+
+// 返回参数
+type MwAccListResp struct {
+ // 总共数量
+ TotalCount int64 `json:"totalCount"`
+ // 当前页配置记录
+ AccRec []*MwAccRec `json:"accRec"`
+}
+
+/******************账号添加***********************/
+// POST /web/acc/add
+
+// 请求参数
+type MwAccAddReq struct {
+ // 名称
+ Name string `json:"name"`
+ // 密码
+ Pwd string `json:"pwd"`
+ // 账户
+ Acc string `json:"acc"`
+ // 邮箱
+ Email string `json:"email"`
+ // 角色ID
+ RoleId int64 `json:"roleId"`
+ // 使能
+ IsEnb bool `json:"isEnb"`
+ // 允许访问的IP
+ LimitIP []string `json:"limitIP"`
+}
+
+type MwAccAddResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************账号删除***********************/
+// POST web/mw/acc/del
+
+// 请求参数
+type MwAccDelReq struct {
+ // 账号Id
+ Id int64 `json:"id,omitempty"`
+}
+
+// 返回参数
+type MwAccDelResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************账号获取信息***********************/
+// GET /web/acc/getinfo
+// 请求参数
+type MwAccGetInfoReq struct {
+ // 角色id
+ Id int64 `form:"id" json:"id,omitempty"`
+}
+
+// 返回参数
+type MwAccGetInfoResp struct {
+ // 账号Id
+ Id int64 `json:"id"`
+ // 用户名
+ UserName string `json:"userName"`
+ // 账号
+ Acc string `json:"acc"`
+ // 密码
+ Pwd string `json:"pwd"`
+ // 邮箱
+ Email string `json:"email"`
+ // 角色Id
+ RoleId int64 `json:"roleId"`
+ // 使能
+ IsEnb bool `json:"isEnb"`
+ // 允许访问的IP
+ LimitIP []string `json:"limitIP"`
+}
+
+/******************角色更新***********************/
+// POST web/mw/acc/up
+
+// 请求参数
+type MwAccUpReq struct {
+ // 账号Id
+ Id int64 `json:"id"`
+ // 用户名
+ UserName string `json:"userName"`
+ // 账号
+ Acc string `json:"acc"`
+ // 密码
+ Pwd string `json:"pwd"`
+ // 邮箱
+ Email string `json:"email"`
+ // 角色Id
+ RoleId int64 `json:"roleId"`
+ // 使能
+ IsEnb bool `json:"isEnb"`
+ // 允许访问的IP
+ LimitIP []string `json:"limitIP"`
+}
+
+// 返回参数
+type MwAccUpResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************账号获取状态***********************/
+// GET web/mw/acc/getstatus
+
+// 请求参数
+type MwAccGetStatusReq struct {
+ // 角色id
+ Id int64 `form:"id" json:"id,omitempty"`
+}
+
+// 返回参数
+type MwAccGetStatusResp struct {
+
+ // 账号Id
+ Id int64 `json:"id"`
+ // 用户名
+ UserName string `json:"userName"`
+ // 使能
+ IsEnb bool `json:"isEnb"`
+ //禁用原因
+ FeedBack string `json:"feedBack"`
+}
+
+/******************账号保存状态***********************/
+// POST web/mw/acc/savestatus
+// 请求参数
+type MwAccSaveStatusReq struct {
+ // 账号Id
+ Id int64 `json:"id"`
+ // 用户名
+ UserName string `json:"userName"`
+ // 使能
+ IsEnb bool `json:"isEnb"`
+ //禁用原因
+ FeedBack string `json:"feedBack"`
+}
+
+// 返回参数
+type MwAccSaveStatusResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
+
+/******************获取账号操作列表***********************/
+// POST web/acc/ops
+// 请求参数
+type AccOpLogReq struct {
+ srv_acc.OpLogSearch
+}
+
+// AccOpLogResp 返回参数
+type AccOpLogResp struct {
+ Total int64 `json:"total" bson:"total"`
+ List []*srv_acc.OpLogInfo `json:"list" bson:"list"`
+}
+
+// 修改账号密码
+type MwAccUpPwdReq struct {
+ // 账号Id
+ Id int64 `json:"id" binding:"required"`
+ // 密码
+ Pwd string `json:"pwd"`
+}
+
+type MwAccUpPwdResp struct {
+ // 是否成功 true:成功 false:未成功
+ Result bool `json:"result,omitempty"`
+}
diff --git a/web/api/web_acc/role.go b/web/api/web_acc/role.go
new file mode 100644
index 0000000..db21f58
--- /dev/null
+++ b/web/api/web_acc/role.go
@@ -0,0 +1,400 @@
+package web_acc
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/pageopt"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/pubdef"
+ "videoweb/web/service/srv_mwacc"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// @Tags 角色
+// @Summary 角色-列表
+// @Description 角色-列表
+// @Accept json
+// @Produce json
+// @Param param body pubdef.Page true "参数列表"
+// @Success 200 {object} MwRoleListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/list [GET]
+func RoleGetList(c *gin.Context) {
+ var req pubdef.Page
+
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ if req.PageNum <= 0 {
+ req.PageNum = 1
+ }
+ if req.PageSize <= 0 {
+ req.PageSize = 10
+ }
+
+ skip := (req.PageNum - 1) * req.PageSize
+ limit := req.PageSize
+
+ // 获取分页信息
+ var mats []pageopt.Matcher
+ dbRoleRec, err := mwacc.GetRoleList(c, skip, limit, mats...)
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, "")
+ return
+ }
+
+ // 获取总条数
+ roleCount, err := mwacc.RoleCountByFilter(c, bson.M{})
+
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, "")
+ return
+ }
+
+ webRoleRec := make([]*MwRoleRec, 0)
+ for _, v := range dbRoleRec {
+ webRoleRec = append(webRoleRec, &MwRoleRec{
+ Id: v.Id,
+ Name: v.Name,
+ Desc: v.Desc,
+ IsEnb: v.IsEnb,
+ ApiMenus: v.ApiMenus[:],
+ CreatedAt: v.CreatedAt,
+ UpdatedAt: v.UpdatedAt,
+ })
+ }
+
+ // 返回正确信息
+ common.ServerJSON(c, srvdef.Success, MwRoleListResp{TotalCount: roleCount, RoleRecs: webRoleRec})
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-添加
+// @Description 角色-添加
+// @Accept json
+// @Produce json
+// @Param param body MwRoleAddReq true "参数列表"
+// @Success 200 {object} MwRoleAddResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/add [POST]
+func RoleAdd(c *gin.Context) {
+ var req MwRoleAddReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ ok, _ := mwacc.RoleAdd(c, req.Name, req.Desc, req.IsEnb)
+ if !ok {
+ common.ServerJSON(c, srvdef.Failure, "")
+ return
+ }
+ // 返回正确信息
+ common.ServerJSON(c, srvdef.Success, MwRoleAddResp{Result: true})
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-删除
+// @Description 角色-删除
+// @Accept json
+// @Produce json
+// @Param param body MwRoleDelReq true "参数列表"
+// @Success 200 {object} MwRoleDelResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/del [POST]
+func RoleDel(c *gin.Context) {
+ var req MwRoleDelReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ ok, err := srv_mwacc.RoleDel(c, req.Id)
+ if !ok {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ common.ServerJSON(c, srvdef.Success, MwRoleDelResp{Result: true})
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-获取信息
+// @Description 角色-获取信息
+// @Accept json
+// @Produce json
+// @Param param body MwRoleGetInfoReq true "参数列表"
+// @Success 200 {object} MwRoleGetInfoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/getinfo [GET]
+func RoleGetInfo(c *gin.Context) {
+
+ var req MwRoleGetInfoReq
+
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ roleInfo, err := srv_mwacc.RoleGetInfo(c, req.Id)
+
+ if err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ resq := &MwRoleGetInfoResp{
+ RoleRec: &MwRoleRec{
+ Id: roleInfo.Id,
+ Name: roleInfo.Name,
+ Desc: roleInfo.Desc,
+ IsEnb: roleInfo.IsEnb,
+ ApiMenus: roleInfo.ApiMenus[:],
+ CreatedAt: roleInfo.CreatedAt,
+ UpdatedAt: roleInfo.UpdatedAt,
+ },
+ }
+ common.ServerJSON(c, srvdef.Success, resq)
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-更新
+// @Description 角色-更新
+// @Accept json
+// @Produce json
+// @Param param body MwRoleUpReq true "参数列表"
+// @Success 200 {object} MwRoleUpResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/up [POST]
+func RoleUp(c *gin.Context) {
+
+ var req MwRoleUpReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ // RoleUp
+ _, err := srv_mwacc.RoleUp(c, req.Id, req.Name, req.Desc, req.IsEnb)
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, MwRoleUpResp{Result: false})
+ return
+ }
+
+ common.ServerJSON(c, srvdef.Success, MwRoleUpResp{Result: true})
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-使能
+// @Description 角色-使能
+// @Accept json
+// @Produce json
+// @Param param body MwRoleEnableReq true "参数列表"
+// @Success 200 {object} MwRoleEnableResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/enable [POST]
+func RoleEnable(c *gin.Context) {
+
+ var req MwRoleEnableReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ _, err := srv_mwacc.RoleEnable(c, req.Id, req.IsEnb)
+ if err != nil {
+ common.ServerJSON(c, err.Code, err.Msg)
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, MwRoleEnableResp{Result: true})
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-下拉列表
+// @Description 角色-下拉列表
+// @Accept json
+// @Produce json
+// @Success 200 {object} MwRoleDropResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/drop [POST]
+func RoleGetDrop(c *gin.Context) {
+ data, _ := mwacc.RoleDrop(c)
+
+ roleDropArr := make([]*MwRoleDrop, 0)
+ for _, v := range data {
+ roleDropArr = append(roleDropArr, &MwRoleDrop{Id: v.Id, Name: v.Name})
+ }
+ common.ServerJSON(c, srvdef.Success, MwRoleDropResp{RoleDrop: roleDropArr})
+ return
+}
+
+func DbRouterToWebRouter(dbRouter *mwacc.Router) (webRouter *Router) {
+ if dbRouter == nil {
+ return
+ }
+
+ webRouter = &Router{
+ Path: dbRouter.Path,
+ Component: dbRouter.Component,
+ Redirect: dbRouter.Redirect,
+ Name: dbRouter.Name,
+ IsShow: dbRouter.IsShow,
+ Hidden: dbRouter.Hidden,
+ Meta: &Router_Meta{
+ Title: dbRouter.Meta.Title,
+ Icon: dbRouter.Meta.Icon,
+ NoCache: dbRouter.Meta.NoCache,
+ },
+ Children: make([]*Router, 0),
+ ApiUrl: make([]*Router_ApiUrl, 0),
+ }
+ for _, v := range dbRouter.Children {
+ webRouter.Children = append(webRouter.Children, DbRouterToWebRouter(&v))
+ }
+ for _, v := range dbRouter.ApiUrl {
+ webRouter.ApiUrl = append(webRouter.ApiUrl, &Router_ApiUrl{
+ Url: v.Url[:],
+ Desc: v.Desc,
+ Status: v.Status,
+ Method: v.Method[:],
+ })
+ }
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-获取权限
+// @Description 角色-获取权限
+// @Accept json
+// @Produce json
+// @Param param body MwRoleGetAuthReq true "参数列表"
+// @Success 200 {object} MwRoleGetAuthResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/getauth [GET]
+func RoleGetAuth(c *gin.Context) {
+ var req MwRoleGetAuthReq
+
+ if err := c.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ roleInfo, err := srv_mwacc.RoleGetInfo(c, req.Id)
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, err.Error())
+ return
+ }
+
+ if roleInfo == nil {
+ common.ServerJSON(c, srvdef.Failure, "没找到角色")
+ return
+ }
+
+ if roleInfo.Id == 0 {
+ common.ServerJSON(c, srvdef.Failure, "没找到角色2")
+ return
+ }
+
+ resq := &MwRoleGetAuthResp{
+ Id: req.Id,
+ Router: make([]*Router, 0),
+ }
+
+ for _, v := range roleInfo.Router {
+
+ resq.Router = append(resq.Router, DbRouterToWebRouter(&v))
+ }
+
+ common.ServerJSON(c, srvdef.Success, resq)
+ return
+}
+
+// 网站Router转数据库Router
+func WebRouterToDbRouter(webRouter *Router) (dbRouter *mwacc.Router) {
+
+ if webRouter == nil {
+ return
+ }
+
+ dbRouter = &mwacc.Router{
+ Path: webRouter.Path,
+ Component: webRouter.Component,
+ Redirect: webRouter.Redirect,
+ Name: webRouter.Name,
+ IsShow: webRouter.IsShow,
+ Hidden: webRouter.Hidden,
+ Meta: mwacc.Meta{
+ Title: webRouter.Meta.Title,
+ Icon: webRouter.Meta.Icon,
+ NoCache: webRouter.Meta.NoCache,
+ },
+ Children: make([]mwacc.Router, 0),
+ ApiUrl: make([]mwacc.Apiurl, 0),
+ }
+ for _, v := range webRouter.Children {
+ dbRouter.Children = append(dbRouter.Children, *WebRouterToDbRouter(v))
+ }
+ for _, v := range webRouter.ApiUrl {
+ dbRouter.ApiUrl = append(dbRouter.ApiUrl, mwacc.Apiurl{
+ Url: v.Url[:],
+ Desc: v.Desc,
+ Status: v.Status,
+ Method: v.Method[:],
+ })
+ }
+ return
+}
+
+// @Tags 角色
+// @Summary 角色-更新权限
+// @Description 角色-更新权限
+// @Accept json
+// @Produce json
+// @Param param body MwRoleUpAuthReq true "参数列表"
+// @Success 200 {object} MwRoleUpAuthResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/role/upauth [GET]
+func RoleUpAuth(c *gin.Context) {
+
+ var req MwRoleUpAuthReq
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ dataSave := make([]*mwacc.Router, 0)
+ for _, v := range req.Router {
+ dataSave = append(dataSave, WebRouterToDbRouter(v))
+ }
+
+ _, err := srv_mwacc.UpAuth(c, req.Id, dataSave)
+ if err != nil {
+ common.ServerJSON(c, srvdef.Failure, MwRoleUpAuthResp{Result: false})
+ return
+ }
+
+ // auth.ClearRoleMap()
+ common.ServerJSON(c, srvdef.Success, MwRoleUpAuthResp{Result: true})
+ return
+}
diff --git a/web/api/web_actress/actress.go b/web/api/web_actress/actress.go
new file mode 100644
index 0000000..236262a
--- /dev/null
+++ b/web/api/web_actress/actress.go
@@ -0,0 +1,109 @@
+package web_actress
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_actress"
+)
+
+// @Tags 女优管理
+// @Summary 分组分页获取女优信息
+// @Description 分组分页获取女优信息
+// @Accept json
+// @Produce json
+// @Param param body srv_actress.ActressListReq true "参数列表"
+// @Success 200 {object} srv_actress.ActressListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/actress/list [GET]
+func Get(ctx *gin.Context) {
+ var req = &srv_actress.ActressListReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_actress.ActressList(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 女优管理
+// @Summary 添加一个女优信息
+// @Description 添加一个女优信息
+// @Accept json
+// @Produce json
+// @Param param body srv_actress.ActressAddReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/actress/add [POST]
+func Add(ctx *gin.Context) {
+ var req = &srv_actress.ActressAddReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_actress.AddActress(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 女优管理
+// @Summary 删除一个女优信息
+// @Description 删除一个女优信息
+// @Accept json
+// @Produce json
+// @Param param body srv_actress.ActressDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/actress/del [POST]
+func Del(ctx *gin.Context) {
+ var req = &srv_actress.ActressDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_actress.DeleteActress(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 女优管理
+// @Summary 更新一个女优信息
+// @Description 更新一个女优信息
+// @Accept json
+// @Produce json
+// @Param param body srv_actress.ActressUpdateReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/actress/update [POST]
+func Update(ctx *gin.Context) {
+ var req = &srv_actress.ActressUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_actress.UpdateActress(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
\ No newline at end of file
diff --git a/web/api/web_analytics/analytics.go b/web/api/web_analytics/analytics.go
new file mode 100644
index 0000000..eb2b733
--- /dev/null
+++ b/web/api/web_analytics/analytics.go
@@ -0,0 +1,155 @@
+package webanalytics
+
+import (
+ "time"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/analyticsdailystat"
+ "videoweb/pub/models/analyticsstat"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// @Tags 获取统计数据
+// @Summary 统计数据
+// @Description 统计数据
+// @Accept json
+// @Produce json
+// @Success 200 {object} srv_translationcategory.TranslationCategoryResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/stat/analytics [POST]
+func GetAnalyticsStat(ctx *gin.Context) {
+ // 当日数据
+ resp := AnalyticsStat{
+ TodayList: make([]LastWebStatInfo, 0),
+ WeekList: make([]LastWebStatInfo, 0),
+ YestodayList: make([]LastWebStatInfo, 0),
+ }
+ now := time.Now()
+ curStatTime := utils.RecentMinute(now, 30) // 按20分钟对齐
+ YestodayTime := curStatTime.AddDate(0, 0, -1)
+ WeekTime := curStatTime.AddDate(0, 0, -7)
+
+ dailyFilter := bson.M{
+ analyticsdailystat.FN.AnalyticsDailyInfo.SumDate: bson.M{"$gt": now.AddDate(0, -1, -1)},
+ }
+ dailyStatList, _ := analyticsdailystat.FindManyAnalyticsDailyInfo(ctx, dailyFilter)
+
+ curFilter := bson.M{
+ analyticsstat.FN.AnalyticsInfo.SumDate: curStatTime,
+ }
+ statList, _ := analyticsstat.FindManyAnalyticsInfo(ctx, curFilter)
+ for _, statInfo := range statList {
+ appStatInfo := LastWebStatInfo{
+ SumDate: statInfo.SumDate,
+ AppId: statInfo.AppId,
+ AppName: statInfo.AppName,
+ ActiveUsers: statInfo.ActiveUsers,
+ YestodayPV: statInfo.YestodayPV,
+ YestodayUV: statInfo.YestodayUV,
+ YestodayActiveUsers: statInfo.YestodayActiveUsers,
+ YestodaySessions: statInfo.YestodaySessions,
+ YestodayNewUsers: statInfo.YestodayNewUsers,
+ }
+ for _, dailyStatInfo := range dailyStatList {
+ if dailyStatInfo.SumDate.Equal(statInfo.DaySumDate) && dailyStatInfo.AppId == statInfo.AppId {
+ appStatInfo.LeadNewCount = dailyStatInfo.LeadNewCount
+ appStatInfo.LeadAmount = dailyStatInfo.LeadAmount
+ }
+ }
+ resp.TodayList = append(resp.TodayList, appStatInfo)
+ }
+
+ yesFilter := bson.M{
+ analyticsstat.FN.AnalyticsInfo.SumDate: YestodayTime,
+ }
+ statList, _ = analyticsstat.FindManyAnalyticsInfo(ctx, yesFilter)
+ for _, statInfo := range statList {
+ appStatInfo := LastWebStatInfo{
+ SumDate: statInfo.SumDate,
+ AppId: statInfo.AppId,
+ AppName: statInfo.AppName,
+ ActiveUsers: statInfo.ActiveUsers,
+ YestodayPV: statInfo.YestodayPV,
+ YestodayUV: statInfo.YestodayUV,
+ YestodayActiveUsers: statInfo.YestodayActiveUsers,
+ YestodaySessions: statInfo.YestodaySessions,
+ YestodayNewUsers: statInfo.YestodayNewUsers,
+ }
+ for _, dailyStatInfo := range dailyStatList {
+ if dailyStatInfo.SumDate.Equal(statInfo.DaySumDate) && dailyStatInfo.AppId == statInfo.AppId {
+ appStatInfo.LeadNewCount = dailyStatInfo.LeadNewCount
+ appStatInfo.LeadAmount = dailyStatInfo.LeadAmount
+ }
+ }
+ resp.YestodayList = append(resp.YestodayList, appStatInfo)
+ }
+
+ weekFilter := bson.M{
+ analyticsstat.FN.AnalyticsInfo.SumDate: WeekTime,
+ }
+ statList, _ = analyticsstat.FindManyAnalyticsInfo(ctx, weekFilter)
+ for _, statInfo := range statList {
+ appStatInfo := LastWebStatInfo{
+ SumDate: statInfo.SumDate,
+ AppId: statInfo.AppId,
+ AppName: statInfo.AppName,
+ ActiveUsers: statInfo.ActiveUsers,
+ YestodayPV: statInfo.YestodayPV,
+ YestodayUV: statInfo.YestodayUV,
+ YestodayActiveUsers: statInfo.YestodayActiveUsers,
+ YestodaySessions: statInfo.YestodaySessions,
+ YestodayNewUsers: statInfo.YestodayNewUsers,
+ }
+ for _, dailyStatInfo := range dailyStatList {
+ if dailyStatInfo.SumDate.Equal(statInfo.DaySumDate) && dailyStatInfo.AppId == statInfo.AppId {
+ appStatInfo.LeadNewCount = dailyStatInfo.LeadNewCount
+ appStatInfo.LeadAmount = dailyStatInfo.LeadAmount
+ }
+ }
+ resp.WeekList = append(resp.WeekList, appStatInfo)
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 获取统计数据
+// @Summary 统计数据
+// @Description 统计数据
+// @Accept json
+// @Produce json
+// @Success 200 {object} srv_translationcategory.TranslationCategoryResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/stat/daily/analytics [POST]
+func GetAnalyticsDailyStat(ctx *gin.Context) {
+ // 当日数据
+ resp := AnalyticsDailyStat{
+ List: make([]LastWebStatInfo, 0),
+ }
+ now := time.Now()
+
+ curFilter := bson.M{
+ analyticsdailystat.FN.AnalyticsDailyInfo.SumDate: bson.M{"$gt": utils.BeginOfMonth(now.AddDate(0, 0, -1)), "$lte": now},
+ }
+ statList, _ := analyticsdailystat.FindManyAnalyticsDailyInfo(ctx, curFilter)
+ for _, statInfo := range statList {
+ appStatInfo := LastWebStatInfo{
+ SumDate: statInfo.SumDate,
+ AppId: statInfo.AppId,
+ AppName: statInfo.AppName,
+ YestodayPV: statInfo.YestodayPV,
+ YestodayUV: statInfo.YestodayUV,
+ YestodayActiveUsers: statInfo.YestodayActiveUsers,
+ YestodaySessions: statInfo.YestodaySessions,
+ YestodayNewUsers: statInfo.YestodayNewUsers,
+ LeadNewCount: statInfo.LeadNewCount,
+ LeadAmount: statInfo.LeadAmount,
+ }
+ resp.List = append(resp.List, appStatInfo)
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
\ No newline at end of file
diff --git a/web/api/web_analytics/proto.go b/web/api/web_analytics/proto.go
new file mode 100644
index 0000000..a87ff70
--- /dev/null
+++ b/web/api/web_analytics/proto.go
@@ -0,0 +1,26 @@
+package webanalytics
+
+import "time"
+
+type LastWebStatInfo struct {
+ SumDate time.Time `json:"sumDate" bson:"sumDate"` // 时间半小时
+ AppName string `json:"appName" bson:"appName"` // APP名称
+ AppId int64 `json:"appId" bson:"appId"` // appId
+ ActiveUsers int64 `json:"activeUsers" bson:"activeUsers"` // 最近半小时活跃数
+ YestodayPV int64 `json:"yestodayPV" bson:"yestodayPV"` // 页面浏览量 (PV)
+ YestodayUV int64 `json:"yestodayUV" bson:"yestodayUV"` // 独立用户数 (UV)
+ YestodayActiveUsers int64 `json:"yestodayActiveUsers" bson:"yestodayActiveUsers"` // 活跃用户数
+ YestodaySessions int64 `json:"yestodaySessions" bson:"yestodaySessions"` // 会话数
+ YestodayNewUsers int64 `json:"yestodayNewUsers" bson:"yestodayNewUsers"` // 新增数量
+ LeadNewCount float64 `json:"leadNewCount" bson:"leadNewCount"` // 日导量新增
+ LeadAmount float64 `json:"leadAmount" bson:"leadAmount"` // 日导量充值
+}
+
+type AnalyticsStat struct {
+ TodayList []LastWebStatInfo `json:"todayList"` //当日数据
+ WeekList []LastWebStatInfo `json:"weekList"` //环比数据
+ YestodayList []LastWebStatInfo `json:"yestodayList"` //同比数据
+}
+type AnalyticsDailyStat struct {
+ List []LastWebStatInfo `json:"list"` //当月数据
+}
\ No newline at end of file
diff --git a/web/api/web_announcement/announcement.go b/web/api/web_announcement/announcement.go
new file mode 100644
index 0000000..1b36739
--- /dev/null
+++ b/web/api/web_announcement/announcement.go
@@ -0,0 +1,109 @@
+package web_announcement
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_announcement"
+
+ "github.com/gin-gonic/gin"
+)
+
+// AnnouncementList
+// @Tags 公告
+// @Summary 公告-列表 --by 王鸣
+// @Description 公告-列表
+// @Accept json
+// @Produce json
+// @Param param body srv_announcement.GetAnnouncementListReq true "参数列表"
+// @Success 200 {object} srv_announcement.GetAnnouncementListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/announcement/list [POST]
+func AnnouncementList(c *gin.Context) {
+ var req srv_announcement.GetAnnouncementListReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, serr := srv_announcement.GetAnnouncementList(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// AnnouncementAdd
+// @Tags 公告
+// @Summary 公告-增加 --by 王鸣
+// @Description 公告-增加
+// @Accept json
+// @Produce json
+// @Param param body srv_announcement.AddAnnouncementReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/announcement/add [POST]
+func AnnouncementAdd(c *gin.Context) {
+ var req srv_announcement.AddAnnouncementReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_announcement.AddAnnouncement(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// AnnouncementDel
+// @Tags 公告
+// @Summary 公告-删除 --by 王鸣
+// @Description 公告-删除
+// @Accept json
+// @Produce json
+// @Param param body srv_announcement.DelAnnouncementReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/announcement/del [POST]
+func AnnouncementDel(c *gin.Context) {
+ var req srv_announcement.DelAnnouncementReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_announcement.DelAnnouncement(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// AnnouncementUpdate
+// @Tags 公告
+// @Summary 公告-更新 --by 王鸣
+// @Description 公告-更新
+// @Accept json
+// @Produce json
+// @Param param body srv_announcement.UpdateAnnouncementReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/announcement/update [POST]
+func AnnouncementUpdate(c *gin.Context) {
+ var req srv_announcement.UpdateAnnouncementReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_announcement.UpdateAnnouncement(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
diff --git a/web/api/web_app/app.go b/web/api/web_app/app.go
new file mode 100644
index 0000000..76271bd
--- /dev/null
+++ b/web/api/web_app/app.go
@@ -0,0 +1,78 @@
+package web_app
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/pub/common"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/pubdef"
+ "videoweb/web/service/srv_app"
+)
+
+func AppSyncReq(c *gin.Context) {
+ var in srv_app.AppVideoInfoSyncReq
+ if err := c.ShouldBindJSON(&in); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, nil)
+ return
+ }
+ if in.AppId != pubdef.AppId {
+ common.ServerJSON(c, srvdef.APPIDError, nil)
+ }
+ data, err := srv_app.AppVideoInfoSync(c, &in)
+ if err != nil {
+ log.Error("AppSyncReq AppVideoInfoSyncReq Failed", log.Any("err", err))
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+func AppCollectionSyncReq(c *gin.Context) {
+ var in srv_app.AppVideoCollectionSyncReq
+ if err := c.ShouldBindJSON(&in); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, nil)
+ return
+ }
+ if in.AppId != pubdef.AppId {
+ common.ServerJSON(c, srvdef.APPIDError, nil)
+ }
+ data, err := srv_app.AppVideoCollectionSync(c, &in)
+ if err != nil {
+ log.Error("AppCollectionSyncReq AppVideoCollectionSync Failed", log.Any("err", err))
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+func AppGetVideoCategory(c *gin.Context) {
+ data, err := srv_app.AppCategoryList(c)
+ if err != nil {
+ log.Error("AppGetVideoCategory AppCategoryList Failed", log.Any("err", err))
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+func AppGetVideoCreator(c *gin.Context) {
+ data, err := srv_app.AppCreatorList(c)
+ if err != nil {
+ log.Error("AppGetVideoCreator AppCategoryList Failed", log.Any("err", err))
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+func AppGetVideoTag(c *gin.Context) {
+ data, err := srv_app.AppTagList(c)
+ if err != nil {
+ log.Error("AppGetVideoTag AppCategoryList Failed", log.Any("err", err))
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, data)
+}
\ No newline at end of file
diff --git a/web/api/web_cfg/cfg.go b/web/api/web_cfg/cfg.go
new file mode 100644
index 0000000..94ef049
--- /dev/null
+++ b/web/api/web_cfg/cfg.go
@@ -0,0 +1,75 @@
+package web_cfg
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_cfg"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 系统配置
+// @Summary 获取系统配置
+// @Description 获取系统配置
+// @Accept json
+// @Produce json
+// @Param param body GetSysConfigReq true "参数列表"
+// @Success 200 {object} GetSysConfigResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/syscfg/get [GET]
+func GetCfg(ctx *gin.Context) {
+ config, err := srv_cfg.GetSystemConfig(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Error())
+ return
+ }
+ resp := &GetSysConfigResp{config}
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 系统配置
+// @Summary 修改系统配置
+// @Description 修改系统配置
+// @Accept json
+// @Produce json
+// @Param param body SysConfigUpdateReq true "参数列表"
+// @Success 200 {object} SysConfigUpdateResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/syscfg/update [POST]
+func UpdateCfg(ctx *gin.Context) {
+ var req SysConfigUpdateReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ if err := srv_cfg.UpdateSystemConfig(ctx, &req.SysCfgUpdate); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Error())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, &SysConfigUpdateResp{})
+}
+
+// @Tags 系统配置
+// @Summary 设置媒资库 appKey
+// @Description 单独设置 sys_cfg.appKey,并刷新内存缓存
+// @Accept json
+// @Produce json
+// @Param param body SetAppKeyReq true "参数列表"
+// @Success 200 {object} SetAppKeyResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/syscfg/appkey [POST]
+func SetAppKey(ctx *gin.Context) {
+ var req SetAppKeyReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ if err := srv_cfg.SetAppKey(ctx, req.AppKey); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Error())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, &SetAppKeyResp{})
+}
diff --git a/web/api/web_cfg/protodef.go b/web/api/web_cfg/protodef.go
new file mode 100644
index 0000000..da1a171
--- /dev/null
+++ b/web/api/web_cfg/protodef.go
@@ -0,0 +1,24 @@
+package web_cfg
+
+import (
+ "videoweb/pub/models/cfg"
+ "videoweb/web/service/srv_cfg"
+)
+
+type GetSysConfigReq struct {
+}
+
+type GetSysConfigResp struct {
+ *cfg.SysCfg
+}
+
+type SysConfigUpdateReq struct {
+ srv_cfg.SysCfgUpdate
+}
+type SysConfigUpdateResp struct{}
+
+type SetAppKeyReq struct {
+ AppKey string `json:"appKey"` // 媒资库 appKey
+}
+
+type SetAppKeyResp struct{}
diff --git a/web/api/web_commend/commend.go b/web/api/web_commend/commend.go
new file mode 100644
index 0000000..09821aa
--- /dev/null
+++ b/web/api/web_commend/commend.go
@@ -0,0 +1,174 @@
+package web_commend
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_commend"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SearchManyCommend
+// @Tags 评论
+// @Summary 评论列表
+// @Description 评论列表
+// @Accept json
+// @Produce json
+// @Param param body srv_commend.CommendSearch true "参数列表"
+// @Success 200 {object} srv_commend.CommendSearchResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/web/post/comment/list [POST]
+func SearchManyCommend(ctx *gin.Context) {
+ var req = &srv_commend.CommendSearch{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ resp, serr := srv_commend.SearchComandList(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+type CommendDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+type CommendDelIpReq struct {
+ Ip string `json:"ip"`
+}
+
+// DelCommend
+// @Tags 评论
+// @Summary 删除一个评论
+// @Description 删除一个评论
+// @Accept json
+// @Produce json
+// @Param param body CommendDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/web/post/comment/del [POST]
+func DelCommend(ctx *gin.Context) {
+ var req = &CommendDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_commend.DelCommend(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
+
+// DelUserCommend
+// @Tags 评论
+// @Summary 删除一个用户所有评论
+// @Description 删除一个用户所有评论
+// @Accept json
+// @Produce json
+// @Param param body CommendDelIpReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/web/post/comment/user/del [POST]
+func DelUserCommend(ctx *gin.Context) {
+ var req = &CommendDelIpReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_commend.DelUserCommend(ctx, req.Ip)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
+
+// ReplayPostComment
+// @Tags 评论
+// @Summary 回复评论
+// @Description 回复评论
+// @Accept json
+// @Produce json
+// @Param param body srv_commend.CommendReplay true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/web/post/comment/replay [POST]
+func ReplayPostComment(ctx *gin.Context) {
+ var req = &srv_commend.CommendReplay{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_commend.AddPostComment(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
+
+type CommendReviewBatchReq struct {
+ Ids *[]int64 `json:"ids"`
+ ReviewStatus int `json:"reviewStatus"`
+}
+
+// ReviewCommendBatch
+// @Tags 评论
+// @Summary 批量审核评论
+// @Description 批量审核评论
+// @Accept json
+// @Produce json
+// @Param param body CommendReviewBatchReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/post/comment/reviewBatch [POST]
+func ReviewCommendBatch(ctx *gin.Context) {
+ var req = &CommendReviewBatchReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_commend.ReviewCommendBatch(ctx, req.Ids, req.ReviewStatus)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
+
+// ReviewComment
+// @Tags 评论
+// @Summary 审核评论
+// @Description 审核评论
+// @Accept json
+// @Produce json
+// @Param param body srv_commend.CommendReviewReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/post/comment/review [POST]
+func ReviewComment(ctx *gin.Context) {
+ var req = &srv_commend.CommendReviewReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_commend.ReviewComment(ctx, req.Id, req.ReviewStatus)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
diff --git a/web/api/web_contactus/contactus.go b/web/api/web_contactus/contactus.go
new file mode 100644
index 0000000..f47a362
--- /dev/null
+++ b/web/api/web_contactus/contactus.go
@@ -0,0 +1,81 @@
+package web_contactus
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_contactus"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 联系我们
+// @Summary 联系我们列表
+// @Description 联系我们列表
+// @Accept json
+// @Produce json
+// @Param param body srv_contactus.GetContactusListReq true "参数列表"
+// @Success 200 {object} srv_contactus.GetContactusListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/web/contactus/list [POST]
+func ContactusList(ctx *gin.Context) {
+ var req = &srv_contactus.GetContactusListReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_contactus.GetContactusList(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 联系我们
+// @Summary 删除一个联系我们
+// @Description 删除一个联系我们
+// @Accept json
+// @Produce json
+// @Param param body srv_contactus.DelOneContactusReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/web/contactus/del [POST]
+func Del(ctx *gin.Context) {
+ var req = &srv_contactus.DelOneContactusReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_contactus.DelContactus(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
+
+// @Tags 联系我们
+// @Summary 修改联系我们状态
+// @Description 修改联系我们状态
+// @Accept json
+// @Produce json
+// @Param param body srv_contactus.UpdateContactusReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/contactus/update [POST]
+func Update(ctx *gin.Context) {
+ var req = &srv_contactus.UpdateContactusReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_contactus.UpdateContactus(ctx, req.Id, req.Status)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
diff --git a/web/api/web_contentexcel/contentexcel.go b/web/api/web_contentexcel/contentexcel.go
new file mode 100644
index 0000000..d6bc7fb
--- /dev/null
+++ b/web/api/web_contentexcel/contentexcel.go
@@ -0,0 +1,62 @@
+package web_contentexcel
+
+import (
+ "fmt"
+ "net/http"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_contentexcel"
+
+ "github.com/gin-gonic/gin"
+)
+
+// /api/web/contentexcel/export [GET]
+func Export(ctx *gin.Context) {
+ req := &srv_contentexcel.ExportReq{}
+ if err := ctx.ShouldBindQuery(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ ids, serr := srv_contentexcel.ParseExportIDs(ctx.QueryArray("ids"))
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ if len(ids) > 0 {
+ req.Ids = ids
+ }
+
+ data, filename, serr := srv_contentexcel.Export(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
+ ctx.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
+}
+
+// /api/web/contentexcel/import [POST]
+func Import(ctx *gin.Context) {
+ contentType := ctx.PostForm("type")
+ fileHeader, err := ctx.FormFile("file")
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "请上传 file 字段")
+ return
+ }
+
+ file, err := fileHeader.Open()
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ defer file.Close()
+
+ result, serr := srv_contentexcel.Import(ctx, contentType, file)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, result)
+}
diff --git a/web/api/web_contenttag/tag.go b/web/api/web_contenttag/tag.go
new file mode 100644
index 0000000..7531045
--- /dev/null
+++ b/web/api/web_contenttag/tag.go
@@ -0,0 +1,131 @@
+package web_contenttag
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/middleware/auth"
+ "videoweb/web/service/srv_contenttag"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 统一标签列表
+// @Tags web统一标签相关接口
+// @Summary 统一标签列表 -- by 王鸣
+// @Description 分页查询统一标签配置
+// @Accept json
+// @Produce json
+// @Param params query proto.ContentTagListReq true "统一标签列表请求"
+// @Success 200 {object} proto.ContentTagListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/content/tag/list [get]
+func List(ctx *gin.Context) {
+ var req proto.ContentTagListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_contenttag.ListContentTag(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Option 统一标签下拉列表
+// @Tags web统一标签相关接口
+// @Summary 统一标签下拉列表 -- by 王鸣
+// @Description 查询启用的统一标签下拉列表,传type时按支持类型过滤
+// @Accept json
+// @Produce json
+// @Param params query proto.ContentTagOptionReq false "统一标签下拉列表请求"
+// @Success 200 {object} proto.ContentTagOptionResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/content/tag/option [get]
+func Option(ctx *gin.Context) {
+ var req proto.ContentTagOptionReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_contenttag.ListContentTagOption(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Add 新增统一标签
+// @Tags web统一标签相关接口
+// @Summary 新增统一标签 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.ContentTagAddReq true "新增统一标签请求"
+// @Success 200 {object} proto.ContentTagAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/content/tag/add [post]
+func Add(ctx *gin.Context) {
+ var req proto.ContentTagAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_contenttag.AddContentTag(ctx, &req, auth.GetAccount(ctx))
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Update 更新统一标签
+// @Tags web统一标签相关接口
+// @Summary 更新统一标签 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.ContentTagUpdateReq true "更新统一标签请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/content/tag/update [post]
+func Update(ctx *gin.Context) {
+ var req proto.ContentTagUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_contenttag.UpdateContentTag(ctx, &req, auth.GetAccount(ctx)); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// Del 删除统一标签
+// @Tags web统一标签相关接口
+// @Summary 删除统一标签 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.ContentTagDelReq true "删除统一标签请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/content/tag/del [post]
+func Del(ctx *gin.Context) {
+ var req proto.ContentTagDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_contenttag.DelContentTag(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_detailnav/detailnav.go b/web/api/web_detailnav/detailnav.go
new file mode 100644
index 0000000..9ac2188
--- /dev/null
+++ b/web/api/web_detailnav/detailnav.go
@@ -0,0 +1,109 @@
+package web_detailnav
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_detailnav"
+
+ "github.com/gin-gonic/gin"
+)
+
+// DetailNavList
+// @Tags 详情页导航配置
+// @Summary 详情页导航配置-列表
+// @Description 详情页导航配置-列表
+// @Accept json
+// @Produce json
+// @Param param body srv_detailnav.GetDetailNavListReq true "参数列表"
+// @Success 200 {object} srv_detailnav.GetDetailNavListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/detailnav/list [POST]
+func DetailNavList(c *gin.Context) {
+ var req srv_detailnav.GetDetailNavListReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, serr := srv_detailnav.GetDetailNavList(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// DetailNavAdd
+// @Tags 详情页导航配置
+// @Summary 详情页导航配置-增加
+// @Description 详情页导航配置-增加
+// @Accept json
+// @Produce json
+// @Param param body srv_detailnav.AddDetailNavReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/detailnav/add [POST]
+func DetailNavAdd(c *gin.Context) {
+ var req srv_detailnav.AddDetailNavReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_detailnav.AddDetailNav(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// DetailNavDel
+// @Tags 详情页导航配置
+// @Summary 详情页导航配置-删除
+// @Description 详情页导航配置-删除
+// @Accept json
+// @Produce json
+// @Param param body srv_detailnav.DelDetailNavReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/detailnav/del [POST]
+func DetailNavDel(c *gin.Context) {
+ var req srv_detailnav.DelDetailNavReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_detailnav.DelDetailNav(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// DetailNavUpdate
+// @Tags 详情页导航配置
+// @Summary 详情页导航配置-更新
+// @Description 详情页导航配置-更新
+// @Accept json
+// @Produce json
+// @Param param body srv_detailnav.UpdateDetailNavReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/detailnav/update [POST]
+func DetailNavUpdate(c *gin.Context) {
+ var req srv_detailnav.UpdateDetailNavReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_detailnav.UpdateDetailNav(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
diff --git a/web/api/web_file/file.go b/web/api/web_file/file.go
new file mode 100644
index 0000000..4e82351
--- /dev/null
+++ b/web/api/web_file/file.go
@@ -0,0 +1,86 @@
+package web_file
+
+import (
+ "io/ioutil"
+ "videoweb/pub/common"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/storage"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 文件上传
+// @Summary 图片上传
+// @Description 图片上传
+// @Accept json
+// @Produce json
+// @Param file formData file true "文件"
+// @Success 200 {object}} UploadImageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/web/file/uploadImg [POST]
+func UploadImage(ctx *gin.Context) {
+ f, err := ctx.FormFile("file")
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ imageUpoadDomain := proxy.BucketImageAddressV2
+ p, err := storage.UploadImage(ctx, f, imageUpoadDomain)
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ServerError, err.Error())
+ return
+ }
+ resp := &UploadImageResp{Path: p}
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 文件上传
+// @Summary 图片批量上传
+// @Description 图片批量上传
+// @Accept json
+// @Produce json
+// @Param file[] formData file true "文件"
+// @Success 200 {object}} UploadImagesResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/web/file/uploadImgs [POST]
+func UploadImages(ctx *gin.Context) {
+ form, err := ctx.MultipartForm()
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp := &UploadImagesResp{
+ Paths: make([]string, 0),
+ }
+ domain := proxy.BucketImageAddressV2
+
+ files := form.File["file[]"]
+ for _, f := range files {
+ fi, err := f.Open()
+ defer func() {
+ _ = fi.Close()
+ }()
+ if err != nil {
+ log.Warn("headers Open file wrong ", log.Any("Error", err))
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ byteData, err := ioutil.ReadAll(fi)
+ if err != nil {
+ log.Warn("request multipart wrong ", log.Any("Error", err))
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ path, err := proxy.UploadImg(f.Filename, byteData, domain)
+ if err != nil {
+ log.Warn("request multipart wrong ", log.Any("Error", err))
+ common.ServerJSON(ctx, srvdef.UploadToFileSrvFailed, err.Error())
+ return
+ }
+ resp.Paths = append(resp.Paths, path)
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/web/api/web_file/proto.go b/web/api/web_file/proto.go
new file mode 100644
index 0000000..03580af
--- /dev/null
+++ b/web/api/web_file/proto.go
@@ -0,0 +1,14 @@
+package web_file
+
+type UploadImageResp struct {
+ Path string `json:"path"`
+}
+
+type UploadImagesResp struct {
+ Paths []string `json:"paths"`
+}
+
+type UploadVideoBodyResp struct {
+ Id string `json:"id"` // 文件服返回的上传文件Id
+ VideoUri string `json:"videoUri"` // 文件服返回的视频uri
+}
diff --git a/web/api/web_homebanner/banner.go b/web/api/web_homebanner/banner.go
new file mode 100644
index 0000000..5add334
--- /dev/null
+++ b/web/api/web_homebanner/banner.go
@@ -0,0 +1,106 @@
+package web_homebanner
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/middleware/auth"
+ "videoweb/web/service/srv_homebanner"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 首页banner列表
+// @Tags web首页banner相关接口
+// @Summary 首页banner列表 -- by 王鸣
+// @Description 分页查询首页banner配置
+// @Accept json
+// @Produce json
+// @Param params query proto.HomeBannerListReq true "首页banner列表请求"
+// @Success 200 {object} proto.HomeBannerListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/home/banner/list [get]
+func List(ctx *gin.Context) {
+ var req proto.HomeBannerListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_homebanner.ListHomeBanner(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Add 新增首页banner
+// @Tags web首页banner相关接口
+// @Summary 新增首页banner -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.HomeBannerAddReq true "新增首页banner请求"
+// @Success 200 {object} proto.HomeBannerAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/home/banner/add [post]
+func Add(ctx *gin.Context) {
+ var req proto.HomeBannerAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_homebanner.AddHomeBanner(ctx, &req, auth.GetAccount(ctx))
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Update 更新首页banner
+// @Tags web首页banner相关接口
+// @Summary 更新首页banner -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.HomeBannerUpdateReq true "更新首页banner请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/home/banner/update [post]
+func Update(ctx *gin.Context) {
+ var req proto.HomeBannerUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_homebanner.UpdateHomeBanner(ctx, &req, auth.GetAccount(ctx)); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// Del 删除首页banner
+// @Tags web首页banner相关接口
+// @Summary 删除首页banner -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.HomeBannerDelReq true "删除首页banner请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/home/banner/del [post]
+func Del(ctx *gin.Context) {
+ var req proto.HomeBannerDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_homebanner.DelHomeBanner(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_hotnav/hotnav.go b/web/api/web_hotnav/hotnav.go
new file mode 100644
index 0000000..fc7c946
--- /dev/null
+++ b/web/api/web_hotnav/hotnav.go
@@ -0,0 +1,109 @@
+package web_hotnav
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_hotnav"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HotNavList
+// @Tags 热门导航配置
+// @Summary 热门导航配置-列表
+// @Description 热门导航配置-列表
+// @Accept json
+// @Produce json
+// @Param param body srv_hotnav.GetHotNavListReq true "参数列表"
+// @Success 200 {object} srv_hotnav.GetHotNavListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/hotnav/list [post]
+func HotNavList(c *gin.Context) {
+ var req srv_hotnav.GetHotNavListReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, serr := srv_hotnav.GetHotNavList(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// HotNavAdd
+// @Tags 热门导航配置
+// @Summary 热门导航配置-增加
+// @Description 热门导航配置-增加
+// @Accept json
+// @Produce json
+// @Param param body srv_hotnav.AddHotNavReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/hotnav/add [post]
+func HotNavAdd(c *gin.Context) {
+ var req srv_hotnav.AddHotNavReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_hotnav.AddHotNav(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// HotNavDel
+// @Tags 热门导航配置
+// @Summary 热门导航配置-删除
+// @Description 热门导航配置-删除
+// @Accept json
+// @Produce json
+// @Param param body srv_hotnav.DelHotNavReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/hotnav/del [post]
+func HotNavDel(c *gin.Context) {
+ var req srv_hotnav.DelHotNavReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_hotnav.DelHotNav(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// HotNavUpdate
+// @Tags 热门导航配置
+// @Summary 热门导航配置-更新
+// @Description 热门导航配置-更新
+// @Accept json
+// @Produce json
+// @Param param body srv_hotnav.UpdateHotNavReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/hotnav/update [post]
+func HotNavUpdate(c *gin.Context) {
+ var req srv_hotnav.UpdateHotNavReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ serr := srv_hotnav.UpdateHotNav(c, &req)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
diff --git a/web/api/web_ipblack/ipblack.go b/web/api/web_ipblack/ipblack.go
new file mode 100644
index 0000000..a8052cf
--- /dev/null
+++ b/web/api/web_ipblack/ipblack.go
@@ -0,0 +1,83 @@
+package web_ipblack
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_ipblack"
+)
+
+// IpBlackList
+// @Tags IP黑名单
+// @Summary IP黑名单-列表
+// @Description IP黑名单-列表
+// @Accept json
+// @Produce json
+// @Param param body srv_ipblack.GetIPBlackListReq true "参数列表"
+// @Success 200 {object} srv_ipblack.GetIPBlackListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/ipblack/list [POST]
+func IpBlackList(c *gin.Context) {
+ var req srv_ipblack.GetIPBlackListReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, err := srv_ipblack.GetIpBlackList(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, resp)
+}
+
+// IpBlackAdd
+// @Tags IP黑名单
+// @Summary IP黑名单-增加
+// @Description IP黑名单-增加
+// @Accept json
+// @Produce json
+// @Param param body srv_ipblack.AddIPBlackListReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/ipblack/add [POST]
+func IpBlackAdd(c *gin.Context) {
+ var req srv_ipblack.AddIPBlackListReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ err := srv_ipblack.AddIpBlack(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
+
+// IpBlackDel
+// @Tags IP黑名单
+// @Summary IP黑名单-删除
+// @Description IP黑名单-删除
+// @Accept json
+// @Produce json
+// @Param param body srv_ipblack.DelIpBlackListReq true "参数列表"
+// @Success 200 {object} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/ipblack/del [POST]
+func IpBlackDel(c *gin.Context) {
+ var req srv_ipblack.DelIpBlackListReq
+ if err := c.ShouldBind(&req); err != nil {
+ common.ServerJSON(c, srvdef.ErrParamError, err.Error())
+ return
+ }
+ err := srv_ipblack.DelIpBlack(c, &req)
+ if err != nil {
+ common.ServerJSON(c, err.Code, "")
+ return
+ }
+ common.ServerJSON(c, srvdef.Success, "")
+}
\ No newline at end of file
diff --git a/web/api/web_mediaresource/resource.go b/web/api/web_mediaresource/resource.go
new file mode 100644
index 0000000..4688c71
--- /dev/null
+++ b/web/api/web_mediaresource/resource.go
@@ -0,0 +1,87 @@
+package web_mediaresource
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_mediaresource"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ResourceList web端短剧媒资列表
+// @Tags web媒资库相关接口
+// @Summary web端短剧媒资列表 -- by 王鸣
+// @Description 分页查询短剧媒资列表
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletResourceListReq true "短剧媒资列表请求"
+// @Success 200 {object} proto.PlayletResourceListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/resource/list [get]
+func ResourceList(ctx *gin.Context) {
+ var req proto.PlayletResourceListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_mediaresource.ListPlayletResource(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ResourceImport web端短剧媒资入库
+// @Tags web媒资库相关接口
+// @Summary web端短剧媒资入库 -- by 王鸣
+// @Description 提交媒资入库任务,后台从媒资中心同步短剧详情并写入短剧和章节集合
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletResourceUseReq true "短剧媒资入库请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/resource/import [post]
+func ResourceImport(ctx *gin.Context) {
+ var req proto.PlayletResourceUseReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ log.Error("短剧媒资入库参数绑定失败", log.E(err))
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_mediaresource.ImportPlayletResource(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ResourceChapters web端短剧媒资剧集列表
+// @Tags web媒资库相关接口
+// @Summary web端短剧媒资剧集列表 -- by 王鸣
+// @Description 按媒资ID查询剧集信息,复用入库时的 mediaSync 接口
+// @Accept json
+// @Produce json
+// @Param id query int true "媒资中心资源ID"
+// @Success 200 {object} proto.PlayletResourceChapterListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/resource/chapters [get]
+func ResourceChapters(ctx *gin.Context) {
+ var req proto.PlayletResourceChapterListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_mediaresource.ListPlayletResourceChapters(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/web/api/web_playlet/category.go b/web/api/web_playlet/category.go
new file mode 100644
index 0000000..9107147
--- /dev/null
+++ b/web/api/web_playlet/category.go
@@ -0,0 +1,105 @@
+package web_playlet
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+// CategoryList web端短剧分类列表
+// @Tags web短剧相关接口
+// @Summary web端短剧分类列表 -- by 王鸣
+// @Description 分页查询短剧分类
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletCategoryListReq true "短剧分类列表请求"
+// @Success 200 {object} proto.PlayletCategoryListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/category/list [get]
+func CategoryList(ctx *gin.Context) {
+ var req proto.PlayletCategoryListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlayletCategory(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// CategoryAdd web端新增短剧分类
+// @Tags web短剧相关接口
+// @Summary web端新增短剧分类 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletCategoryAddReq true "新增短剧分类请求"
+// @Success 200 {object} proto.PlayletCategoryAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/category/add [post]
+func CategoryAdd(ctx *gin.Context) {
+ var req proto.PlayletCategoryAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.AddPlayletCategory(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// CategoryUpdate web端更新短剧分类
+// @Tags web短剧相关接口
+// @Summary web端更新短剧分类 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletCategoryUpdateReq true "更新短剧分类请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/category/update [post]
+func CategoryUpdate(ctx *gin.Context) {
+ var req proto.PlayletCategoryUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.UpdatePlayletCategory(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// CategoryDel web端删除短剧分类
+// @Tags web短剧相关接口
+// @Summary web端删除短剧分类 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletCategoryDelReq true "删除短剧分类请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/category/del [post]
+func CategoryDel(ctx *gin.Context) {
+ var req proto.PlayletCategoryDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.DelPlayletCategory(ctx, req.Id); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_playlet/home_config.go b/web/api/web_playlet/home_config.go
new file mode 100644
index 0000000..dc96c8e
--- /dev/null
+++ b/web/api/web_playlet/home_config.go
@@ -0,0 +1,130 @@
+package web_playlet
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HomeConfigList web端首页配置列表
+// @Tags web短剧相关接口
+// @Summary web端首页配置列表
+// @Description 分页查询短剧首页配置
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletHomeConfigListReq true "首页配置列表请求"
+// @Success 200 {object} proto.PlayletHomeConfigListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/home-config/list [get]
+func HomeConfigList(ctx *gin.Context) {
+ var req proto.PlayletHomeConfigListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlayletHomeConfig(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// HomeConfigTopicOptions web端首页专题下拉选项
+// @Tags web短剧相关接口
+// @Summary web端分页查询首页专题可选项
+// @Description dataType 1查询短剧题材 2查询视频分类,排除其他已启用模块占用的专题,configId用于回显当前配置已选项
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletHomeTopicOptionsReq true "首页专题选项请求"
+// @Success 200 {object} proto.PlayletHomeTopicOptionsResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/home-config/topic/options [get]
+func HomeConfigTopicOptions(ctx *gin.Context) {
+ var req proto.PlayletHomeTopicOptionsReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlayletHomeTopicOptions(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// HomeConfigAdd web端新增首页配置
+// @Tags web短剧相关接口
+// @Summary web端新增首页配置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletHomeConfigAddReq true "新增首页配置请求"
+// @Success 200 {object} proto.PlayletHomeConfigAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/home-config/add [post]
+func HomeConfigAdd(ctx *gin.Context) {
+ var req proto.PlayletHomeConfigAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.AddPlayletHomeConfig(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// HomeConfigUpdate web端更新首页配置
+// @Tags web短剧相关接口
+// @Summary web端更新首页配置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletHomeConfigUpdateReq true "更新首页配置请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/home-config/update [post]
+func HomeConfigUpdate(ctx *gin.Context) {
+ var req proto.PlayletHomeConfigUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.UpdatePlayletHomeConfig(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// HomeConfigDel web端删除首页配置
+// @Tags web短剧相关接口
+// @Summary web端删除首页配置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletHomeConfigDelReq true "删除首页配置请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/home-config/del [post]
+func HomeConfigDel(ctx *gin.Context) {
+ var req proto.PlayletHomeConfigDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.DelPlayletHomeConfig(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_playlet/playlet.go b/web/api/web_playlet/playlet.go
new file mode 100644
index 0000000..93e9ec0
--- /dev/null
+++ b/web/api/web_playlet/playlet.go
@@ -0,0 +1,543 @@
+package web_playlet
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/middleware/auth"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/service/srv_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List web端短剧列表
+// @Tags web短剧相关接口
+// @Summary web端短剧列表 -- by 王鸣
+// @Description 分页查询短剧列表
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletListReq true "短剧列表请求"
+// @Success 200 {object} proto.PlayletListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/list [get]
+func List(ctx *gin.Context) {
+ var req proto.PlayletListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlaylet(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Add web端新增短剧
+// @Tags web短剧相关接口
+// @Summary web端新增短剧 -- by 王鸣
+// @Description 新增 playlet 集合短剧数据,支持分类、题材、标签、支付方式、推荐和 banner 配置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletAddReq true "新增短剧请求"
+// @Success 200 {object} proto.PlayletAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/add [post]
+func Add(ctx *gin.Context) {
+ var req proto.PlayletAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.AddPlaylet(ctx, &req, auth.GetAccount(ctx))
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Update web端编辑短剧
+// @Tags web短剧相关接口
+// @Summary web端编辑短剧 -- by 王鸣
+// @Description 按短剧ID编辑 playlet 集合短剧数据,只更新请求中传入的字段
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletUpdateReq true "编辑短剧请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/update [post]
+func Update(ctx *gin.Context) {
+ var req proto.PlayletUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.UpdatePlaylet(ctx, &req, auth.GetAccount(ctx)); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// Del 删除短剧及其关联数据
+// @Tags web短剧相关接口
+// @Summary web端单/批量删除短剧
+// @Description 删除短剧、剧集及关联用户数据,上架和下架短剧均可删除
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletDelReq true "删除短剧请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/del [post]
+func Del(ctx *gin.Context) {
+ var req proto.PlayletDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.DelPlaylet(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetStatus web端单/批量设置短剧上下架
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧上下架 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 status 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletStatusSetReq true "单/批量设置短剧上下架请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/status/set [post]
+func SetStatus(ctx *gin.Context) {
+ var req proto.PlayletStatusSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletStatus(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetRecommend web端单/批量设置短剧是否推荐
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧是否推荐 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletRecommendSetReq true "单/批量设置短剧是否推荐请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/recommend/set [post]
+func SetRecommend(ctx *gin.Context) {
+ var req proto.PlayletRecommendSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletRecommend(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetFeatured web端单/批量设置短剧是否爆剧
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧是否爆剧 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 isFeatured 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletFeaturedSetReq true "单/批量设置短剧是否爆剧请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/featured/set [post]
+func SetFeatured(ctx *gin.Context) {
+ var req proto.PlayletFeaturedSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletFeatured(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetBanner web端单/批量设置短剧是否显示在首页banner
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧是否显示在首页banner -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 showOnBanner 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletBannerSetReq true "单/批量设置短剧是否显示在首页banner请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/banner/set [post]
+func SetBanner(ctx *gin.Context) {
+ var req proto.PlayletBannerSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletBanner(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetTheme web端单/批量设置短剧题材
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧题材 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletThemeSetReq true "单/批量设置短剧题材请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/theme/set [post]
+func SetTheme(ctx *gin.Context) {
+ var req proto.PlayletThemeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletTheme(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetTag web端单/批量设置短剧标签
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧标签 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 tagIds 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletTagSetReq true "单/批量设置短剧标签请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/tag/set [post]
+func SetTag(ctx *gin.Context) {
+ var req proto.PlayletTagSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletTag(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetCategory web端单/批量设置短剧分类
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧分类 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 categoryIds 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletCategorySetReq true "单/批量设置短剧分类请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/category/set [post]
+func SetCategory(ctx *gin.Context) {
+ var req proto.PlayletCategorySetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletCategory(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetVirtualLike web端单/批量设置短剧虚拟点赞数
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧虚拟点赞数 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 virtualCountLike 字段,每条短剧从最小值和最大值之间随机取不重复数值入库
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletVirtualLikeSetReq true "单/批量随机设置短剧虚拟点赞数请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/like/virtual/set [post]
+func SetVirtualLike(ctx *gin.Context) {
+ var req proto.PlayletVirtualLikeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletVirtualLike(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetVirtualBrowse web端单/批量设置短剧虚拟观看数
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧虚拟观看数 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletVirtualBrowseSetReq true "设置短剧虚拟观看数请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/browse/virtual/set [post]
+func SetVirtualBrowse(ctx *gin.Context) {
+ var req proto.PlayletVirtualBrowseSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletVirtualBrowse(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetVirtualHeat web端单/批量设置短剧虚拟热度
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧虚拟热度 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletVirtualHeatSetReq true "设置短剧虚拟热度请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/heat/virtual/set [post]
+func SetVirtualHeat(ctx *gin.Context) {
+ var req proto.PlayletVirtualHeatSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletVirtualHeat(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetPayType web端单/批量设置短剧默认支付方式
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧默认支付方式 -- by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 payType/singleCoins 字段
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletPayTypeSetReq true "单/批量设置短剧默认支付方式请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/pay-type/set [post]
+func SetPayType(ctx *gin.Context) {
+ var req proto.PlayletPayTypeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletPayType(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ResetWatermark web端按短剧ID批量重置剧集水印
+// @Tags web短剧相关接口
+// @Summary web端按短剧ID批量重置剧集水印 -- by 王鸣
+// @Description 根据短剧ID集合重置剧集水印状态,保留已有水印地址以便换水印期间继续观看
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletWatermarkResetReq true "批量重置短剧水印请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/watermark/reset [post]
+func ResetWatermark(ctx *gin.Context) {
+ var req proto.PlayletWatermarkResetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.ResetPlayletWatermark(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterList web端短剧剧集列表
+// @Tags web短剧相关接口
+// @Summary web端短剧剧集列表 -- by 王鸣
+// @Description 分页查询 playlet_chapter 集合短剧剧集数据,默认按集数升序返回
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletChapterListReq true "短剧剧集列表请求"
+// @Success 200 {object} proto.PlayletChapterListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/chapter/list [get]
+func ChapterList(ctx *gin.Context) {
+ var req proto.PlayletChapterListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlayletChapter(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterAdd web端新增短剧剧集
+// @Tags web短剧相关接口
+// @Summary web端新增短剧剧集 -- by 王鸣
+// @Description 新增 playlet_chapter 集合短剧剧集数据
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletChapterAddReq true "新增短剧剧集请求"
+// @Success 200 {object} proto.PlayletChapterAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/chapter/add [post]
+func ChapterAdd(ctx *gin.Context) {
+ var req proto.PlayletChapterAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.AddPlayletChapter(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterDel web端单/批量删除短剧剧集
+// @Tags web短剧相关接口
+// @Summary web端单/批量删除短剧剧集
+// @Description 根据剧集ID集合删除 playlet_chapter 记录,仅原创短剧支持删除
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletChapterDelReq true "单/批量删除短剧剧集请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/chapter/del [post]
+func ChapterDel(ctx *gin.Context) {
+ var req proto.PlayletChapterDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.DelPlayletChapter(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterSync web端手动同步短剧剧集
+// @Tags web短剧相关接口
+// @Summary web端手动单/批量同步短剧剧集
+// @Description 根据短剧ID集合从媒资中心同步新增剧集,playletIds传1个表示单条同步,传多个表示批量同步,允许已完结短剧补集或修正状态
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletChapterSyncReq true "手动单/批量同步短剧剧集请求"
+// @Success 200 {object} proto.PlayletChapterSyncResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/chapter/sync [post]
+func ChapterSync(ctx *gin.Context) {
+ var req proto.PlayletChapterSyncReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_mediaresource.SyncPlayletChapters(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterUpdate web端编辑短剧剧集
+// @Tags web短剧相关接口
+// @Summary web端编辑短剧剧集 -- by 王鸣
+// @Description 按剧集ID编辑 playlet_chapter 集合短剧剧集数据,只更新请求中传入的字段
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletChapterUpdateReq true "编辑短剧剧集请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/chapter/update [post]
+func ChapterUpdate(ctx *gin.Context) {
+ var req proto.PlayletChapterUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.UpdatePlayletChapter(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterSetPayType web端单/批量设置短剧剧集支付方式
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧剧集支付方式 -- by 王鸣
+// @Description 根据剧集ID集合批量更新 playlet_chapter 集合 payType 和 singleCoins 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletChapterPayTypeSetReq true "单/批量设置短剧剧集支付方式请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/chapter/pay-type/set [post]
+func ChapterSetPayType(ctx *gin.Context) {
+ var req proto.PlayletChapterPayTypeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletChapterPayType(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_playlet/tag.go b/web/api/web_playlet/tag.go
new file mode 100644
index 0000000..c8088b4
--- /dev/null
+++ b/web/api/web_playlet/tag.go
@@ -0,0 +1,105 @@
+package web_playlet
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+// TagList web端标签列表
+// @Tags web短剧相关接口
+// @Summary web端标签列表 -- by 王鸣
+// @Description 分页查询短剧标签
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletTagListReq true "标签列表请求"
+// @Success 200 {object} proto.PlayletTagListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/tag/list [get]
+func TagList(ctx *gin.Context) {
+ var req proto.PlayletTagListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlayletTag(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// TagAdd web端新增标签
+// @Tags web短剧相关接口
+// @Summary web端新增标签 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletTagAddReq true "新增标签请求"
+// @Success 200 {object} proto.PlayletTagAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/tag/add [post]
+func TagAdd(ctx *gin.Context) {
+ var req proto.PlayletTagAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.AddPlayletTag(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// TagUpdate web端更新标签
+// @Tags web短剧相关接口
+// @Summary web端更新标签 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletTagUpdateReq true "更新标签请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/tag/update [post]
+func TagUpdate(ctx *gin.Context) {
+ var req proto.PlayletTagUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.UpdatePlayletTag(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// TagDel web端删除标签
+// @Tags web短剧相关接口
+// @Summary web端删除标签 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletTagDelReq true "删除标签请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/tag/del [post]
+func TagDel(ctx *gin.Context) {
+ var req proto.PlayletTagDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.DelPlayletTag(ctx, req.Id); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_playlet/theme.go b/web/api/web_playlet/theme.go
new file mode 100644
index 0000000..14aa38d
--- /dev/null
+++ b/web/api/web_playlet/theme.go
@@ -0,0 +1,105 @@
+package web_playlet
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ThemeList web端题材列表
+// @Tags web短剧相关接口
+// @Summary web端题材列表 -- by 王鸣
+// @Description 分页查询短剧题材
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletThemeListReq true "题材列表请求"
+// @Success 200 {object} proto.PlayletThemeListPageResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/theme/list [get]
+func ThemeList(ctx *gin.Context) {
+ var req proto.PlayletThemeListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.ListPlayletTheme(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ThemeAdd web端新增题材
+// @Tags web短剧相关接口
+// @Summary web端新增题材 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletThemeAddReq true "新增题材请求"
+// @Success 200 {object} proto.PlayletThemeAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/theme/add [post]
+func ThemeAdd(ctx *gin.Context) {
+ var req proto.PlayletThemeAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playlet.AddPlayletTheme(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ThemeUpdate web端更新题材
+// @Tags web短剧相关接口
+// @Summary web端更新题材 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletThemeUpdateReq true "更新题材请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/theme/update [post]
+func ThemeUpdate(ctx *gin.Context) {
+ var req proto.PlayletThemeUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.UpdatePlayletTheme(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ThemeDel web端删除题材
+// @Tags web短剧相关接口
+// @Summary web端删除题材 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletThemeDelReq true "删除题材请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/theme/del [post]
+func ThemeDel(ctx *gin.Context) {
+ var req proto.PlayletThemeDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.DelPlayletTheme(ctx, req.Id); err != nil {
+ common.ServerJSON(ctx, err, err.GetMsg())
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_questionnaire/questionnaire.go b/web/api/web_questionnaire/questionnaire.go
new file mode 100644
index 0000000..35cc2f6
--- /dev/null
+++ b/web/api/web_questionnaire/questionnaire.go
@@ -0,0 +1,55 @@
+package web_questionnaire
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_questionnaire"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 问卷调查
+// @Summary 查询问卷列表
+// @Description 支持分页及条件筛选(question_type、status、时间范围)
+// @Accept json
+// @Produce json
+// @Param param body srv_questionnaire.ListReq true "查询参数"
+// @Success 200 {object} srv_questionnaire.ListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/web/questionnaire/list [POST]
+func List(ctx *gin.Context) {
+ var req srv_questionnaire.ListReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_questionnaire.List(ctx, &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 问卷调查
+// @Summary 处理问卷
+// @Description 只有状态为"待处理"的问卷才允许更新
+// @Accept json
+// @Produce json
+// @Param param body srv_questionnaire.HandleReq true "处理参数"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/web/questionnaire/handle [POST]
+func Handle(ctx *gin.Context) {
+ var req srv_questionnaire.HandleReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := srv_questionnaire.Handle(ctx, &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
diff --git a/web/api/web_richtextcontent/richtextcontent.go b/web/api/web_richtextcontent/richtextcontent.go
new file mode 100644
index 0000000..023e08c
--- /dev/null
+++ b/web/api/web_richtextcontent/richtextcontent.go
@@ -0,0 +1,111 @@
+package web_richtextcontent
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/richtextcontentmod"
+ "videoweb/web/service/srv_richtextcontent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 富文本内容
+// @Summary 分组分页获取富文本内容
+// @Description 分组分页获取富文本内容
+// @Accept json
+// @Produce json
+// @Param param body srv_richtextcontent.GetRichTextContentListReq true "参数列表"
+// @Success 200 {object} srv_richtextcontent.GetRichTextContentResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/richtextcontent/list [POST]
+func List(ctx *gin.Context) {
+ var req = &srv_richtextcontent.GetRichTextContentListReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_richtextcontent.GetRichTextContentList(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 富文本内容
+// @Summary 添加一个富文本内容
+// @Description 添加一个富文本内容
+// @Accept json
+// @Produce json
+// @Param param body richtextcontentmod.RichTextContentModel true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/richtextcontent/add [POST]
+func Add(ctx *gin.Context) {
+ var req = &richtextcontentmod.RichTextContentModel{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_richtextcontent.AddRichTextContent(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 富文本内容
+// @Summary 删除一个富文本内容
+// @Description 删除一个富文本内容
+// @Accept json
+// @Produce json
+// @Param param body srv_richtextcontent.DelRichTextContentReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/richtextcontent/del [POST]
+func Del(ctx *gin.Context) {
+ var req = &srv_richtextcontent.DelRichTextContentReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_richtextcontent.DelRichTextContent(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 富文本内容
+// @Summary 更新一个富文本内容
+// @Description 更新一个富文本内容
+// @Accept json
+// @Produce json
+// @Param param body srv_richtextcontent.UpdateRichTextContentReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/richtextcontent/update [POST]
+func Update(ctx *gin.Context) {
+ var req = &srv_richtextcontent.UpdateRichTextContentReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_richtextcontent.UpdateRichTextContent(ctx, req.Id, &req.Param)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_slug/slug.go b/web/api/web_slug/slug.go
new file mode 100644
index 0000000..2a9cac1
--- /dev/null
+++ b/web/api/web_slug/slug.go
@@ -0,0 +1,37 @@
+package web_slug
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_slug"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SyncSlug 补齐历史空 slug
+// @Tags web slug相关接口
+// @Summary 同步历史空 slug
+// @Description 为视频、分类、女优、作者、短剧、统一标签等空 slug 按 SEO 关键词或名称自动生成。可重复调用,hasMore 为 true 时继续请求。type 为空处理全部,也可指定 video/videoCategory/actress/videoCreater/playlet/playletCategory/playletTheme/contentTag。
+// @Accept json
+// @Produce json
+// @Param param body proto.SlugSyncReq false "同步参数"
+// @Success 200 {object} proto.SlugSyncResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Security ApiKeyAuth
+// @Router /api/web/slug/sync [POST]
+func SyncSlug(ctx *gin.Context) {
+ var req proto.SlugSyncReq
+ if ctx.Request.ContentLength > 0 {
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ }
+ resp, serr := srv_slug.SyncEmptySlugs(ctx, &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/web/api/web_translation/category.go b/web/api/web_translation/category.go
new file mode 100644
index 0000000..8042d1f
--- /dev/null
+++ b/web/api/web_translation/category.go
@@ -0,0 +1,111 @@
+package web_translation
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/web/service/srv_translationcategory"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 导航分类
+// @Summary 分组分页获取导航分类
+// @Description 分组分页获取导航分类
+// @Accept json
+// @Produce json
+// @Param param body srv_translationcategory.TranslationCategoryReq true "参数列表"
+// @Success 200 {object} srv_translationcategory.TranslationCategoryResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translationcategory/list [POST]
+func GetTranslationCategory(ctx *gin.Context) {
+ var req = &srv_translationcategory.TranslationCategoryReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_translationcategory.GetTranslationCategorys(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 导航分类
+// @Summary 添加一个导航分类
+// @Description 添加一个导航分类
+// @Accept json
+// @Produce json
+// @Param param body translationcategorymod.TranslationCategory true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translationcategory/add [POST]
+func AddOnetTranslationCategory(ctx *gin.Context) {
+ var req = &translationcategorymod.TranslationCategory{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_translationcategory.AddTranslationCategory(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 导航分类
+// @Summary 删除一个导航分类
+// @Description 删除一个导航分类
+// @Accept json
+// @Produce json
+// @Param param body srv_translationcategory.TranslationCategoryDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translationcategory/del [POST]
+func DelOnetTranslationCategory(ctx *gin.Context) {
+ var req = &srv_translationcategory.TranslationCategoryDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_translationcategory.DelTranslationCategory(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 导航分类
+// @Summary 更新一个导航分类
+// @Description 更新一个导航分类
+// @Accept json
+// @Produce json
+// @Param param body srv_translationcategory.TranslationCategoryUpdateReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translationcategory/update [POST]
+func UpdateOnetTranslationCategory(ctx *gin.Context) {
+ var req = &srv_translationcategory.TranslationCategoryUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_translationcategory.UpdateTranslationCategory(ctx, req.Id, req.Param)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_translation/translation.go b/web/api/web_translation/translation.go
new file mode 100644
index 0000000..765d9f5
--- /dev/null
+++ b/web/api/web_translation/translation.go
@@ -0,0 +1,111 @@
+package web_translation
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/translationmod"
+ "videoweb/web/service/srv_translation"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 导航跳转信息
+// @Summary 分组分页获取导航跳转信息
+// @Description 分组分页获取导航跳转信息
+// @Accept json
+// @Produce json
+// @Param param body srv_translation.TranslationInfoReq true "参数列表"
+// @Success 200 {object} srv_translation.TranslationInfoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translation/list [POST]
+func GetTranslation(ctx *gin.Context) {
+ var req = &srv_translation.TranslationInfoReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_translation.GetTranslationInfos(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 导航跳转信息
+// @Summary 添加一个导航跳转信息
+// @Description 添加一个导航跳转信息
+// @Accept json
+// @Produce json
+// @Param param body translationmod.TranslationInfo true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translation/add [POST]
+func AddOnetTranslation(ctx *gin.Context) {
+ var req = &translationmod.TranslationInfo{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_translation.AddTranslationInfo(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 导航跳转信息
+// @Summary 删除一个导航跳转信息
+// @Description 删除一个导航跳转信息
+// @Accept json
+// @Produce json
+// @Param param body srv_translation.TranslationInfoDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translation/del [POST]
+func DelOnetTranslation(ctx *gin.Context) {
+ var req = &srv_translation.TranslationInfoDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_translation.DelTranslationInfo(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 导航跳转信息
+// @Summary 更新一个导航跳转信息
+// @Description 更新一个导航跳转信息
+// @Accept json
+// @Produce json
+// @Param param body srv_translation.TranslationInfoUpdateReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/translation/update [POST]
+func UpdateOnetTranslation(ctx *gin.Context) {
+ var req = &srv_translation.TranslationInfoUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_translation.UpdateTranslationInfo(ctx, req.Id, req.Param)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_user/user.go b/web/api/web_user/user.go
new file mode 100644
index 0000000..eb7e0ae
--- /dev/null
+++ b/web/api/web_user/user.go
@@ -0,0 +1,52 @@
+package web_user
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_user"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 用户管理
+// @Summary 查询用户列表
+// @Accept json
+// @Produce json
+// @Param param body srv_user.UserListReq true "查询参数"
+// @Success 200 {object} srv_user.UserListResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/user/list [POST]
+func GetUserList(ctx *gin.Context) {
+ var req srv_user.UserListReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_user.GetUsers(ctx, &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 用户管理
+// @Summary 更新用户信息
+// @Accept json
+// @Produce json
+// @Param param body srv_user.UserUpdateReq true "更新参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/user/update [POST]
+func UpdateUser(ctx *gin.Context) {
+ var req srv_user.UserUpdateReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if serr := srv_user.UpdateUser(ctx, req.Id, req.Param); serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_video/category.go b/web/api/web_video/category.go
new file mode 100644
index 0000000..41a345e
--- /dev/null
+++ b/web/api/web_video/category.go
@@ -0,0 +1,111 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/web/service/srv_videocategory"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetVideoCategory
+// @Tags 视频分类
+// @Summary 分组分页获取视频分类
+// @Description 分组分页获取视频分类
+// @Accept json
+// @Produce json
+// @Param param body srv_videocategory.VideoCategoryReq true "参数列表"
+// @Success 200 {object} srv_videocategory.VideoCategoryResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocategory/list [POST]
+func GetVideoCategory(ctx *gin.Context) {
+ var req = &srv_videocategory.VideoCategoryReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_videocategory.GetVideoCategorys(ctx, req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// AddOneVideoCategory
+// @Tags 视频分类
+// @Summary 添加一个视频分类
+// @Description 添加一个视频分类
+// @Accept json
+// @Produce json
+// @Param param body videocategorymod.VideoCategory true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocategory/add [POST]
+func AddOneVideoCategory(ctx *gin.Context) {
+ var req = &videocategorymod.VideoCategory{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ err := srv_videocategory.AddVideoCategory(ctx, req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// DelOneVideoCategory
+// @Tags 视频分类
+// @Summary 删除一个视频分类
+// @Description 删除一个视频分类
+// @Accept json
+// @Produce json
+// @Param param body srv_videocategory.VideoCategoryDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocategory/del [POST]
+func DelOneVideoCategory(ctx *gin.Context) {
+ var req = &srv_videocategory.VideoCategoryDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ err := srv_videocategory.DelVideoCategory(ctx, req.Id)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// UpdateOneVideoCategory
+// @Tags 视频分类
+// @Summary 更新一个视频分类
+// @Description 更新一个视频分类
+// @Accept json
+// @Produce json
+// @Param param body srv_videocategory.VideoCategoryUpdate true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocategory/update [POST]
+func UpdateOneVideoCategory(ctx *gin.Context) {
+ var req = &srv_videocategory.VideoCategoryUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ err := srv_videocategory.UpdateVideoCategory(ctx, req.Id, req.Param)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_video/creater.go b/web/api/web_video/creater.go
new file mode 100644
index 0000000..17d7632
--- /dev/null
+++ b/web/api/web_video/creater.go
@@ -0,0 +1,111 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/web/service/srv_videocreater"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 视频作者
+// @Summary 分组分页获取视频作者
+// @Description 分组分页获取视频作者
+// @Accept json
+// @Produce json
+// @Param param body srv_videocreater.VideoCreaterReq true "参数列表"
+// @Success 200 {object} srv_videocreater.VideoCreaterResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocreater/list [POST]
+func GetVideoCreater(ctx *gin.Context) {
+ var req = &srv_videocreater.VideoCreaterReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_videocreater.GetVideoCreaters(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 视频作者
+// @Summary 添加一个视频作者
+// @Description 添加一个视频作者
+// @Accept json
+// @Produce json
+// @Param param body videocreatermod.VideoCreaterInfo true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocreater/add [POST]
+func AddOneVideoCreater(ctx *gin.Context) {
+ var req = &videocreatermod.VideoCreaterInfo{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videocreater.AddVideoCreater(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 视频作者
+// @Summary 删除一个视频作者
+// @Description 删除一个视频作者
+// @Accept json
+// @Produce json
+// @Param param body srv_videocreater.VideoCreaterDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocreater/del [POST]
+func DelOneVideoCreater(ctx *gin.Context) {
+ var req = &srv_videocreater.VideoCreaterDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videocreater.DelVideoCreater(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 视频作者
+// @Summary 更新一个视频作者
+// @Description 更新一个视频作者
+// @Accept json
+// @Produce json
+// @Param param body srv_videocreater.VideoCreaterUpdate true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocreater/update [POST]
+func UpdateOneVideoCreater(ctx *gin.Context) {
+ var req = &srv_videocreater.VideoCreaterUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_videocreater.UpdateVideoCreater(ctx, req.Id, req.Param)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_video/externallist.go b/web/api/web_video/externallist.go
new file mode 100644
index 0000000..9aae43e
--- /dev/null
+++ b/web/api/web_video/externallist.go
@@ -0,0 +1,24 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_videoinfo"
+
+ "github.com/gin-gonic/gin"
+)
+
+// VideoList 按日期查询已上架视频。
+func VideoList(ctx *gin.Context) {
+ param := srv_videoinfo.ExternalVideoListReq{}
+ if err := ctx.ShouldBindQuery(¶m); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ data, serr := srv_videoinfo.SearchExternalVideoList(ctx, ¶m)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, data)
+}
diff --git a/web/api/web_video/hottag.go b/web/api/web_video/hottag.go
new file mode 100644
index 0000000..2d38e7b
--- /dev/null
+++ b/web/api/web_video/hottag.go
@@ -0,0 +1,43 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_videotag"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 标签管理
+// @Summary 查询播放量最多的标签
+// @Description 根据视频累计播放量聚合返回热门标签列表
+// @Accept json
+// @Produce json
+// @Param limit query int false "返回条数,默认10,最大1000"
+// @Success 200 {object} srv_videotag.HotTagResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /wbapi/hjsq/hottag [GET]
+func GetVideoPlayData(ctx *gin.Context) {
+ var req = &srv_videotag.HotTagReq{}
+ if err := ctx.ShouldBindQuery(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_videotag.GetHotTagsByPlayData(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags IP统计
+// @Summary 查询今日和昨日去重访客IP数
+// @Produce json
+// @Success 200 {object} srv_videotag.IpStatReq "成功"
+// @Router /wbapi/hjsq/ipstat [GET]
+func GetIpStatData(ctx *gin.Context) {
+ fetchIpStat := srv_videotag.FetchIpStat(ctx)
+ common.ServerJSON(ctx, srvdef.Success, fetchIpStat)
+}
diff --git a/web/api/web_video/m3u8.go b/web/api/web_video/m3u8.go
new file mode 100644
index 0000000..ec0efff
--- /dev/null
+++ b/web/api/web_video/m3u8.go
@@ -0,0 +1,102 @@
+package web_video
+
+import (
+ "bytes"
+ "go.mongodb.org/mongo-driver/bson"
+ "io"
+ "net/http"
+ "videoweb/pub/common"
+ "videoweb/pub/common/m3u8"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/cfg"
+ "videoweb/web/webg"
+
+ "github.com/gin-gonic/gin"
+)
+
+type fileHandler func(ctx *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error)
+
+func Play(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ m3u8Name := ctx.Param("key")
+
+ var req m3u8.Req
+ if err = ctx.BindQuery(&req); err != nil {
+ return
+ }
+ req.Name = m3u8Name
+ cfgSys, _ := cfg.FindOneSysCfg(ctx, bson.M{})
+ cdnDomain := webg.Conf.CdnDomain
+ if cfgSys != nil {
+ cdnDomain = cfgSys.MediaWebCdn
+ }
+ origDomain := webg.Conf.OrigDomain
+ _, buff, err := m3u8.TransFormWithSafely(&req, cdnDomain, origDomain, true, webg.Conf.TsAuthKey, webg.Conf.LsjMediaConf.M3u8Yuan, "")
+ if err != nil {
+ return
+ }
+ contentType = "application/vnd.apple.mpegurl"
+ reader = bytes.NewReader(buff.Bytes())
+ contentLength = int64(buff.Len())
+ extraHeaders = map[string]string{}
+ return
+ }
+
+ handlerFile(ctx, fn)
+}
+
+func Enkey(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ buffer, err := m3u8.Enkey(c)
+ if err != nil {
+ return
+ }
+ contentLength = int64(buffer.Len())
+ contentType = "application/octet-stream"
+ reader = buffer
+ extraHeaders = map[string]string{}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+// TranscodeM3u8 转码专用 m3u8 接口(不鉴权)。
+func TranscodeM3u8(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ name := ctx.Param("key")
+ _, buff, err := m3u8.TransformForTranscode(name, webg.Conf.OrigDomain, webg.Conf.CdnDomain, webg.Conf.WebM3u8Host, webg.Conf.LsjMediaConf.M3u8Yuan, webg.Conf.LsjMediaConf.TsYuan)
+ if err != nil {
+ return
+ }
+ contentType = "application/vnd.apple.mpegurl"
+ reader = bytes.NewReader(buff.Bytes())
+ contentLength = int64(buff.Len())
+ extraHeaders = map[string]string{}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+func LsjEnkey(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ buffer, err := m3u8.LsjEnkey(c)
+ if err != nil {
+ return
+ }
+ contentLength = int64(buffer.Len())
+ contentType = "application/octet-stream"
+ reader = buffer
+ extraHeaders = map[string]string{}
+ return
+ }
+ handlerFile(ctx, fn)
+}
+
+func handlerFile(ctx *gin.Context, h fileHandler) {
+ contentLength, contentType, reader, extraHeaders, err := h(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.DownloadM3u8File, err)
+ return
+ }
+ ctx.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
+}
diff --git a/web/api/web_video/taginfo.go b/web/api/web_video/taginfo.go
new file mode 100644
index 0000000..7c75bb4
--- /dev/null
+++ b/web/api/web_video/taginfo.go
@@ -0,0 +1,111 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/web/service/srv_videotag"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 标签管理
+// @Summary 分组分页获取标签管理
+// @Description 分组分页获取标签管理
+// @Accept json
+// @Produce json
+// @Param param body srv_videotag.VideoTagInfoReq true "参数列表"
+// @Success 200 {object} srv_videotag.VideoTagInfoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotag/list [POST]
+func GetVideoTagInfo(ctx *gin.Context) {
+ var req = &srv_videotag.VideoTagInfoReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_videotag.GetVideoTagInfos(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 标签管理
+// @Summary 添加一个标签管理
+// @Description 添加一个标签管理
+// @Accept json
+// @Produce json
+// @Param param body videotagmod.VideoTagInfo true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotag/add [POST]
+func AddOneVideoTagInfo(ctx *gin.Context) {
+ var req = &videotagmod.VideoTagInfo{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videotag.AddVideoTagInfo(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 标签管理
+// @Summary 删除一个标签管理
+// @Description 删除一个标签管理
+// @Accept json
+// @Produce json
+// @Param param body srv_videotag.VideoTagInfoDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotag/del [POST]
+func DelOneVideoTagInfo(ctx *gin.Context) {
+ var req = &srv_videotag.VideoTagInfoDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videotag.DelVideoTagInfo(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 标签管理
+// @Summary 更新一个标签管理
+// @Description 更新一个标签管理
+// @Accept json
+// @Produce json
+// @Param param body srv_videotag.VideoTagInfoUpdate true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotag/update [POST]
+func UpdateOneVideoTagInfo(ctx *gin.Context) {
+ var req = &srv_videotag.VideoTagInfoUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_videotag.UpdateVideoTagInfo(ctx, req.Id, req.Param)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_video/video.go b/web/api/web_video/video.go
new file mode 100644
index 0000000..bd1d0e2
--- /dev/null
+++ b/web/api/web_video/video.go
@@ -0,0 +1,344 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/middleware/auth"
+ "videoweb/web/service/srv_videoinfo"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 视频管理
+// @Summary 分组分页获取视频管理
+// @Description 分组分页获取视频管理
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.VideoInfoReq true "参数列表"
+// @Success 200 {object} srv_videoinfo.VideoInfoResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/list [POST]
+func GetVideoInfo(ctx *gin.Context) {
+ var req = &srv_videoinfo.VideoInfoReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_videoinfo.GetVideoInfos(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetVideoRecommend 单/批量设置视频是否推荐
+// @Tags 视频管理
+// @Summary 单/批量设置视频是否推荐 -- by 王鸣
+// @Description 根据视频ID集合批量更新 video_information 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.SetRecommendReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/recommend/set [post]
+func SetVideoRecommend(ctx *gin.Context) {
+ req := &srv_videoinfo.SetRecommendReq{}
+ if err := ctx.ShouldBindJSON(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ if err := srv_videoinfo.SetVideoRecommend(ctx, req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetVideoFakeLikes 单/批量设置视频假点赞数
+// @Tags 视频管理
+// @Summary 单/批量设置视频假点赞数 --by 旺鸣
+// @Description 根据ids批量随机设置视频fake.likes,随机范围包含最小值和最大值
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.SetFakeLikesReq true "参数列表"
+// @Success 200 {object} srv_videoinfo.SetFakeCountResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/setFakeLikes [post]
+func SetVideoFakeLikes(ctx *gin.Context) {
+ req := &srv_videoinfo.SetFakeLikesReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, err := srv_videoinfo.SetVideoFakeLikes(ctx, req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetVideoFakeWatchTimes 单/批量设置视频假观看次数
+// @Tags 视频管理
+// @Summary 单/批量设置视频假观看次数 --by 旺鸣
+// @Description 根据ids批量随机设置视频fake.watchTimes,随机范围包含最小值和最大值
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.SetFakeWatchTimesReq true "参数列表"
+// @Success 200 {object} srv_videoinfo.SetFakeCountResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/setFakeWatchTimes [post]
+func SetVideoFakeWatchTimes(ctx *gin.Context) {
+ req := &srv_videoinfo.SetFakeWatchTimesReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, err := srv_videoinfo.SetVideoFakeWatchTimes(ctx, req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetVideoHeat 单/批量设置视频热度
+// @Tags 视频管理
+// @Summary 单/批量设置视频热度 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.SetVideoHeatReq true "视频热度设置请求"
+// @Success 200 {object} srv_videoinfo.SetFakeCountResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/heat/set [post]
+func SetVideoHeat(ctx *gin.Context) {
+ req := &srv_videoinfo.SetVideoHeatReq{}
+ if err := ctx.ShouldBindJSON(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, err := srv_videoinfo.SetVideoHeat(ctx, req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// /api/web/videoinfo/fillZeroFakeFlow [post]
+func FillZeroVideoFakeFlow(ctx *gin.Context) {
+ //临时注释,有需要再放开
+ //resp, err := srv_videoinfo.FillZeroVideoFakeFlow(ctx)
+ //if err != nil {
+ // common.ServerJSON(ctx, err.Code, err.Tips)
+ // return
+ //}
+ //common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 视频管理
+// @Summary 从切片列表同步多个视频
+// @Description 从切片列表同步多个视频
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.AddVideoReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/sync [POST]
+func SyncVideoInfo(ctx *gin.Context) {
+ var req = &srv_videoinfo.AddVideoReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videoinfo.SyncVideoInfo(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 视频管理
+// @Summary 从媒资库直同步到视频列表
+// @Description 从媒资库直同步到视频列表,后续切片同步时按 mediaSourceId 覆盖
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.SyncMediaFileReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/syncMediaFile [POST]
+func SyncVideoFromMediaFile(ctx *gin.Context) {
+ var req = &srv_videoinfo.SyncMediaFileReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ result, serr := srv_videoinfo.SyncVideoFromMediaFile(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, result)
+}
+
+// @Tags 视频管理
+// @Summary 删除一个视频管理
+// @Description 删除一个视频管理
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.VideoInfoDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/del [POST]
+func DelOneVideoInfo(ctx *gin.Context) {
+ var req = &srv_videoinfo.VideoInfoDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videoinfo.DelVideoInfo(ctx, req.Id)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 视频管理
+// @Summary 更新一个视频管理
+// @Description 更新一个视频管理
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.VideoInfoUpdateReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/update [POST]
+func UpdateOneVideoInfo(ctx *gin.Context) {
+ var req = &srv_videoinfo.VideoInfoUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.New(srvdef.ErrParamError, err.Error(), err.Error()), nil)
+ return
+ }
+ userId := ctx.GetInt64(auth.AccId)
+ serr := srv_videoinfo.UpdateVideoInfo(ctx, req.Id, userId, req.Param)
+ if serr != nil {
+ common.ServerJSON(ctx, videoUpdateErr(serr), nil)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// videoUpdateErr 把业务错误里的具体原因带到接口提示里。
+func videoUpdateErr(err *srvdef.SrvErr) *srvdef.SrvErr {
+ if err == nil {
+ return nil
+ }
+ if err.Tips == "" && err.Msg != "" && err.Msg != err.Code.Msg() {
+ return srvdef.New(err.Code, err.Msg, err.Msg)
+ }
+ return err
+}
+
+// @Tags 视频管理
+// @Summary 新增一个视频合集
+// @Description 新增一个视频合集
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.AddVideoCollectionReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/addcoll [POST]
+func AddOneVideoCollection(ctx *gin.Context) {
+ var req = &srv_videoinfo.AddVideoCollectionReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// AddVideoTags 批量追加视频标签
+// @Tags 视频管理
+// @Summary 批量追加视频标签 -- by 王鸣
+// @Description 根据视频ID集合批量追加统一标签,已存在的标签不会重复追加
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.VideoAddTagsReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/addTags [post]
+func AddVideoTags(ctx *gin.Context) {
+ req := &srv_videoinfo.VideoAddTagsReq{}
+ if err := ctx.ShouldBindJSON(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ if err := srv_videoinfo.AddVideoTags(ctx, req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetVideoTags 单/批量设置视频标签
+// @Tags 视频管理
+// @Summary 单/批量设置视频标签 -- by 王鸣
+// @Description 根据视频ID集合批量覆盖 video_information 集合 tags 字段,传1个ID表示单条设置,传多个ID表示批量设置,tagIds传空数组表示清空标签
+// @Accept json
+// @Produce json
+// @Param param body srv_videoinfo.VideoSetTagsReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videoinfo/tag/set [post]
+func SetVideoTags(ctx *gin.Context) {
+ req := &srv_videoinfo.VideoSetTagsReq{}
+ if err := ctx.ShouldBindJSON(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ if err := srv_videoinfo.SetVideoTags(ctx, req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+type TranscodeVideoReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+
+// @Tags 视频管理
+// @Summary 提交视频 H265 转码
+// @Router /api/web/videoinfo/transcode [POST]
+func TranscodeVideo(ctx *gin.Context) {
+ var req TranscodeVideoReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if serr := srv_videoinfo.TranscodeVideo(ctx, req.Id); serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, nil)
+}
diff --git a/web/api/web_videocollection/videocollection.go b/web/api/web_videocollection/videocollection.go
new file mode 100644
index 0000000..7881c95
--- /dev/null
+++ b/web/api/web_videocollection/videocollection.go
@@ -0,0 +1,83 @@
+package web_videocollection
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_videocollection"
+)
+
+// @Tags 合集管理
+// @Summary 获取合集视频列表
+// @Description 获取合集视频列表
+// @Accept json
+// @Produce json
+// @Param param body srv_videocollection.GetCollectionMediaListReq true "参数列表"
+// @Success 200 {object} srv_videocollection.GetCollectionMediaListResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocollection/media/list [POST]
+func GetVideoCollectionMediaList(ctx *gin.Context) {
+ var req = &srv_videocollection.GetCollectionMediaListReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_videocollection.GetCollectionMediaList(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// @Tags 合集管理
+// @Summary 修改合集里的视频列表
+// @Description 修改合集里的视频列表
+// @Accept json
+// @Produce json
+// @Param param body srv_videocollection.VideoCollectionOptionMediaReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocollection/media/option [POST]
+func UpdateVideoCollectionMediaList(ctx *gin.Context) {
+ var req = &srv_videocollection.VideoCollectionOptionMediaReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videocollection.VideoCollectionOptionMedia(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 合集管理
+// @Summary 修改合集里的视频排序
+// @Description 修改合集里的视频排序
+// @Accept json
+// @Produce json
+// @Param param body srv_videocollection.UpdateCollectionMediaRankReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videocollection/media/rank [POST]
+func UpdateVideoCollectionMediaRank(ctx *gin.Context) {
+ var req = &srv_videocollection.UpdateCollectionMediaRankReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videocollection.UpdateCollectionMediaRank(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
\ No newline at end of file
diff --git a/web/conf/m3u8.key b/web/conf/m3u8.key
new file mode 100644
index 0000000..6cce2a7
--- /dev/null
+++ b/web/conf/m3u8.key
@@ -0,0 +1 @@
+—�ˆȒšÁC�„
\ No newline at end of file
diff --git a/web/conf/web.yaml b/web/conf/web.yaml
new file mode 100644
index 0000000..ff61555
--- /dev/null
+++ b/web/conf/web.yaml
@@ -0,0 +1,39 @@
+redis:
+ addr: 172.104.181.99:16789
+ db: 3
+ passwd: "B6vJHlYiTseLj77z"
+mongodb:
+ addr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_mdcom?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
+ logaddr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_mdcom_log?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
+es:
+ addr: "http://172.104.181.99:9200"
+ user: "elastic"
+ pass: "admin123456"
+jwtKey: "2311b15a4da1c80dc541b99abb622233"
+tgToken: ""
+runMod: "dev"
+tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
+pprof: false
+host: "172.104.181.99"
+listenAddr: "25121"
+prefetchSubmitUrl: "http://172.104.171.80:9067/master/submit"
+aiCenterAppId: 25222
+aiUndressingCallbackUrl: "https://web_mdcomtest-ht.80sygtm.com/api/web/aiorder/strip/callback"
+upAppId: 8
+swagger:
+ host: "172.104.181.99"
+ user: "mdc"
+ pass: "2@6A1#9e%9c2"
+bucketVideoAddressV2: "http://139.162.22.207:24996"
+bucketVideoAddress: "http://tong3yuants.bbaazz.me"
+
+# 媒资配置
+mediaResourceDBCfg:
+ baseUrl: "https://mediav2-api.htycpc.com"
+ merchat: "91porn"
+ secret: "08f1c3ab5e11e9b6c18aa24c06d1eb77"
+
+# 原创短剧数据回流到 AI 内容中台
+dataCenterBackflow:
+ key: "ld_XLWM9pojLnf_OZFQ1ISac4cELop-WnJH_CcjS5D_ROY"
+ selfSourceBaseUrl: "https://172.104.181.99:25128/api/web/m3u8"
diff --git a/web/deploy_prod.sh b/web/deploy_prod.sh
new file mode 100755
index 0000000..e518b5c
--- /dev/null
+++ b/web/deploy_prod.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+echo "开始"
+rm -rf ./web_hghl_web ./web_hghl_web.tar.gz
+GOOS=linux GOARCH=amd64 go build -o web_hghl_web
+tar zvcf web_hghl_web.tar.gz web_hghl_web
+
+
+scp web_hghl_web.tar.gz root@172.104.166.87:/data/uzip
+ssh root@172.104.166.87 "/server/web_hghl/web/up_app.sh"
+
+rm -rf ./web_hghl_web ./web_hghl_web.tar.gz
\ No newline at end of file
diff --git a/web/docs/docs.go b/web/docs/docs.go
new file mode 100644
index 0000000..c6b096d
--- /dev/null
+++ b/web/docs/docs.go
@@ -0,0 +1,13396 @@
+// Package swag_web_docs Code generated by swaggo/swag. DO NOT EDIT
+package swag_web_docs
+
+import "github.com/swaggo/swag"
+
+const docTemplate = `{
+ "schemes": {{ marshal .Schemes }},
+ "swagger": "2.0",
+ "info": {
+ "description": "{{escape .Description}}",
+ "title": "{{.Title}}",
+ "contact": {},
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "{{.Version}}"
+ },
+ "host": "{{.Host}}",
+ "basePath": "{{.BasePath}}",
+ "paths": {
+ "/api/web/acc/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-添加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-添加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccAddResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccDelResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/getinfo": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-获取信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-获取信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/getstatus": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-获取状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-获取状态",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetStatusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetStatusResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/ops": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-获取操作记录",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-获取操作记录",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.AccOpLogReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.AccOpLogResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/savestatus": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-保存状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-保存状态",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccSaveStatusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccSaveStatusResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/up": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/uppwd": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-更新密码",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-更新密码",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpPwdReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpPwdReq"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "添加一个女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "删除一个女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "分组分页获取女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "更新一个女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/del": {
+ "post": {
+ "description": "删除一个联系我们",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们"
+ ],
+ "summary": "删除一个联系我们",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.DelOneContactusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/list": {
+ "post": {
+ "description": "联系我们列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们"
+ ],
+ "summary": "联系我们列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.GetContactusListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.GetContactusListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改联系我们状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们"
+ ],
+ "summary": "修改联系我们状态",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.UpdateContactusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "新增统一标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增统一标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "删除统一标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除统一标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询统一标签配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "统一标签列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "状态 true-启用 false-停用",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 1,
+ 5
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ],
+ "description": "内容类型 1-视频 5-短剧",
+ "name": "type",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/option": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "查询启用的统一标签下拉列表,传type时按支持类型过滤",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "统一标签下拉列表 -- by 王鸣",
+ "parameters": [
+ {
+ "enum": [
+ 1,
+ 5
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ],
+ "description": "内容类型 1-视频 5-短剧,不传返回全部启用标签",
+ "name": "type",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagOptionResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "更新统一标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新统一标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-增加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-增加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.AddDetailNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.DelDetailNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.GetDetailNavListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.GetDetailNavListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.UpdateDetailNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/file/uploadImg": {
+ "post": {
+ "description": "图片上传",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "文件上传"
+ ],
+ "summary": "图片上传",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "文件",
+ "name": "file",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_file.UploadImageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/file/uploadImgs": {
+ "post": {
+ "description": "图片批量上传",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "文件上传"
+ ],
+ "summary": "图片批量上传",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "文件",
+ "name": "file[]",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_file.UploadImagesResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "新增首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "删除首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询首页banner配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "首页banner列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "首页 banner ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量,默认10,最大100",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "状态 true-启用 false-停用",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "更新首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-增加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-增加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.AddHotNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.DelHotNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.GetHotNavListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.GetHotNavListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.UpdateHotNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/ipblack/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "IP黑名单-增加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP黑名单"
+ ],
+ "summary": "IP黑名单-增加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.AddIPBlackListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/ipblack/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "IP黑名单-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP黑名单"
+ ],
+ "summary": "IP黑名单-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.DelIpBlackListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/ipblack/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "IP黑名单-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP黑名单"
+ ],
+ "summary": "IP黑名单-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.GetIPBlackListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.GetIPBlackListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/login/acc": {
+ "post": {
+ "description": "登录-登录",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "登录"
+ ],
+ "summary": "登录-登录",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MainWebLoginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MainWebLoginResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/login/bind": {
+ "post": {
+ "description": "登录-绑定二维码",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "登录"
+ ],
+ "summary": "登录-绑定二维码",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.BindQurlReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.BindQurlResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/login/slogin": {
+ "post": {
+ "description": "管理员sso登陆",
+ "consumes": [
+ "multipart/form-data",
+ "application/json"
+ ],
+ "produces": [
+ "application/json",
+ "text/html"
+ ],
+ "tags": [
+ "web-单点登陆"
+ ],
+ "summary": "管理员sso登陆",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "账号",
+ "name": "name",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "密码",
+ "name": "password",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "{\"msg\": \"管理员信息\"\"}",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "{\"msg\": \"操作失败\"}",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "新增 playlet 集合短剧数据,支持分类、题材、标签、支付方式、推荐和 banner 配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增短剧请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/banner/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 showOnBanner 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧是否显示在首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧是否显示在首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletBannerSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端删除短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧分类列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "分类ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "启用状态",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 categoryIds 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategorySetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "新增 playlet_chapter 集合短剧剧集数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧剧集 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据剧集ID集合删除 playlet_chapter 记录,仅原创短剧支持删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量删除短剧剧集",
+ "parameters": [
+ {
+ "description": "单/批量删除短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询 playlet_chapter 集合短剧剧集数据,默认按集数升序返回",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧剧集列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "剧集ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusFailed": "处理失败",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone",
+ "WatermarkStatusFailed"
+ ],
+ "description": "水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败",
+ "name": "watermarkStatus",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/pay-type/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据剧集ID集合批量更新 playlet_chapter 集合 payType 和 singleCoins 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧剧集支付方式 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧剧集支付方式请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterPayTypeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/sync": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合从媒资中心同步新增剧集,playletIds传1个表示单条同步,传多个表示批量同步,允许已完结短剧补集或修正状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端手动单/批量同步短剧剧集",
+ "parameters": [
+ {
+ "description": "手动单/批量同步短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterSyncReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterSyncResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按剧集ID编辑 playlet_chapter 集合短剧剧集数据,只更新请求中传入的字段",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端编辑短剧剧集 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "编辑短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/featured/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 isFeatured 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧是否爆剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧是否爆剧请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletFeaturedSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/like/virtual/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 virtualCountLike 字段,每条短剧从最小值和最大值之间随机取不重复数值入库",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧虚拟点赞数 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量随机设置短剧虚拟点赞数请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletVirtualLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "分类ID",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集(对应 media_type=6)",
+ "ContentTypeAnime": "动漫(对应 media_type=1)",
+ "ContentTypeComic": "漫画短剧(对应 media_type=4)",
+ "ContentTypeManga": "漫画(对应 media_type=2)",
+ "ContentTypeTheater": "短剧(对应 media_type=5)"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ],
+ "description": "内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画",
+ "name": "contentType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否爆剧,true-是、false-否",
+ "name": "isFeatured",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否原创:true只查原创,false只查非原创,不传查全部",
+ "name": "isOriginal",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "name": "isRecommend",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "最后操作人名称,支持模糊查询",
+ "name": "operatorName",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ],
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "name": "payType",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否显示在首页banner,true-显示、false-不显示",
+ "name": "showOnBanner",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ],
+ "description": "上架状态,0-下架、1-上架",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "tagId",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "题材ID",
+ "name": "themeId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧标题,支持模糊查询",
+ "name": "title",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/pay-type/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 payType/singleCoins 字段",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧默认支付方式 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧默认支付方式请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletPayTypeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/recommend/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧是否推荐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧是否推荐请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletRecommendSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/status/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 status 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧上下架 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧上下架请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletStatusSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端删除标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧标签",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端标签列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 tagIds 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端删除题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧题材",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端题材列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "题材ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "题材名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "上架状态",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按短剧ID编辑 playlet 集合短剧数据,只更新请求中传入的字段",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端编辑短剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "编辑短剧请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/watermark/reset": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合重置剧集水印状态,保留已有水印地址以便换水印期间继续观看",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端按短剧ID批量重置剧集水印 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "批量重置短剧水印请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletWatermarkResetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/del": {
+ "post": {
+ "description": "删除一个评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "删除一个评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_commend.CommendDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/list": {
+ "post": {
+ "description": "评论列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "评论列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendSearch"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendSearchResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/replay": {
+ "post": {
+ "description": "回复评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "回复评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendReplay"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/review": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "审核评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "审核评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendReviewReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/reviewBatch": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "批量审核评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "批量审核评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_commend.CommendReviewBatchReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/user/del": {
+ "post": {
+ "description": "删除一个用户所有评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "删除一个用户所有评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_commend.CommendDelIpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/questionnaire/handle": {
+ "post": {
+ "description": "只有状态为\"待处理\"的问卷才允许更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "问卷调查"
+ ],
+ "summary": "处理问卷",
+ "parameters": [
+ {
+ "description": "处理参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_questionnaire.HandleReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/questionnaire/list": {
+ "post": {
+ "description": "支持分页及条件筛选(question_type、status、时间范围)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "问卷调查"
+ ],
+ "summary": "查询问卷列表",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_questionnaire.ListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_questionnaire.ListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/resource/chapters": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按媒资ID查询剧集信息,复用入库时的 mediaSync 接口",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web媒资库相关接口"
+ ],
+ "summary": "web端短剧媒资剧集列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "媒资中心资源ID",
+ "name": "id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletResourceChapterListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/resource/import": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "提交媒资入库任务,后台从媒资中心同步短剧详情并写入短剧和章节集合",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web媒资库相关接口"
+ ],
+ "summary": "web端短剧媒资入库 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧媒资入库请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletResourceUseReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/resource/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧媒资列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web媒资库相关接口"
+ ],
+ "summary": "web端短剧媒资列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "媒体类型 1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧",
+ "name": "media_type",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码",
+ "name": "pageNumber",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "个数",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "同步类型 1-全量同步 2-增量同步",
+ "name": "sync_type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签",
+ "name": "tag",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标题",
+ "name": "title",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletResourceListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "添加一个富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/richtextcontentmod.RichTextContentModel"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "删除一个富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.DelRichTextContentReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "分组分页获取富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.GetRichTextContentListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.GetRichTextContentResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "更新一个富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.UpdateRichTextContentReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-添加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-添加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleAddResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleDelResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/drop": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-下拉列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-下拉列表",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleDropResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/enable": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-使能",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-使能",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleEnableReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleEnableResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/getauth": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-获取权限",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-获取权限",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetAuthReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetAuthResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/getinfo": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-获取信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-获取信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/pubdef.Page"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/up": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/upauth": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-更新权限",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-更新权限",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpAuthReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpAuthResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/slug/sync": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "为视频、分类、女优、作者、短剧、统一标签等空 slug 按 SEO 关键词或名称自动生成。可重复调用,hasMore 为 true 时继续请求。type 为空处理全部,也可指定 video/videoCategory/actress/videoCreater/playlet/playletCategory/playletTheme/contentTag。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web slug相关接口"
+ ],
+ "summary": "同步历史空 slug",
+ "parameters": [
+ {
+ "description": "同步参数",
+ "name": "param",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/proto.SlugSyncReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.SlugSyncResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/stat/analytics": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "统计数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "获取统计数据"
+ ],
+ "summary": "统计数据",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/stat/daily/analytics": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "统计数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "获取统计数据"
+ ],
+ "summary": "统计数据",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/syscfg/get": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "获取系统配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "系统配置"
+ ],
+ "summary": "获取系统配置",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_cfg.GetSysConfigReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_cfg.GetSysConfigResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/syscfg/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改系统配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "系统配置"
+ ],
+ "summary": "修改系统配置",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_cfg.SysConfigUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_cfg.SysConfigUpdateResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "添加一个导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/translationmod.TranslationInfo"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "删除一个导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "分组分页获取导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "更新一个导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "添加一个导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/translationcategorymod.TranslationCategory"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "删除一个导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "分组分页获取导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "更新一个导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/user/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "查询用户列表",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.UserListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_user.UserListResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/user/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "更新用户信息",
+ "parameters": [
+ {
+ "description": "更新参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.UserUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "添加一个视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videocategorymod.VideoCategory"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "删除一个视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "分组分页获取视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "更新一个视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryUpdate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocollection/media/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "获取合集视频列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合集管理"
+ ],
+ "summary": "获取合集视频列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.GetCollectionMediaListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.GetCollectionMediaListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocollection/media/option": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改合集里的视频列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合集管理"
+ ],
+ "summary": "修改合集里的视频列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.VideoCollectionOptionMediaReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocollection/media/rank": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改合集里的视频排序",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合集管理"
+ ],
+ "summary": "修改合集里的视频排序",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.UpdateCollectionMediaRankReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "添加一个视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videocreatermod.VideoCreaterInfo"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "删除一个视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "分组分页获取视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "更新一个视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterUpdate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/addTags": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据视频ID集合批量追加统一标签,已存在的标签不会重复追加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "批量追加视频标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoAddTagsReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/addcoll": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "新增一个视频合集",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "新增一个视频合集",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.AddVideoCollectionReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个视频管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "删除一个视频管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取视频管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "分组分页获取视频管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/recommend/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据视频ID集合批量更新 video_information 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频是否推荐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetRecommendReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/setFakeLikes": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据ids批量随机设置视频fake.likes,随机范围包含最小值和最大值",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频假点赞数 --by 旺鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeLikesReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeCountResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/setFakeWatchTimes": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据ids批量随机设置视频fake.watchTimes,随机范围包含最小值和最大值",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频假观看次数 --by 旺鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeWatchTimesReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeCountResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/sync": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "从切片列表同步多个视频",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "从切片列表同步多个视频",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.AddVideoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/syncMediaFile": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "从媒资库直同步到视频列表,后续切片同步时按 mediaSourceId 覆盖",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "从媒资库直同步到视频列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SyncMediaFileReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/tag/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据视频ID集合批量覆盖 video_information 集合 tags 字段,传1个ID表示单条设置,传多个ID表示批量设置,tagIds传空数组表示清空标签",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoSetTagsReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/transcode": {
+ "post": {
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "提交视频 H265 转码",
+ "responses": {}
+ }
+ },
+ "/api/web/videoinfo/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个视频管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "更新一个视频管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "添加一个标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videotagmod.VideoTagInfo"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "删除一个标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "分组分页获取标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "更新一个标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoUpdate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/wbapi/hjsq/hottag": {
+ "get": {
+ "description": "根据视频累计播放量聚合返回热门标签列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "查询播放量最多的标签",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "返回条数,默认10,最大1000",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.HotTagResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/wbapi/hjsq/ipstat": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP统计"
+ ],
+ "summary": "查询今日和昨日去重访客IP数",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.IpStatReq"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "actress.Actress": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "real": {
+ "description": "真实流量",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.Flow"
+ }
+ ]
+ },
+ "realUpdateAt": {
+ "description": "播放相关数据更新时间",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "actress.Meas": {
+ "type": "object",
+ "properties": {
+ "blood": {
+ "description": "血型",
+ "type": "string"
+ },
+ "bust": {
+ "description": "胸围 英寸",
+ "type": "integer"
+ },
+ "cup": {
+ "description": "罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I",
+ "type": "integer"
+ },
+ "dia": {
+ "description": "直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)",
+ "type": "integer"
+ },
+ "gender": {
+ "description": "性别 0:女 1:男 2:其他",
+ "type": "integer"
+ },
+ "height": {
+ "description": "身高 厘米",
+ "type": "integer"
+ },
+ "hip": {
+ "description": "臀围 英寸",
+ "type": "integer"
+ },
+ "len": {
+ "description": "长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)",
+ "type": "integer"
+ },
+ "waist": {
+ "description": "腰围 英寸",
+ "type": "integer"
+ },
+ "weight": {
+ "description": "体重 公斤",
+ "type": "integer"
+ }
+ }
+ },
+ "contactusmod.ContactUsModel": {
+ "type": "object",
+ "properties": {
+ "categoryName": {
+ "description": "主旨分类",
+ "type": "string"
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮件",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "string"
+ },
+ "ip": {
+ "description": "评论IP",
+ "type": "string"
+ },
+ "name": {
+ "description": "名字",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0=初始 1=已回复 2=不予处理 3=已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "contenttagmod.ContentTagType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 5
+ ],
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ]
+ },
+ "detailnavmod.DetailNavModel": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "url": {
+ "description": "导航链接",
+ "type": "string"
+ }
+ }
+ },
+ "hotnavmod.HotNav": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "icon": {
+ "description": "图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "介绍",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ }
+ }
+ },
+ "ipblackmod.IPBlackModel": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "备注",
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "ip": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "playletchaptermod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletchaptermod.WatermarkStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusFailed": "处理失败",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone",
+ "WatermarkStatusFailed"
+ ]
+ },
+ "playletmod.ContentType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集(对应 media_type=6)",
+ "ContentTypeAnime": "动漫(对应 media_type=1)",
+ "ContentTypeComic": "漫画短剧(对应 media_type=4)",
+ "ContentTypeManga": "漫画(对应 media_type=2)",
+ "ContentTypeTheater": "短剧(对应 media_type=5)"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ]
+ },
+ "playletmod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletmod.Status": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ]
+ },
+ "proto.ContentTagAddReq": {
+ "type": "object",
+ "required": [
+ "name",
+ "status",
+ "types"
+ ],
+ "properties": {
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或名称自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ }
+ }
+ },
+ "proto.ContentTagAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContentTagDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContentTagListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContentTagOptionItem": {
+ "type": "object",
+ "properties": {
+ "bindCount": {
+ "description": "当前查询类型的绑定数量,不传type时返回0",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "typeNames": {
+ "description": "支持类型名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ }
+ }
+ },
+ "proto.ContentTagOptionResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "标签下拉列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentTagOptionItem"
+ }
+ }
+ }
+ },
+ "proto.ContentTagUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ }
+ }
+ },
+ "proto.HomeBannerAddReq": {
+ "type": "object",
+ "required": [
+ "image",
+ "name",
+ "status"
+ ],
+ "properties": {
+ "image": {
+ "description": "图片",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "URL",
+ "type": "string"
+ },
+ "verticalImage": {
+ "description": "竖版图片",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HomeBannerAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "image": {
+ "description": "图片",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "URL",
+ "type": "string"
+ },
+ "verticalImage": {
+ "description": "竖版图片",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletAddReq": {
+ "type": "object",
+ "required": [
+ "title"
+ ],
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图",
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "contentType": {
+ "description": "内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "cover": {
+ "description": "封面原图",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "isFeatured": {
+ "description": "是否爆剧,true-是、false-否",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告",
+ "type": "string"
+ },
+ "recommendRank": {
+ "description": "推荐排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或标题自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态,0-下架、1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "topRank": {
+ "description": "置顶排序",
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletBannerSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner,true-显示、false-不显示",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletCategoryAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "icon": {
+ "description": "分类图标",
+ "type": "string"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或名称自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "启用状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletCategoryAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCategoryDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCategoryListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCategorySetReq": {
+ "type": "object",
+ "properties": {
+ "categoryIds": {
+ "description": "分类ID集合,可设置多个分类,传空数组表示清空分类,不传表示参数错误",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletCategoryUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "icon": {
+ "description": "分类图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "启用状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterAddReq": {
+ "type": "object",
+ "required": [
+ "currentEpisode",
+ "playletId"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "视频高度",
+ "type": "integer"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaSourceId": {
+ "description": "media切片服原片ID,上传正片已创建原片时直接沿用",
+ "type": "string"
+ },
+ "mediaTime": {
+ "description": "影片长度,单位秒",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "title": {
+ "description": "剧集标题",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "原片视频地址",
+ "type": "string"
+ },
+ "width": {
+ "description": "视频宽度",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "剧集ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterDelReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "剧集ID集合,传1个表示单条删除,传多个表示批量删除",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletChapterListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterPayTypeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "剧集ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时必填且需大于0",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterSyncReq": {
+ "type": "object",
+ "required": [
+ "playletIds"
+ ],
+ "properties": {
+ "playletIds": {
+ "description": "短剧ID集合,传1个表示单条同步,传多个表示批量同步",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletChapterSyncResp": {
+ "type": "object",
+ "properties": {
+ "failedCount": {
+ "description": "失败数量",
+ "type": "integer"
+ },
+ "list": {
+ "description": "同步结果集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterSyncResult"
+ }
+ },
+ "successCount": {
+ "description": "成功数量",
+ "type": "integer"
+ },
+ "total": {
+ "description": "同步总数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterSyncResult": {
+ "type": "object",
+ "properties": {
+ "newChapterCount": {
+ "description": "本次新增剧集数量",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "reason": {
+ "description": "失败原因",
+ "type": "string"
+ },
+ "success": {
+ "description": "是否同步成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "视频高度",
+ "type": "integer"
+ },
+ "id": {
+ "description": "剧集ID",
+ "type": "integer"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度,单位秒",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "title": {
+ "description": "剧集标题",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "原片视频地址",
+ "type": "string"
+ },
+ "width": {
+ "description": "视频宽度",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletFeaturedSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isFeatured": {
+ "description": "是否爆剧,true-是、false-否",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletPayTypeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "payType": {
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时必填且需大于0",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRecommendSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isRecommend": {
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletResourceChapterItem": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createTime": {
+ "description": "创建时间",
+ "type": "integer"
+ },
+ "episodeNumber": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hashId": {
+ "description": "资源hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "媒资内容ID",
+ "type": "integer"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "playTime": {
+ "description": "播放时长(秒)",
+ "type": "integer"
+ },
+ "saleCoin": {
+ "description": "单集售价",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "updateTime": {
+ "description": "更新时间",
+ "type": "integer"
+ },
+ "videoUrl": {
+ "description": "播放地址",
+ "type": "string"
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceChapterListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "剧集列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletResourceChapterItem"
+ }
+ },
+ "mediaId": {
+ "description": "媒资中心资源ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "短剧标题",
+ "type": "string"
+ },
+ "total": {
+ "description": "剧集数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceListItem": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者",
+ "type": "string"
+ },
+ "create_time": {
+ "description": "创建时间",
+ "type": "integer"
+ },
+ "data_version": {
+ "description": "数据版本",
+ "type": "integer"
+ },
+ "horizontal_cover": {
+ "description": "横版封面",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isAdd": {
+ "description": "是否已加入后台数据",
+ "type": "boolean"
+ },
+ "media_type": {
+ "description": "媒体类型",
+ "type": "integer"
+ },
+ "sale_coin": {
+ "description": "整部售价",
+ "type": "integer"
+ },
+ "summary": {
+ "description": "简介",
+ "type": "string"
+ },
+ "tags_text": {
+ "description": "标签文字",
+ "type": "string"
+ },
+ "terminal_cdo": {
+ "description": "上传者",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "total_episode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "update_status": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "update_time": {
+ "description": "修改时间",
+ "type": "integer"
+ },
+ "vertical_cover": {
+ "description": "竖版封面",
+ "type": "string"
+ },
+ "vlogger_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletResourceListItem"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceUseReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "媒资中心资源ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletStatusSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "status": {
+ "description": "上架状态,0-下架、1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "proto.PlayletTagAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletTagAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagIds": {
+ "description": "标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletTagUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "icon": {
+ "description": "题材图标",
+ "type": "string"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或名称自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合,可设置多个题材",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletThemeUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "icon": {
+ "description": "题材图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图",
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类ID集合,传空数组表示清空分类",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "contentType": {
+ "description": "内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "cover": {
+ "description": "封面原图",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧,true-是、false-否",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告",
+ "type": "string"
+ },
+ "recommendRank": {
+ "description": "推荐排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态,0-下架、1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合,传空数组表示清空标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "topRank": {
+ "description": "置顶排序",
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletVirtualLikeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "maxVirtualCountLike": {
+ "description": "虚拟点赞数最大值,必须大于等于最小值",
+ "type": "integer"
+ },
+ "minVirtualCountLike": {
+ "description": "虚拟点赞数最小值,必须大于等于0",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletWatermarkResetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条,传多个表示批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.SlugSyncReq": {
+ "type": "object",
+ "properties": {
+ "maxSize": {
+ "description": "本次最多处理的空 slug 记录数,默认500,最大2000",
+ "type": "integer"
+ },
+ "type": {
+ "description": "可选,指定类型;空则按顺序处理全部",
+ "type": "string"
+ }
+ }
+ },
+ "proto.SlugSyncResp": {
+ "type": "object",
+ "properties": {
+ "actressUpdated": {
+ "description": "本次补齐的女优数",
+ "type": "integer"
+ },
+ "contentTagUpdated": {
+ "description": "本次补齐的统一标签数",
+ "type": "integer"
+ },
+ "hasMore": {
+ "description": "是否还有未处理的历史数据",
+ "type": "boolean"
+ },
+ "playletCategoryUpdated": {
+ "description": "本次补齐的短剧分类数",
+ "type": "integer"
+ },
+ "playletThemeUpdated": {
+ "description": "本次补齐的短剧主题数",
+ "type": "integer"
+ },
+ "playletUpdated": {
+ "description": "本次补齐的短剧数",
+ "type": "integer"
+ },
+ "totalUpdated": {
+ "description": "本次合计补齐数",
+ "type": "integer"
+ },
+ "videoCategoryUpdated": {
+ "description": "本次补齐的视频分类数",
+ "type": "integer"
+ },
+ "videoCreaterUpdated": {
+ "description": "本次补齐的作者数",
+ "type": "integer"
+ },
+ "videoUpdated": {
+ "description": "本次补齐的视频数",
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.CategoryDisplayPosition": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "CategoryDisplayPositionBoth": "TAB和首页都显示",
+ "CategoryDisplayPositionHomeOnly": "仅首页",
+ "CategoryDisplayPositionTabOnly": "仅TAB"
+ },
+ "x-enum-varnames": [
+ "CategoryDisplayPositionTabOnly",
+ "CategoryDisplayPositionHomeOnly",
+ "CategoryDisplayPositionBoth"
+ ]
+ },
+ "pubdef.CategorySortType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "x-enum-comments": {
+ "CategorySortTypeAddedTimeAsc": "上架时间升序",
+ "CategorySortTypeAddedTimeDesc": "上架时间降序",
+ "CategorySortTypeLikeAsc": "点赞(收藏)次数升序",
+ "CategorySortTypeLikeDesc": "点赞(收藏)次数降序",
+ "CategorySortTypeSortType": "自定义排序",
+ "CategorySortTypeUpdateTimeAsc": "更新时间升序",
+ "CategorySortTypeUpdateTimeDesc": "更新时间降序",
+ "CategorySortTypeWatchAsc": "观看次数升序",
+ "CategorySortTypeWatchDesc": "观看次数降序"
+ },
+ "x-enum-varnames": [
+ "CategorySortTypeAddedTimeDesc",
+ "CategorySortTypeWatchDesc",
+ "CategorySortTypeLikeDesc",
+ "CategorySortTypeAddedTimeAsc",
+ "CategorySortTypeWatchAsc",
+ "CategorySortTypeLikeAsc",
+ "CategorySortTypeUpdateTimeDesc",
+ "CategorySortTypeUpdateTimeAsc",
+ "CategorySortTypeSortType"
+ ]
+ },
+ "pubdef.CommentObjectType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 4
+ ],
+ "x-enum-comments": {
+ "CommentObjectTypePlaylet": "短剧",
+ "CommentObjectTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "CommentObjectTypeVideo",
+ "CommentObjectTypePlaylet"
+ ]
+ },
+ "pubdef.DisplayMode": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "DisplayModeAvatarList": "头像列表",
+ "DisplayModeBigCover": "大封面列表(一大图两列)",
+ "DisplayModeCardCarousel": "卡片层叠轮播",
+ "DisplayModeDefault": "默认(上图下标题多列)",
+ "DisplayModeImageText": "图文列表(左图右标题多列)"
+ },
+ "x-enum-varnames": [
+ "DisplayModeDefault",
+ "DisplayModeCardCarousel",
+ "DisplayModeAvatarList",
+ "DisplayModeImageText",
+ "DisplayModeBigCover"
+ ]
+ },
+ "pubdef.FakeFlow": {
+ "type": "object",
+ "properties": {
+ "likes": {
+ "description": "假点赞数",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "假观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.Flow": {
+ "type": "object",
+ "properties": {
+ "comments": {
+ "description": "评论条数",
+ "type": "integer"
+ },
+ "likes": {
+ "description": "真实点赞数(收藏总量)",
+ "type": "integer"
+ },
+ "monthLikes": {
+ "description": "近30日收藏量",
+ "type": "integer"
+ },
+ "monthWatchs": {
+ "description": "本月观看次数",
+ "type": "integer"
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "todayWatchs": {
+ "description": "当日观看次数",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "真实观看次数",
+ "type": "integer"
+ },
+ "weekLikes": {
+ "description": "近7日收藏量",
+ "type": "integer"
+ },
+ "weekWatchs": {
+ "description": "本周观看次数",
+ "type": "integer"
+ },
+ "yearLikes": {
+ "description": "近365日收藏量",
+ "type": "integer"
+ },
+ "yearWatchs": {
+ "description": "本年观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.InformationStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "InformationStatusVerifyCancel": "下架",
+ "InformationStatusVerifyOK": "上架",
+ "InformationStatusWaiting": "初始化"
+ },
+ "x-enum-varnames": [
+ "InformationStatusWaiting",
+ "InformationStatusVerifyOK",
+ "InformationStatusVerifyCancel"
+ ]
+ },
+ "pubdef.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeMobile": "手机登陆",
+ "LoginTypeQrCode": "二维码登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeMobile",
+ "LoginTypeQrCode",
+ "LoginTypeAccount"
+ ]
+ },
+ "pubdef.Page": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.SeoValue": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "网站SEO标题",
+ "type": "string"
+ }
+ }
+ },
+ "pubdef.TranslationType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "TranslationTypeBottom": "底部导航",
+ "TranslationTypeSub": "二级导航",
+ "TranslationTypeUrl": "URL模式"
+ },
+ "x-enum-varnames": [
+ "TranslationTypeUrl",
+ "TranslationTypeSub",
+ "TranslationTypeBottom"
+ ]
+ },
+ "pubdef.UserStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "UserStatusDisable": "封禁",
+ "UserStatusNormal": "正常",
+ "UserStatusSlient": "禁言"
+ },
+ "x-enum-varnames": [
+ "UserStatusNormal",
+ "UserStatusDisable",
+ "UserStatusSlient"
+ ]
+ },
+ "questionnairemod.Attachment": {
+ "type": "object",
+ "properties": {
+ "fileType": {
+ "description": "附件类型(img / video)",
+ "type": "string"
+ },
+ "url": {
+ "description": "附件URL地址",
+ "type": "string"
+ }
+ }
+ },
+ "questionnairemod.QuestionnaireModel": {
+ "type": "object",
+ "properties": {
+ "attachments": {
+ "description": "附件列表,最多3个",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/questionnairemod.Attachment"
+ }
+ },
+ "city": {
+ "description": "所在城市(选填)",
+ "type": "string"
+ },
+ "contact": {
+ "description": "联系方式(选填)",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间(自动生成)",
+ "type": "string"
+ },
+ "description": {
+ "description": "问题描述(必填)",
+ "type": "string"
+ },
+ "deviceModel": {
+ "description": "设备型号(选填)",
+ "type": "string"
+ },
+ "deviceType": {
+ "description": "设备类型:iPhone、Android、iPad、其他",
+ "type": "string"
+ },
+ "frequency": {
+ "description": "出现频率:每次都会、经常出现、偶尔出现、仅现一次",
+ "type": "string"
+ },
+ "id": {
+ "description": "唯一标识(MongoDB自动生成)",
+ "type": "string"
+ },
+ "networkType": {
+ "description": "网络类型:WI-FI、5G、4G、其他",
+ "type": "string"
+ },
+ "operator": {
+ "description": "运营商:电信、联通、移动、广电",
+ "type": "string"
+ },
+ "province": {
+ "description": "所在省份(必填)",
+ "type": "string"
+ },
+ "questionType": {
+ "description": "问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题",
+ "type": "string"
+ },
+ "remark": {
+ "description": "备注,处理问卷时填写(选填)",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态:待处理、已处理、拒绝",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间(自动更新)",
+ "type": "string"
+ },
+ "userId": {
+ "description": "App端登录用户ID,0表示匿名",
+ "type": "integer"
+ }
+ }
+ },
+ "richtextcontentmod.RichTextContentModel": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序值",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题(内部)",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "srv_acc.OpLogInfo": {
+ "type": "object",
+ "properties": {
+ "actId": {
+ "description": "操作人Id",
+ "type": "integer"
+ },
+ "actName": {
+ "description": "操作人",
+ "type": "string"
+ },
+ "action": {
+ "description": "操作目的",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "操作时间",
+ "type": "string"
+ },
+ "param": {
+ "description": "操作内容",
+ "type": "string"
+ },
+ "path": {
+ "description": "请求接口",
+ "type": "string"
+ }
+ }
+ },
+ "srv_actress.ActressAddReq": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或别名自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressDelReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressListReq": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "sortType": {
+ "description": "排序方式 0=创建时间 1=评分 2=排序值 3=作品数 4=总观看次数 5=热度值",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/actress.Actress"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressUpdateParam": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "srv_actress.ActressUpdateReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "param"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "$ref": "#/definitions/srv_actress.ActressUpdateParam"
+ }
+ }
+ },
+ "srv_commend.CommendReplay": {
+ "type": "object",
+ "properties": {
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "score": {
+ "description": "评分",
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "srv_commend.CommendReviewReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "reviewStatus": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_commend.CommendSearch": {
+ "type": "object",
+ "properties": {
+ "level": {
+ "description": "评论级别",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 0=视频 4=短剧",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CommentObjectType"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "reviewStatus": {
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "srv_commend.CommendSearchResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_commend.PostCommentInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_commend.PostCommentInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "ip": {
+ "description": "评论IP",
+ "type": "string"
+ },
+ "level": {
+ "description": "评论级别",
+ "type": "integer"
+ },
+ "likes": {
+ "description": "点赞数",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectName": {
+ "description": "对象类型名称 视频/短剧",
+ "type": "string"
+ },
+ "objectType": {
+ "description": "对象类型 0=视频 4=短剧",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CommentObjectType"
+ }
+ ]
+ },
+ "parentsId": {
+ "description": "上级评论ID",
+ "type": "integer"
+ },
+ "replyUserName": {
+ "description": "回复名称",
+ "type": "string"
+ },
+ "reviewStatus": {
+ "description": "审核状态 0 未审核 1审核成功 2 审核失败",
+ "type": "integer"
+ },
+ "status": {
+ "description": "评论用户的状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "topRank": {
+ "description": "置顶排序",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "srv_contactus.DelOneContactusReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_contactus.GetContactusListReq": {
+ "type": "object",
+ "properties": {
+ "categoryName": {
+ "description": "分类",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "ip": {
+ "description": "ip",
+ "type": "string"
+ },
+ "name": {
+ "description": "姓名",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_contactus.GetContactusListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contactusmod.ContactUsModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_contactus.UpdateContactusReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_detailnav.AddDetailNavReq": {
+ "type": "object",
+ "required": [
+ "name",
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "导航链接",
+ "type": "string"
+ }
+ }
+ },
+ "srv_detailnav.DelDetailNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_detailnav.GetDetailNavListReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_detailnav.GetDetailNavListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detailnavmod.DetailNavModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_detailnav.UpdateDetailNavParam": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "sort": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ },
+ "url": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_detailnav.UpdateDetailNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "param": {
+ "$ref": "#/definitions/srv_detailnav.UpdateDetailNavParam"
+ }
+ }
+ },
+ "srv_hotnav.AddHotNavReq": {
+ "type": "object",
+ "required": [
+ "title",
+ "url"
+ ],
+ "properties": {
+ "icon": {
+ "description": "图标",
+ "type": "string"
+ },
+ "intro": {
+ "description": "介绍",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ }
+ }
+ },
+ "srv_hotnav.DelHotNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_hotnav.GetHotNavListReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_hotnav.GetHotNavListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/hotnavmod.HotNav"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_hotnav.UpdateHotNavParam": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "type": "string"
+ },
+ "intro": {
+ "type": "string"
+ },
+ "sort": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_hotnav.UpdateHotNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "$ref": "#/definitions/srv_hotnav.UpdateHotNavParam"
+ }
+ }
+ },
+ "srv_ipblack.AddIPBlackListReq": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "备注",
+ "type": "string"
+ },
+ "ip": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_ipblack.DelIpBlackListReq": {
+ "type": "object",
+ "properties": {
+ "ip": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_ipblack.GetIPBlackListReq": {
+ "type": "object",
+ "properties": {
+ "ip": {
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_ipblack.GetIPBlackListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ipblackmod.IPBlackModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_questionnaire.HandleReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "description": "问卷ID(ObjectID 字符串)",
+ "type": "string"
+ },
+ "remark": {
+ "description": "备注(选填)",
+ "type": "string"
+ },
+ "status": {
+ "description": "目标状态:processed 已处理 / reject 拒绝",
+ "type": "string"
+ }
+ }
+ },
+ "srv_questionnaire.ListReq": {
+ "type": "object",
+ "properties": {
+ "endTime": {
+ "description": "创建时间范围-结束,Unix 时间戳(秒,选填)",
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "questionType": {
+ "description": "模糊匹配:问题类型(选填)",
+ "type": "string"
+ },
+ "startTime": {
+ "description": "创建时间范围-开始,Unix 时间戳(秒,选填)",
+ "type": "integer"
+ },
+ "status": {
+ "description": "精确匹配:处理状态(选填)",
+ "type": "string"
+ }
+ }
+ },
+ "srv_questionnaire.ListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/questionnairemod.QuestionnaireModel"
+ }
+ },
+ "total": {
+ "description": "满足条件的总条数",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_richtextcontent.DelRichTextContentReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_richtextcontent.GetRichTextContentListReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "srv_richtextcontent.GetRichTextContentResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/richtextcontentmod.RichTextContentModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_richtextcontent.UpdateRichTextContentData": {
+ "type": "object",
+ "properties": {
+ "_id": {
+ "type": "integer"
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "location": {
+ "description": "位置",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序值",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_richtextcontent.UpdateRichTextContentReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_richtextcontent.UpdateRichTextContentData"
+ }
+ ]
+ }
+ }
+ },
+ "srv_translation.TranslationInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "导航ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "transCategoryId": {
+ "description": "导航分类ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/translationmod.TranslationInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoUpdate": {
+ "type": "object",
+ "properties": {
+ "htmlId": {
+ "description": "富文本ID",
+ "type": "integer"
+ },
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "link": {
+ "description": "外部跳转",
+ "type": "string"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "transCategoryId": {
+ "type": "integer"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_translation.TranslationInfoUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "导航分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "导航分类名称",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/translationcategorymod.TranslationCategory"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "categoryType": {
+ "description": "导航类型 1=url跳转 2=二级导航 3=底部导航",
+ "type": "integer"
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "跳转URL",
+ "type": "string"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_user.UserListReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "nickName": {
+ "description": "昵称(模糊)",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ }
+ }
+ },
+ "srv_user.UserListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/usermod.User"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_user.UserUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "$ref": "#/definitions/usermod.UserUpdate"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryReq": {
+ "type": "object",
+ "properties": {
+ "displayPosition": {
+ "description": "显示位置 0=仅TAB 1=仅首页 2=两者都显示",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategoryDisplayPosition"
+ }
+ ]
+ },
+ "id": {
+ "description": "分类 ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称(模糊匹配)",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "sortType": {
+ "description": "排序方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategorySortType"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videocategorymod.VideoCategory"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryUpdate": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "displayMode": {
+ "description": "展示方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.DisplayMode"
+ }
+ ]
+ },
+ "displayPosition": {
+ "description": "显示位置 0=仅TAB 1=仅首页 2=两者都显示",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategoryDisplayPosition"
+ }
+ ]
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sortType": {
+ "description": "排序方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategorySortType"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "tags": {
+ "description": "关联视频标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "translationCategoryId": {
+ "description": "导航分类",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videocollection.CollectionMediaInfo": {
+ "type": "object",
+ "properties": {
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "nodes": {
+ "description": "节点",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.Node"
+ }
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videocollection.GetCollectionMediaListReq": {
+ "type": "object",
+ "properties": {
+ "collectionID": {
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocollection.GetCollectionMediaListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_videocollection.CollectionMediaInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocollection.UpdateCollectionMediaRankReq": {
+ "type": "object",
+ "properties": {
+ "collectionID": {
+ "description": "合集ID",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "videoId": {
+ "description": "视频ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocollection.VideoCollectionOptionMediaReq": {
+ "type": "object",
+ "properties": {
+ "collectionID": {
+ "description": "合集ID",
+ "type": "integer"
+ },
+ "isAdd": {
+ "description": "增加还是移除",
+ "type": "boolean"
+ },
+ "videoIds": {
+ "description": "视频ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videocreatermod.VideoCreaterInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "background": {
+ "description": "背景",
+ "type": "string"
+ },
+ "createType": {
+ "description": "创作者类型 0=普通 1=官方",
+ "type": "integer"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "image": {
+ "description": "头像",
+ "type": "string"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.AddVideoCollectionReq": {
+ "type": "object",
+ "properties": {
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "firstTag": {
+ "description": "一级标签",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ }
+ }
+ },
+ "srv_videoinfo.AddVideoReq": {
+ "type": "object",
+ "properties": {
+ "fileId": {
+ "description": "资源id",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "srv_videoinfo.SetFakeCountResp": {
+ "type": "object",
+ "properties": {
+ "matchedCount": {
+ "description": "匹配到的视频数量",
+ "type": "integer"
+ },
+ "modifiedCount": {
+ "description": "实际修改的视频数量",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SetFakeLikesReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,支持单个或批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "likesMax": {
+ "description": "假点赞数随机最大值,包含该值",
+ "type": "integer"
+ },
+ "likesMin": {
+ "description": "假点赞数随机最小值,包含该值",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SetFakeWatchTimesReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,支持单个或批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "watchTimesMax": {
+ "description": "假观看次数随机最大值,包含该值",
+ "type": "integer"
+ },
+ "watchTimesMin": {
+ "description": "假观看次数随机最小值,包含该值",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SetRecommendReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ }
+ }
+ },
+ "srv_videoinfo.SyncMediaFileItem": {
+ "type": "object",
+ "properties": {
+ "bango": {
+ "type": "string"
+ },
+ "coverImg": {
+ "type": "string"
+ },
+ "fieldNameFs": {
+ "type": "string"
+ },
+ "firstTag": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "string"
+ },
+ "keyworld": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "publish": {
+ "type": "string"
+ },
+ "secondTags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "taskId": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "videoTime": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SyncMediaFileReq": {
+ "type": "object",
+ "properties": {
+ "fileId": {
+ "description": "媒资库影片 ID",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "firstTagMap": {
+ "description": "覆盖一级标签",
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "minVideoSeconds": {
+ "description": "最短时长,0 表示不限制",
+ "type": "integer"
+ },
+ "sourceVersion": {
+ "description": "v1 / v2",
+ "type": "string"
+ },
+ "videoList": {
+ "description": "媒资库搜索结果,优先使用",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_videoinfo.SyncMediaFileItem"
+ }
+ }
+ }
+ },
+ "srv_videoinfo.VideoAddTagsReq": {
+ "type": "object",
+ "required": [
+ "tags",
+ "videoIds"
+ ],
+ "properties": {
+ "tags": {
+ "description": "统一标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "videoIds": {
+ "description": "视频ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoReq": {
+ "type": "object",
+ "properties": {
+ "actressIds": {
+ "description": "女优ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "bango": {
+ "description": "番号,",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "创建时间-截止",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "string"
+ },
+ "mediaType": {
+ "description": "资源类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "optionAccount": {
+ "description": "最后操作人",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "sort": {
+ "description": "排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数。按分类查询且排序方式为排序值时,使用该分类的 categoryRanks 排序",
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "创建时间-开始",
+ "type": "string"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "tag": {
+ "description": "标签",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "videoFileId": {
+ "description": "切片id(切片列表的ID)",
+ "type": "string"
+ },
+ "videoTaskId": {
+ "description": "视频任务id",
+ "type": "string"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.VideoInformation"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoUpdate": {
+ "type": "object",
+ "properties": {
+ "actressIds": {
+ "description": "女优ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "addedTime": {
+ "type": "string"
+ },
+ "bango": {
+ "description": "番号,唯一",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "firstTag": {
+ "description": "一级标签",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "issuedAt": {
+ "description": "发行时间",
+ "type": "string"
+ },
+ "issuedBrand": {
+ "description": "发行商",
+ "type": "string"
+ },
+ "keyworld": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "mediaType": {
+ "description": "资源类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "nodes": {
+ "description": "节点",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.Node"
+ }
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secondUpdatedAt": {
+ "description": "二次修改时间",
+ "type": "string"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "unencryptedImage": {
+ "description": "不加密封面",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videoinfo.VideoSetTagsReq": {
+ "type": "object",
+ "required": [
+ "ids"
+ ],
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagIds": {
+ "description": "标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "srv_videotag.HotTagItem": {
+ "type": "object",
+ "properties": {
+ "tagId": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "watchTimes": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.HotTagResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_videotag.HotTagItem"
+ }
+ }
+ }
+ },
+ "srv_videotag.IpStatReq": {
+ "type": "object",
+ "properties": {
+ "todayIP": {
+ "type": "integer"
+ },
+ "yesterdayIP": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videotagmod.VideoTagInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "images": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "translationcategorymod.TranslationCategory": {
+ "type": "object",
+ "properties": {
+ "categoryType": {
+ "description": "导航类型 1:url 2:二级导航 3:底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.TranslationType"
+ }
+ ]
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "跳转URL",
+ "type": "string"
+ }
+ }
+ },
+ "translationmod.TranslationInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "htmlId": {
+ "description": "富文本ID",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "link": {
+ "description": "外部跳转",
+ "type": "string"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "transCategoryId": {
+ "description": "导航分类",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "usermod.User": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "age": {
+ "description": "年龄",
+ "type": "integer"
+ },
+ "agentInviteCode": {
+ "description": "被邀请的inviteCode",
+ "type": "string"
+ },
+ "appVer": {
+ "description": "app版本",
+ "type": "string"
+ },
+ "auth": {
+ "description": "是否认证",
+ "type": "boolean"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "backgroud": {
+ "description": "背景图",
+ "type": "string"
+ },
+ "consecutiveLogin": {
+ "description": "连续登陆天数",
+ "type": "integer"
+ },
+ "consumptionTotal": {
+ "description": "金币总消费数量",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "currentVipExpire": {
+ "description": "当前vip过期时间",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示",
+ "type": "string"
+ },
+ "devV2": {
+ "description": "设备唯一设备ID",
+ "type": "string"
+ },
+ "device": {
+ "description": "设备",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "商区码",
+ "type": "string"
+ },
+ "gender": {
+ "description": "性别 0-未知,1-男,2-女",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "introduction": {
+ "description": "简介",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "invites": {
+ "description": "我邀请的人的个数(我邀请他人)",
+ "type": "integer"
+ },
+ "isRegister": {
+ "description": "是否已注册",
+ "type": "boolean"
+ },
+ "lastIp": {
+ "description": "上次登陆IP",
+ "type": "string"
+ },
+ "lastLoginAt": {
+ "description": "上次登陆时间",
+ "type": "string"
+ },
+ "loginType": {
+ "description": "登陆方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "mac": {
+ "description": "设备唯一mac地址",
+ "type": "string"
+ },
+ "modifyUserNameAt": {
+ "description": "上一次修改用户时间",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oriVer": {
+ "description": "注册时的app版本",
+ "type": "string"
+ },
+ "password": {
+ "description": "登陆密码",
+ "type": "string"
+ },
+ "recharge": {
+ "description": "当前充值余额,金币个数",
+ "type": "integer"
+ },
+ "rechargeTotal": {
+ "description": "累计充值额,金币个数",
+ "type": "integer"
+ },
+ "regIp": {
+ "description": "注册IP",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区",
+ "type": "string"
+ },
+ "registerTime": {
+ "type": "string"
+ },
+ "score": {
+ "description": "积分",
+ "type": "integer"
+ },
+ "scoreInvites": {
+ "description": "邀请人获得的积分累计",
+ "type": "integer"
+ },
+ "scoreLvName": {
+ "description": "积分等级名称",
+ "type": "string"
+ },
+ "scoreTotal": {
+ "description": "累计积分",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ },
+ "sysMsgAt": {
+ "description": "获取系统消息时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "系统类型 ios android",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "usermod.UserUpdate": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "age": {
+ "description": "年龄",
+ "type": "integer"
+ },
+ "agentInviteCode": {
+ "description": "被邀请的inviteCode",
+ "type": "string"
+ },
+ "appVer": {
+ "description": "app版本",
+ "type": "string"
+ },
+ "auth": {
+ "description": "是否认证",
+ "type": "boolean"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "backgroud": {
+ "description": "背景图",
+ "type": "string"
+ },
+ "consecutiveLogin": {
+ "description": "连续登陆天数",
+ "type": "integer"
+ },
+ "consumptionTotal": {
+ "description": "金币总消费数量",
+ "type": "integer"
+ },
+ "currentVipExpire": {
+ "description": "当前vip过期时间",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示",
+ "type": "string"
+ },
+ "devV2": {
+ "description": "设备唯一设备ID",
+ "type": "string"
+ },
+ "device": {
+ "description": "设备",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "商区码",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "gender": {
+ "description": "性别 0-未知,1-男,2-女",
+ "type": "integer"
+ },
+ "introduction": {
+ "description": "简介",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "invites": {
+ "description": "我邀请的人的个数(我邀请他人)",
+ "type": "integer"
+ },
+ "isRegister": {
+ "description": "是否已注册",
+ "type": "boolean"
+ },
+ "lastIp": {
+ "description": "上次登陆IP",
+ "type": "string"
+ },
+ "lastLoginAt": {
+ "description": "上次登陆时间",
+ "type": "string"
+ },
+ "loginType": {
+ "description": "登陆方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "mac": {
+ "description": "设备唯一mac地址",
+ "type": "string"
+ },
+ "merchantExpiredAt": {
+ "description": "商家/兼职有效期",
+ "type": "string"
+ },
+ "modifyUserNameAt": {
+ "description": "上一次修改用户时间",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oriVer": {
+ "description": "注册时的app版本",
+ "type": "string"
+ },
+ "password": {
+ "description": "登陆密码",
+ "type": "string"
+ },
+ "recharge": {
+ "description": "当前充值余额,金币个数",
+ "type": "integer"
+ },
+ "rechargeTotal": {
+ "description": "累计充值额,金币个数",
+ "type": "integer"
+ },
+ "regIp": {
+ "description": "注册IP",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区",
+ "type": "string"
+ },
+ "registerTime": {
+ "type": "string"
+ },
+ "score": {
+ "description": "积分",
+ "type": "integer"
+ },
+ "scoreInvites": {
+ "description": "邀请人获得的积分累计",
+ "type": "integer"
+ },
+ "scoreLvName": {
+ "description": "积分等级名称",
+ "type": "string"
+ },
+ "scoreTotal": {
+ "description": "累计积分",
+ "type": "integer"
+ },
+ "showCardExpiredAt": {
+ "description": "最新会员卡过期时间",
+ "type": "string"
+ },
+ "showCardId": {
+ "description": "最新会员卡id",
+ "type": "integer"
+ },
+ "showCardName": {
+ "description": "最新会员卡名称",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ },
+ "sysMsgAt": {
+ "description": "获取系统消息时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "系统类型 ios android",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videocategorymod.VideoCategory": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "displayMode": {
+ "description": "展示方式 0:默认 1:卡片层叠轮播 2:头像列表 3:图文列表 4:大封面列表",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.DisplayMode"
+ }
+ ]
+ },
+ "displayPosition": {
+ "description": "显示位置 0=仅TAB 1=仅首页 2=两者都显示",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategoryDisplayPosition"
+ }
+ ]
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sortType": {
+ "description": "排序方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategorySortType"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "tags": {
+ "description": "关联视频标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "translationCategoryId": {
+ "description": "导航分类",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videocreatermod.VideoCreaterInfo": {
+ "type": "object",
+ "properties": {
+ "background": {
+ "description": "背景",
+ "type": "string"
+ },
+ "createType": {
+ "description": "创作者类型 0=普通 1=官方",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "image": {
+ "description": "头像",
+ "type": "string"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "name": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.HevcStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ -1
+ ],
+ "x-enum-comments": {
+ "HevcStatusFailed": "转码失败",
+ "HevcStatusNone": "未提交转码",
+ "HevcStatusPending": "转码中",
+ "HevcStatusSuccess": "转码成功"
+ },
+ "x-enum-varnames": [
+ "HevcStatusNone",
+ "HevcStatusPending",
+ "HevcStatusSuccess",
+ "HevcStatusFailed"
+ ]
+ },
+ "videoinformationmod.MediaType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "MediaType_AV": "AV视频",
+ "MediaType_Advitise": "广告",
+ "MediaType_Collection": "合集",
+ "MediaType_Video": "视频"
+ },
+ "x-enum-varnames": [
+ "MediaType_Video",
+ "MediaType_Collection",
+ "MediaType_Advitise",
+ "MediaType_AV"
+ ]
+ },
+ "videoinformationmod.Node": {
+ "type": "object",
+ "properties": {
+ "images": {
+ "description": "图片",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "nodeId": {
+ "description": "节点ID",
+ "type": "string"
+ },
+ "text": {
+ "description": "文本",
+ "type": "string"
+ },
+ "title": {
+ "description": "节点标题",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ },
+ "videos": {
+ "description": "视频信息列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ }
+ }
+ },
+ "videoinformationmod.VideoInfo": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "fileId": {
+ "description": "媒资库ID",
+ "type": "string"
+ },
+ "h265Url": {
+ "description": "H.265 m3u8 播放地址,空字符串表示暂无该编码源",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "H.264 m3u8 播放地址(始终存在)",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.VideoInformation": {
+ "type": "object",
+ "properties": {
+ "actressIds": {
+ "description": "女优ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "audioFile": {
+ "description": "音频文件路径",
+ "type": "string"
+ },
+ "bango": {
+ "description": "番号,唯一",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "fake": {
+ "description": "伪造流量",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.FakeFlow"
+ }
+ ]
+ },
+ "firstTag": {
+ "description": "一级标签",
+ "type": "string"
+ },
+ "hevcFailCount": {
+ "description": "主视频转码累计失败次数",
+ "type": "integer"
+ },
+ "hevcMetaInfo": {
+ "description": "H265 视频元信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoMetaInfo"
+ }
+ ]
+ },
+ "hevcStatus": {
+ "description": "主视频 H265 转码状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.HevcStatus"
+ }
+ ]
+ },
+ "id": {
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "issuedAt": {
+ "description": "发行时间",
+ "type": "string"
+ },
+ "issuedBrand": {
+ "description": "发行商",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "mediaType": {
+ "description": "资源类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "nodes": {
+ "description": "节点",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.Node"
+ }
+ },
+ "optionAccount": {
+ "description": "最后操作人",
+ "type": "string"
+ },
+ "pageUpdatedAt": {
+ "description": "页面展示更新时间",
+ "type": "string"
+ },
+ "previewVtt": {
+ "description": "雪碧图 vtt",
+ "type": "string"
+ },
+ "real": {
+ "description": "真实流量",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.Flow"
+ }
+ ]
+ },
+ "realUpdateAt": {
+ "description": "播放相关数据更新时间",
+ "type": "string"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secondUpdatedAt": {
+ "description": "二次修改时间",
+ "type": "string"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "smartCover": {
+ "description": "智能封面",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "syncId": {
+ "description": "同步ID",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "unencryptedImage": {
+ "description": "不加密封面",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "uploadUserName": {
+ "description": "上传用户名",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "videoCount": {
+ "description": "合集视频数量",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.VideoMetaInfo": {
+ "type": "object",
+ "properties": {
+ "audioBitrate": {
+ "type": "integer"
+ },
+ "audioChannels": {
+ "type": "integer"
+ },
+ "audioCodec": {
+ "type": "string"
+ },
+ "audioSampleRate": {
+ "type": "integer"
+ },
+ "bitrate": {
+ "type": "integer"
+ },
+ "codec": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "number"
+ },
+ "fps": {
+ "type": "number"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "m3u8Url": {
+ "type": "string"
+ },
+ "pixFmt": {
+ "type": "string"
+ },
+ "profile": {
+ "type": "string"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "videoBitrate": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ }
+ },
+ "videotagmod.VideoTagInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "images": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "isShowHome": {
+ "description": "是否显示在首页",
+ "type": "boolean"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "watchTimes": {
+ "description": "预计算:该标签下所有视频的累计播放量",
+ "type": "integer"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.AccOpLogReq": {
+ "type": "object",
+ "properties": {
+ "actId": {
+ "description": "操作人Id",
+ "type": "integer"
+ },
+ "action": {
+ "description": "操作目的",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "开始时间",
+ "type": "string"
+ },
+ "pageNum": {
+ "description": "页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "path": {
+ "description": "请求接口",
+ "type": "string"
+ },
+ "startTime": {
+ "description": "开始时间",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.AccOpLogResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_acc.OpLogInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.BindQurlReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.BindQurlResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MainWebLoginReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ },
+ "code": {
+ "description": "验证码",
+ "type": "string"
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MainWebLoginResp": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ },
+ "accId": {
+ "description": "账户id",
+ "type": "integer"
+ },
+ "qurl": {
+ "description": "二维码",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "roleId": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "token": {
+ "description": "token",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccAddReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色ID",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccAddResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwAccDelReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccDelResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwAccGetInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccGetInfoResp": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账号",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccGetStatusReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccGetStatusResp": {
+ "type": "object",
+ "properties": {
+ "feedBack": {
+ "description": "禁用原因",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccListReq": {
+ "type": "object",
+ "properties": {
+ "keyWords": {
+ "description": "搜索条件",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 1正常 2 禁用",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccListResp": {
+ "type": "object",
+ "properties": {
+ "accRec": {
+ "description": "当前页配置记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.MwAccRec"
+ }
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccRec": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账号",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "integer"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "feedBack": {
+ "description": "禁用原因",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isBase32": {
+ "description": "// 二步签名\nBase32 string ` + "`" + `json:\"base32\"` + "`" + `\n是否已签名",
+ "type": "boolean"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "roleName": {
+ "description": "角色名",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccSaveStatusReq": {
+ "type": "object",
+ "properties": {
+ "feedBack": {
+ "description": "禁用原因",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccSaveStatusResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwAccUpPwdReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccUpReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账号",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccUpResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleAddReq": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "isEnb": {
+ "description": "状态",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleAddResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleDelReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleDelResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleDrop": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "name": {
+ "description": "角色名称",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleDropResp": {
+ "type": "object",
+ "properties": {
+ "roleDrop": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.MwRoleDrop"
+ }
+ }
+ }
+ },
+ "web_acc.MwRoleEnableReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleEnableResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleGetAuthReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleGetAuthResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "router": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router"
+ }
+ }
+ }
+ },
+ "web_acc.MwRoleGetInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleGetInfoResp": {
+ "type": "object",
+ "properties": {
+ "roleRec": {
+ "$ref": "#/definitions/web_acc.MwRoleRec"
+ }
+ }
+ },
+ "web_acc.MwRoleListResp": {
+ "type": "object",
+ "properties": {
+ "roleRecs": {
+ "description": "当前页配置记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.MwRoleRec"
+ }
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleRec": {
+ "type": "object",
+ "properties": {
+ "apiMenus": {
+ "description": "已有权限的api",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "是否启用 false:禁用 true:启用",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "角色名称",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleUpAuthReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "router": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router"
+ }
+ }
+ }
+ },
+ "web_acc.MwRoleUpAuthResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleUpReq": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "状态",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleUpResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.Router": {
+ "type": "object",
+ "properties": {
+ "apiUrl": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router_ApiUrl"
+ }
+ },
+ "children": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router"
+ }
+ },
+ "component": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "isShow": {
+ "type": "boolean"
+ },
+ "meta": {
+ "$ref": "#/definitions/web_acc.Router_Meta"
+ },
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ },
+ "redirect": {
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.Router_ApiUrl": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "type": "string"
+ },
+ "method": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "status": {
+ "type": "boolean"
+ },
+ "url": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "web_acc.Router_Meta": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "type": "string"
+ },
+ "noCache": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
+ "web_cfg.GetSysConfigReq": {
+ "type": "object"
+ },
+ "web_cfg.GetSysConfigResp": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "dhDescription": {
+ "description": "导航SEO描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "导航SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhNewUrlAddr1": {
+ "description": "导航页-线路1",
+ "type": "string"
+ },
+ "dhNewUrlAddr2": {
+ "description": "导航页-线路2",
+ "type": "string"
+ },
+ "dhNewUrlAddr3": {
+ "description": "导航页-线路3",
+ "type": "string"
+ },
+ "dhSiteName": {
+ "description": "导航SEO网站名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航SEO标题",
+ "type": "string"
+ },
+ "downloadVideoUrl": {
+ "description": "下载视频URL",
+ "type": "string"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "freeVPN": {
+ "description": "免费vpn链接",
+ "type": "string"
+ },
+ "gitAddr": {
+ "description": "git地址",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "imgAppCdn": {
+ "description": "图片CDN(app使用)",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgWebCdn": {
+ "description": "图片CDN(web使用)",
+ "type": "string"
+ },
+ "m3u8DayRateLimitMax": {
+ "description": "每 IP 每天最多不同正片数,\u003c=0 使用默认 80",
+ "type": "integer"
+ },
+ "mediaAppCdn": {
+ "description": "视频CDN(app使用)",
+ "type": "string"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "mediaWebCdn": {
+ "description": "视频CDN(web使用)",
+ "type": "string"
+ },
+ "mp4Addr": {
+ "description": "mp4地址(web使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "网站管理",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "potaoGroup": {
+ "description": "土豆群",
+ "type": "string"
+ },
+ "qq": {
+ "description": "qq群",
+ "type": "string"
+ },
+ "sitemapDefaultImages": {
+ "description": "Sitemap里面默认封面图",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "telegramAdd": {
+ "description": "加入我们tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ },
+ "unencryptedCdn": {
+ "description": "不加密资源使用的cdn",
+ "type": "string"
+ },
+ "useAddress": {
+ "description": "所有使用中的域名",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "version": {
+ "description": "版本号,用于更新乐观锁",
+ "type": "integer"
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部右边)",
+ "type": "string"
+ },
+ "watermarkMediaBottomLeft": {
+ "description": "视频水印文件(底部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "description": "视频水印文件(顶部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTopRight": {
+ "description": "视频水印文件(顶部右边)",
+ "type": "string"
+ },
+ "webAddr": {
+ "description": "网站SEO网站地址",
+ "type": "string"
+ },
+ "webDescription": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "webKeyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webSiteName": {
+ "description": "网站SEO网站名称",
+ "type": "string"
+ },
+ "webTitle": {
+ "description": "SEO",
+ "type": "string"
+ },
+ "weiChart": {
+ "description": "微信群",
+ "type": "string"
+ }
+ }
+ },
+ "web_cfg.SysConfigUpdateReq": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "dhDescription": {
+ "description": "导航SEO描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "导航SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhNewUrlAddr1": {
+ "description": "导航页-线路一",
+ "type": "string"
+ },
+ "dhNewUrlAddr2": {
+ "description": "导航页-线路2",
+ "type": "string"
+ },
+ "dhNewUrlAddr3": {
+ "description": "导航页-线路3",
+ "type": "string"
+ },
+ "dhSiteName": {
+ "description": "导航SEO网站名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航SEO标题",
+ "type": "string"
+ },
+ "downloadVideoUrl": {
+ "description": "下载视频URL",
+ "type": "string"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "gitAddr": {
+ "description": "git地址",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "imgAppCdn": {
+ "description": "图片CDN(app使用)",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgWebCdn": {
+ "description": "图片CDN(web使用)",
+ "type": "string"
+ },
+ "m3u8DayRateLimitMax": {
+ "description": "每 IP 每天最多不同正片数,\u003c=0 使用默认 80",
+ "type": "integer"
+ },
+ "mediaAppCdn": {
+ "description": "视频CDN(app使用)",
+ "type": "string"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "mediaWebCdn": {
+ "description": "视频CDN(web使用)",
+ "type": "string"
+ },
+ "mp4Addr": {
+ "description": "mp4地址(web使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "最新地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "potaoGroup": {
+ "description": "土豆群",
+ "type": "string"
+ },
+ "qq": {
+ "description": "qq群",
+ "type": "string"
+ },
+ "sitemapDefaultImages": {
+ "description": "Sitemap里面默认封面图",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "telegramAdd": {
+ "description": "加入我们tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ },
+ "unencryptedCdn": {
+ "description": "不加密资源使用的cdn",
+ "type": "string"
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部右边)",
+ "type": "string"
+ },
+ "watermarkMediaBottomLeft": {
+ "description": "视频水印文件(底部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "description": "视频水印文件(顶部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTopRight": {
+ "description": "视频水印文件(顶部右边)",
+ "type": "string"
+ },
+ "webAddr": {
+ "description": "网站SEO网站地址",
+ "type": "string"
+ },
+ "webDescription": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "webKeyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webSiteName": {
+ "description": "网站SEO网站名称",
+ "type": "string"
+ },
+ "webTitle": {
+ "description": "SEO",
+ "type": "string"
+ },
+ "weiChart": {
+ "description": "微信群",
+ "type": "string"
+ }
+ }
+ },
+ "web_cfg.SysConfigUpdateResp": {
+ "type": "object"
+ },
+ "web_commend.CommendDelIpReq": {
+ "type": "object",
+ "properties": {
+ "ip": {
+ "type": "string"
+ }
+ }
+ },
+ "web_commend.CommendDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "web_commend.CommendReviewBatchReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "reviewStatus": {
+ "type": "integer"
+ }
+ }
+ },
+ "web_file.UploadImageResp": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ }
+ },
+ "web_file.UploadImagesResp": {
+ "type": "object",
+ "properties": {
+ "paths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "ApiKeyAuth": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ },
+ "BasicAuth": {
+ "type": "basic"
+ },
+ "OAuth2AccessCode": {
+ "type": "oauth2",
+ "flow": "accessCode",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information"
+ }
+ },
+ "OAuth2Application": {
+ "type": "oauth2",
+ "flow": "application",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Implicit": {
+ "type": "oauth2",
+ "flow": "implicit",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Password": {
+ "type": "oauth2",
+ "flow": "password",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "read": "Grants read access",
+ "write": "Grants write access"
+ }
+ }
+ }
+}`
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = &swag.Spec{
+ Version: "1.0",
+ Host: "localhost:12667",
+ BasePath: "/",
+ Schemes: []string{"http", "https"},
+ Title: "站群后台接口",
+ Description: "这是站群后台接口服务.",
+ InfoInstanceName: "swagger",
+ SwaggerTemplate: docTemplate,
+ LeftDelim: "{{",
+ RightDelim: "}}",
+}
+
+func init() {
+ swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
+}
diff --git a/web/docs/swagger.json b/web/docs/swagger.json
new file mode 100644
index 0000000..cc334bf
--- /dev/null
+++ b/web/docs/swagger.json
@@ -0,0 +1,13376 @@
+{
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "swagger": "2.0",
+ "info": {
+ "description": "这是站群后台接口服务.",
+ "title": "站群后台接口",
+ "contact": {},
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0"
+ },
+ "host": "localhost:12667",
+ "basePath": "/",
+ "paths": {
+ "/api/web/acc/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-添加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-添加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccAddResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccDelResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/getinfo": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-获取信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-获取信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/getstatus": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-获取状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-获取状态",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetStatusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccGetStatusResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/ops": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-获取操作记录",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-获取操作记录",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.AccOpLogReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.AccOpLogResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/savestatus": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-保存状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-保存状态",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccSaveStatusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccSaveStatusResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/up": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/acc/uppwd": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "账号-更新密码",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "账号"
+ ],
+ "summary": "账号-更新密码",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpPwdReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwAccUpPwdReq"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "添加一个女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "删除一个女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "分组分页获取女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/actress/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个女优信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "女优管理"
+ ],
+ "summary": "更新一个女优信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_actress.ActressUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/del": {
+ "post": {
+ "description": "删除一个联系我们",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们"
+ ],
+ "summary": "删除一个联系我们",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.DelOneContactusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/list": {
+ "post": {
+ "description": "联系我们列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们"
+ ],
+ "summary": "联系我们列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.GetContactusListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.GetContactusListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改联系我们状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们"
+ ],
+ "summary": "修改联系我们状态",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_contactus.UpdateContactusReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "新增统一标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增统一标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "删除统一标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除统一标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询统一标签配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "统一标签列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "状态 true-启用 false-停用",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 1,
+ 5
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ],
+ "description": "内容类型 1-视频 5-短剧",
+ "name": "type",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/option": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "查询启用的统一标签下拉列表,传type时按支持类型过滤",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "统一标签下拉列表 -- by 王鸣",
+ "parameters": [
+ {
+ "enum": [
+ 1,
+ 5
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ],
+ "description": "内容类型 1-视频 5-短剧,不传返回全部启用标签",
+ "name": "type",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagOptionResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/content/tag/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web统一标签相关接口"
+ ],
+ "summary": "更新统一标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新统一标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContentTagUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-增加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-增加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.AddDetailNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.DelDetailNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.GetDetailNavListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.GetDetailNavListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/detailnav/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "详情页导航配置-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "详情页导航配置"
+ ],
+ "summary": "详情页导航配置-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_detailnav.UpdateDetailNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/file/uploadImg": {
+ "post": {
+ "description": "图片上传",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "文件上传"
+ ],
+ "summary": "图片上传",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "文件",
+ "name": "file",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_file.UploadImageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/file/uploadImgs": {
+ "post": {
+ "description": "图片批量上传",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "文件上传"
+ ],
+ "summary": "图片批量上传",
+ "parameters": [
+ {
+ "type": "file",
+ "description": "文件",
+ "name": "file[]",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_file.UploadImagesResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "新增首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "删除首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询首页banner配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "首页banner列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "首页 banner ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码,默认1",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量,默认10,最大100",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "状态 true-启用 false-停用",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/home/banner/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web首页banner相关接口"
+ ],
+ "summary": "更新首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.HomeBannerUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-增加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-增加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.AddHotNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.DelHotNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.GetHotNavListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.GetHotNavListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/hotnav/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "热门导航配置-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "热门导航配置"
+ ],
+ "summary": "热门导航配置-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_hotnav.UpdateHotNavReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/ipblack/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "IP黑名单-增加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP黑名单"
+ ],
+ "summary": "IP黑名单-增加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.AddIPBlackListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/ipblack/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "IP黑名单-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP黑名单"
+ ],
+ "summary": "IP黑名单-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.DelIpBlackListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/ipblack/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "IP黑名单-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP黑名单"
+ ],
+ "summary": "IP黑名单-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.GetIPBlackListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_ipblack.GetIPBlackListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/login/acc": {
+ "post": {
+ "description": "登录-登录",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "登录"
+ ],
+ "summary": "登录-登录",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MainWebLoginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MainWebLoginResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/login/bind": {
+ "post": {
+ "description": "登录-绑定二维码",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "登录"
+ ],
+ "summary": "登录-绑定二维码",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.BindQurlReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.BindQurlResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/login/slogin": {
+ "post": {
+ "description": "管理员sso登陆",
+ "consumes": [
+ "multipart/form-data",
+ "application/json"
+ ],
+ "produces": [
+ "application/json",
+ "text/html"
+ ],
+ "tags": [
+ "web-单点登陆"
+ ],
+ "summary": "管理员sso登陆",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "账号",
+ "name": "name",
+ "in": "formData",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "密码",
+ "name": "password",
+ "in": "formData",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "{\"msg\": \"管理员信息\"\"}",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "{\"msg\": \"操作失败\"}",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "新增 playlet 集合短剧数据,支持分类、题材、标签、支付方式、推荐和 banner 配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增短剧请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/banner/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 showOnBanner 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧是否显示在首页banner -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧是否显示在首页banner请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletBannerSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端删除短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧分类列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "分类ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "分类名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "启用状态",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 categoryIds 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategorySetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/category/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新短剧分类 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新短剧分类请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCategoryUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "新增 playlet_chapter 集合短剧剧集数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧剧集 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据剧集ID集合删除 playlet_chapter 记录,仅原创短剧支持删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量删除短剧剧集",
+ "parameters": [
+ {
+ "description": "单/批量删除短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询 playlet_chapter 集合短剧剧集数据,默认按集数升序返回",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧剧集列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "剧集ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusFailed": "处理失败",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone",
+ "WatermarkStatusFailed"
+ ],
+ "description": "水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败",
+ "name": "watermarkStatus",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/pay-type/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据剧集ID集合批量更新 playlet_chapter 集合 payType 和 singleCoins 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧剧集支付方式 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧剧集支付方式请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterPayTypeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/sync": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合从媒资中心同步新增剧集,playletIds传1个表示单条同步,传多个表示批量同步,允许已完结短剧补集或修正状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端手动单/批量同步短剧剧集",
+ "parameters": [
+ {
+ "description": "手动单/批量同步短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterSyncReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterSyncResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按剧集ID编辑 playlet_chapter 集合短剧剧集数据,只更新请求中传入的字段",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端编辑短剧剧集 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "编辑短剧剧集请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/featured/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 isFeatured 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧是否爆剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧是否爆剧请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletFeaturedSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/like/virtual/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 virtualCountLike 字段,每条短剧从最小值和最大值之间随机取不重复数值入库",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧虚拟点赞数 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量随机设置短剧虚拟点赞数请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletVirtualLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "分类ID",
+ "name": "categoryId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集(对应 media_type=6)",
+ "ContentTypeAnime": "动漫(对应 media_type=1)",
+ "ContentTypeComic": "漫画短剧(对应 media_type=4)",
+ "ContentTypeManga": "漫画(对应 media_type=2)",
+ "ContentTypeTheater": "短剧(对应 media_type=5)"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ],
+ "description": "内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画",
+ "name": "contentType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否爆剧,true-是、false-否",
+ "name": "isFeatured",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否原创:true只查原创,false只查非原创,不传查全部",
+ "name": "isOriginal",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "name": "isRecommend",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "最后操作人名称,支持模糊查询",
+ "name": "operatorName",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ],
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "name": "payType",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否显示在首页banner,true-显示、false-不显示",
+ "name": "showOnBanner",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ],
+ "description": "上架状态,0-下架、1-上架",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "标签ID",
+ "name": "tagId",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "题材ID",
+ "name": "themeId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧标题,支持模糊查询",
+ "name": "title",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/pay-type/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 payType/singleCoins 字段",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧默认支付方式 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧默认支付方式请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletPayTypeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/recommend/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧是否推荐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧是否推荐请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletRecommendSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/status/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 status 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧上下架 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧上下架请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletStatusSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端删除标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧标签",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端标签列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 tagIds 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/tag/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新标签请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletTagUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端删除题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧题材",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端题材列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "题材ID",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "题材名称,支持模糊查询",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "上架状态",
+ "name": "status",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端单/批量设置短剧题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "单/批量设置短剧题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新题材 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新题材请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按短剧ID编辑 playlet 集合短剧数据,只更新请求中传入的字段",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端编辑短剧 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "编辑短剧请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/watermark/reset": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合重置剧集水印状态,保留已有水印地址以便换水印期间继续观看",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端按短剧ID批量重置剧集水印 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "批量重置短剧水印请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletWatermarkResetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/del": {
+ "post": {
+ "description": "删除一个评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "删除一个评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_commend.CommendDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/list": {
+ "post": {
+ "description": "评论列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "评论列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendSearch"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendSearchResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/replay": {
+ "post": {
+ "description": "回复评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "回复评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendReplay"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/review": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "审核评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "审核评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_commend.CommendReviewReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/reviewBatch": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "批量审核评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "批量审核评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_commend.CommendReviewBatchReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/post/comment/user/del": {
+ "post": {
+ "description": "删除一个用户所有评论",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论"
+ ],
+ "summary": "删除一个用户所有评论",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_commend.CommendDelIpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/questionnaire/handle": {
+ "post": {
+ "description": "只有状态为\"待处理\"的问卷才允许更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "问卷调查"
+ ],
+ "summary": "处理问卷",
+ "parameters": [
+ {
+ "description": "处理参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_questionnaire.HandleReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/questionnaire/list": {
+ "post": {
+ "description": "支持分页及条件筛选(question_type、status、时间范围)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "问卷调查"
+ ],
+ "summary": "查询问卷列表",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_questionnaire.ListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_questionnaire.ListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/resource/chapters": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按媒资ID查询剧集信息,复用入库时的 mediaSync 接口",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web媒资库相关接口"
+ ],
+ "summary": "web端短剧媒资剧集列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "媒资中心资源ID",
+ "name": "id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletResourceChapterListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/resource/import": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "提交媒资入库任务,后台从媒资中心同步短剧详情并写入短剧和章节集合",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web媒资库相关接口"
+ ],
+ "summary": "web端短剧媒资入库 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧媒资入库请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletResourceUseReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/resource/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧媒资列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web媒资库相关接口"
+ ],
+ "summary": "web端短剧媒资列表 -- by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "媒体类型 1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧",
+ "name": "media_type",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码",
+ "name": "pageNumber",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "个数",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "同步类型 1-全量同步 2-增量同步",
+ "name": "sync_type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标签",
+ "name": "tag",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "标题",
+ "name": "title",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletResourceListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "添加一个富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/richtextcontentmod.RichTextContentModel"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "删除一个富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.DelRichTextContentReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "分组分页获取富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.GetRichTextContentListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.GetRichTextContentResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/richtextcontent/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个富文本内容",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "富文本内容"
+ ],
+ "summary": "更新一个富文本内容",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_richtextcontent.UpdateRichTextContentReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-添加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-添加",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleAddResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-删除",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-删除",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleDelResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/drop": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-下拉列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-下拉列表",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleDropResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/enable": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-使能",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-使能",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleEnableReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleEnableResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/getauth": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-获取权限",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-获取权限",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetAuthReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetAuthResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/getinfo": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-获取信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-获取信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleGetInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/pubdef.Page"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/up": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-更新",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-更新",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/role/upauth": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "角色-更新权限",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "角色"
+ ],
+ "summary": "角色-更新权限",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpAuthReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_acc.MwRoleUpAuthResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/slug/sync": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "为视频、分类、女优、作者、短剧、统一标签等空 slug 按 SEO 关键词或名称自动生成。可重复调用,hasMore 为 true 时继续请求。type 为空处理全部,也可指定 video/videoCategory/actress/videoCreater/playlet/playletCategory/playletTheme/contentTag。",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web slug相关接口"
+ ],
+ "summary": "同步历史空 slug",
+ "parameters": [
+ {
+ "description": "同步参数",
+ "name": "param",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/proto.SlugSyncReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.SlugSyncResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/stat/analytics": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "统计数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "获取统计数据"
+ ],
+ "summary": "统计数据",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/stat/daily/analytics": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "统计数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "获取统计数据"
+ ],
+ "summary": "统计数据",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/syscfg/get": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "获取系统配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "系统配置"
+ ],
+ "summary": "获取系统配置",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_cfg.GetSysConfigReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_cfg.GetSysConfigResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/syscfg/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改系统配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "系统配置"
+ ],
+ "summary": "修改系统配置",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/web_cfg.SysConfigUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/web_cfg.SysConfigUpdateResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "添加一个导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/translationmod.TranslationInfo"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "删除一个导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "分组分页获取导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translation/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个导航跳转信息",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航跳转信息"
+ ],
+ "summary": "更新一个导航跳转信息",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translation.TranslationInfoUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "添加一个导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/translationcategorymod.TranslationCategory"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "删除一个导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "分组分页获取导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/translationcategory/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "导航分类"
+ ],
+ "summary": "更新一个导航分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/user/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "查询用户列表",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.UserListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_user.UserListResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/user/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "更新用户信息",
+ "parameters": [
+ {
+ "description": "更新参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.UserUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "添加一个视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videocategorymod.VideoCategory"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "删除一个视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "分组分页获取视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocategory/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个视频分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频分类"
+ ],
+ "summary": "更新一个视频分类",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryUpdate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocollection/media/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "获取合集视频列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合集管理"
+ ],
+ "summary": "获取合集视频列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.GetCollectionMediaListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.GetCollectionMediaListResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocollection/media/option": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改合集里的视频列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合集管理"
+ ],
+ "summary": "修改合集里的视频列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.VideoCollectionOptionMediaReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocollection/media/rank": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "修改合集里的视频排序",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "合集管理"
+ ],
+ "summary": "修改合集里的视频排序",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocollection.UpdateCollectionMediaRankReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "添加一个视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videocreatermod.VideoCreaterInfo"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "删除一个视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "分组分页获取视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videocreater/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个视频作者",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频作者"
+ ],
+ "summary": "更新一个视频作者",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterUpdate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/addTags": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据视频ID集合批量追加统一标签,已存在的标签不会重复追加",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "批量追加视频标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoAddTagsReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/addcoll": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "新增一个视频合集",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "新增一个视频合集",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.AddVideoCollectionReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个视频管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "删除一个视频管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取视频管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "分组分页获取视频管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/recommend/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据视频ID集合批量更新 video_information 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频是否推荐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetRecommendReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/setFakeLikes": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据ids批量随机设置视频fake.likes,随机范围包含最小值和最大值",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频假点赞数 --by 旺鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeLikesReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeCountResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/setFakeWatchTimes": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据ids批量随机设置视频fake.watchTimes,随机范围包含最小值和最大值",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频假观看次数 --by 旺鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeWatchTimesReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SetFakeCountResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/sync": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "从切片列表同步多个视频",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "从切片列表同步多个视频",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.AddVideoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/syncMediaFile": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "从媒资库直同步到视频列表,后续切片同步时按 mediaSourceId 覆盖",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "从媒资库直同步到视频列表",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.SyncMediaFileReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/tag/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据视频ID集合批量覆盖 video_information 集合 tags 字段,传1个ID表示单条设置,传多个ID表示批量设置,tagIds传空数组表示清空标签",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "单/批量设置视频标签 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoSetTagsReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videoinfo/transcode": {
+ "post": {
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "提交视频 H265 转码",
+ "responses": {}
+ }
+ },
+ "/api/web/videoinfo/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个视频管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频管理"
+ ],
+ "summary": "更新一个视频管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "添加一个标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videotagmod.VideoTagInfo"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "删除一个标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "删除一个标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分组分页获取标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "分组分页获取标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotag/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "更新一个标签管理",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "更新一个标签管理",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoUpdate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/wbapi/hjsq/hottag": {
+ "get": {
+ "description": "根据视频累计播放量聚合返回热门标签列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "标签管理"
+ ],
+ "summary": "查询播放量最多的标签",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "返回条数,默认10,最大1000",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.HotTagResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/wbapi/hjsq/ipstat": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "IP统计"
+ ],
+ "summary": "查询今日和昨日去重访客IP数",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotag.IpStatReq"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "actress.Actress": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "real": {
+ "description": "真实流量",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.Flow"
+ }
+ ]
+ },
+ "realUpdateAt": {
+ "description": "播放相关数据更新时间",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "actress.Meas": {
+ "type": "object",
+ "properties": {
+ "blood": {
+ "description": "血型",
+ "type": "string"
+ },
+ "bust": {
+ "description": "胸围 英寸",
+ "type": "integer"
+ },
+ "cup": {
+ "description": "罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I",
+ "type": "integer"
+ },
+ "dia": {
+ "description": "直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)",
+ "type": "integer"
+ },
+ "gender": {
+ "description": "性别 0:女 1:男 2:其他",
+ "type": "integer"
+ },
+ "height": {
+ "description": "身高 厘米",
+ "type": "integer"
+ },
+ "hip": {
+ "description": "臀围 英寸",
+ "type": "integer"
+ },
+ "len": {
+ "description": "长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)",
+ "type": "integer"
+ },
+ "waist": {
+ "description": "腰围 英寸",
+ "type": "integer"
+ },
+ "weight": {
+ "description": "体重 公斤",
+ "type": "integer"
+ }
+ }
+ },
+ "contactusmod.ContactUsModel": {
+ "type": "object",
+ "properties": {
+ "categoryName": {
+ "description": "主旨分类",
+ "type": "string"
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮件",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "string"
+ },
+ "ip": {
+ "description": "评论IP",
+ "type": "string"
+ },
+ "name": {
+ "description": "名字",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 0=初始 1=已回复 2=不予处理 3=已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "contenttagmod.ContentTagType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 5
+ ],
+ "x-enum-comments": {
+ "ContentTagTypePlaylet": "短剧",
+ "ContentTagTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "ContentTagTypeVideo",
+ "ContentTagTypePlaylet"
+ ]
+ },
+ "detailnavmod.DetailNavModel": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "url": {
+ "description": "导航链接",
+ "type": "string"
+ }
+ }
+ },
+ "hotnavmod.HotNav": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "icon": {
+ "description": "图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "intro": {
+ "description": "介绍",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ }
+ }
+ },
+ "ipblackmod.IPBlackModel": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "备注",
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "ip": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "playletchaptermod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletchaptermod.WatermarkStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusFailed": "处理失败",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone",
+ "WatermarkStatusFailed"
+ ]
+ },
+ "playletmod.ContentType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集(对应 media_type=6)",
+ "ContentTypeAnime": "动漫(对应 media_type=1)",
+ "ContentTypeComic": "漫画短剧(对应 media_type=4)",
+ "ContentTypeManga": "漫画(对应 media_type=2)",
+ "ContentTypeTheater": "短剧(对应 media_type=5)"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ]
+ },
+ "playletmod.PayType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PayTypeCoin": "金币",
+ "PayTypeFree": "免费",
+ "PayTypeVip": "VIP"
+ },
+ "x-enum-varnames": [
+ "PayTypeFree",
+ "PayTypeCoin",
+ "PayTypeVip"
+ ]
+ },
+ "playletmod.Status": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ]
+ },
+ "proto.ContentTagAddReq": {
+ "type": "object",
+ "required": [
+ "name",
+ "status",
+ "types"
+ ],
+ "properties": {
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或名称自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ }
+ }
+ },
+ "proto.ContentTagAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContentTagDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContentTagListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContentTagOptionItem": {
+ "type": "object",
+ "properties": {
+ "bindCount": {
+ "description": "当前查询类型的绑定数量,不传type时返回0",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "typeNames": {
+ "description": "支持类型名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ }
+ }
+ },
+ "proto.ContentTagOptionResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "标签下拉列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContentTagOptionItem"
+ }
+ }
+ }
+ },
+ "proto.ContentTagUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "标签名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "types": {
+ "description": "支持类型 1-视频 5-短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contenttagmod.ContentTagType"
+ }
+ }
+ }
+ },
+ "proto.HomeBannerAddReq": {
+ "type": "object",
+ "required": [
+ "image",
+ "name",
+ "status"
+ ],
+ "properties": {
+ "image": {
+ "description": "图片",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "URL",
+ "type": "string"
+ },
+ "verticalImage": {
+ "description": "竖版图片",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HomeBannerAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "image": {
+ "description": "图片",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 true-启用 false-停用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "URL",
+ "type": "string"
+ },
+ "verticalImage": {
+ "description": "竖版图片",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletAddReq": {
+ "type": "object",
+ "required": [
+ "title"
+ ],
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图",
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "contentType": {
+ "description": "内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "cover": {
+ "description": "封面原图",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "isFeatured": {
+ "description": "是否爆剧,true-是、false-否",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告",
+ "type": "string"
+ },
+ "recommendRank": {
+ "description": "推荐排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或标题自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态,0-下架、1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "topRank": {
+ "description": "置顶排序",
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletBannerSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner,true-显示、false-不显示",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletCategoryAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "icon": {
+ "description": "分类图标",
+ "type": "string"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或名称自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "启用状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletCategoryAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCategoryDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCategoryListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCategorySetReq": {
+ "type": "object",
+ "properties": {
+ "categoryIds": {
+ "description": "分类ID集合,可设置多个分类,传空数组表示清空分类,不传表示参数错误",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletCategoryUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "icon": {
+ "description": "分类图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "启用状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterAddReq": {
+ "type": "object",
+ "required": [
+ "currentEpisode",
+ "playletId"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "视频高度",
+ "type": "integer"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaSourceId": {
+ "description": "media切片服原片ID,上传正片已创建原片时直接沿用",
+ "type": "string"
+ },
+ "mediaTime": {
+ "description": "影片长度,单位秒",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "title": {
+ "description": "剧集标题",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "原片视频地址",
+ "type": "string"
+ },
+ "width": {
+ "description": "视频宽度",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "剧集ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterDelReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "剧集ID集合,传1个表示单条删除,传多个表示批量删除",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletChapterListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterPayTypeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "剧集ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时必填且需大于0",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterSyncReq": {
+ "type": "object",
+ "required": [
+ "playletIds"
+ ],
+ "properties": {
+ "playletIds": {
+ "description": "短剧ID集合,传1个表示单条同步,传多个表示批量同步",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletChapterSyncResp": {
+ "type": "object",
+ "properties": {
+ "failedCount": {
+ "description": "失败数量",
+ "type": "integer"
+ },
+ "list": {
+ "description": "同步结果集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterSyncResult"
+ }
+ },
+ "successCount": {
+ "description": "成功数量",
+ "type": "integer"
+ },
+ "total": {
+ "description": "同步总数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterSyncResult": {
+ "type": "object",
+ "properties": {
+ "newChapterCount": {
+ "description": "本次新增剧集数量",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "reason": {
+ "description": "失败原因",
+ "type": "string"
+ },
+ "success": {
+ "description": "是否同步成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "视频高度",
+ "type": "integer"
+ },
+ "id": {
+ "description": "剧集ID",
+ "type": "integer"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度,单位秒",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ }
+ ]
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "title": {
+ "description": "剧集标题",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "原片视频地址",
+ "type": "string"
+ },
+ "width": {
+ "description": "视频宽度",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletFeaturedSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isFeatured": {
+ "description": "是否爆剧,true-是、false-否",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletPayTypeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "payType": {
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时必填且需大于0",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRecommendSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isRecommend": {
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletResourceChapterItem": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createTime": {
+ "description": "创建时间",
+ "type": "integer"
+ },
+ "episodeNumber": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hashId": {
+ "description": "资源hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "媒资内容ID",
+ "type": "integer"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "playTime": {
+ "description": "播放时长(秒)",
+ "type": "integer"
+ },
+ "saleCoin": {
+ "description": "单集售价",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "updateTime": {
+ "description": "更新时间",
+ "type": "integer"
+ },
+ "videoUrl": {
+ "description": "播放地址",
+ "type": "string"
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceChapterListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "剧集列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletResourceChapterItem"
+ }
+ },
+ "mediaId": {
+ "description": "媒资中心资源ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "短剧标题",
+ "type": "string"
+ },
+ "total": {
+ "description": "剧集数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceListItem": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者",
+ "type": "string"
+ },
+ "create_time": {
+ "description": "创建时间",
+ "type": "integer"
+ },
+ "data_version": {
+ "description": "数据版本",
+ "type": "integer"
+ },
+ "horizontal_cover": {
+ "description": "横版封面",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isAdd": {
+ "description": "是否已加入后台数据",
+ "type": "boolean"
+ },
+ "media_type": {
+ "description": "媒体类型",
+ "type": "integer"
+ },
+ "sale_coin": {
+ "description": "整部售价",
+ "type": "integer"
+ },
+ "summary": {
+ "description": "简介",
+ "type": "string"
+ },
+ "tags_text": {
+ "description": "标签文字",
+ "type": "string"
+ },
+ "terminal_cdo": {
+ "description": "上传者",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "total_episode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "update_status": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "update_time": {
+ "description": "修改时间",
+ "type": "integer"
+ },
+ "vertical_cover": {
+ "description": "竖版封面",
+ "type": "string"
+ },
+ "vlogger_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletResourceListItem"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletResourceUseReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "媒资中心资源ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletStatusSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "status": {
+ "description": "上架状态,0-下架、1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "proto.PlayletTagAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletTagAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagIds": {
+ "description": "标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletTagUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "icon": {
+ "description": "题材图标",
+ "type": "string"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或名称自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据"
+ },
+ "pageNum": {
+ "description": "当前页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数量",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合,可设置多个题材",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.PlayletThemeUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "icon": {
+ "description": "题材图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图",
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类ID集合,传空数组表示清空分类",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "contentType": {
+ "description": "内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.ContentType"
+ }
+ ]
+ },
+ "cover": {
+ "description": "封面原图",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧,true-是、false-否",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐,true-推荐、false-不推荐",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式,0-免费、1-金币、2-VIP",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.PayType"
+ }
+ ]
+ },
+ "preview": {
+ "description": "预告",
+ "type": "string"
+ },
+ "recommendRank": {
+ "description": "推荐排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "singleCoins": {
+ "description": "单集金币,支付方式为1-金币时生效",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug,历史数据为空时更新会自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态,0-下架、1-上架",
+ "allOf": [
+ {
+ "$ref": "#/definitions/playletmod.Status"
+ }
+ ]
+ },
+ "tagIds": {
+ "description": "标签ID集合,传空数组表示清空标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "topRank": {
+ "description": "置顶排序",
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletVirtualLikeSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "maxVirtualCountLike": {
+ "description": "虚拟点赞数最大值,必须大于等于最小值",
+ "type": "integer"
+ },
+ "minVirtualCountLike": {
+ "description": "虚拟点赞数最小值,必须大于等于0",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletWatermarkResetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条,传多个表示批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "proto.SlugSyncReq": {
+ "type": "object",
+ "properties": {
+ "maxSize": {
+ "description": "本次最多处理的空 slug 记录数,默认500,最大2000",
+ "type": "integer"
+ },
+ "type": {
+ "description": "可选,指定类型;空则按顺序处理全部",
+ "type": "string"
+ }
+ }
+ },
+ "proto.SlugSyncResp": {
+ "type": "object",
+ "properties": {
+ "actressUpdated": {
+ "description": "本次补齐的女优数",
+ "type": "integer"
+ },
+ "contentTagUpdated": {
+ "description": "本次补齐的统一标签数",
+ "type": "integer"
+ },
+ "hasMore": {
+ "description": "是否还有未处理的历史数据",
+ "type": "boolean"
+ },
+ "playletCategoryUpdated": {
+ "description": "本次补齐的短剧分类数",
+ "type": "integer"
+ },
+ "playletThemeUpdated": {
+ "description": "本次补齐的短剧主题数",
+ "type": "integer"
+ },
+ "playletUpdated": {
+ "description": "本次补齐的短剧数",
+ "type": "integer"
+ },
+ "totalUpdated": {
+ "description": "本次合计补齐数",
+ "type": "integer"
+ },
+ "videoCategoryUpdated": {
+ "description": "本次补齐的视频分类数",
+ "type": "integer"
+ },
+ "videoCreaterUpdated": {
+ "description": "本次补齐的作者数",
+ "type": "integer"
+ },
+ "videoUpdated": {
+ "description": "本次补齐的视频数",
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.CategoryDisplayPosition": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "CategoryDisplayPositionBoth": "TAB和首页都显示",
+ "CategoryDisplayPositionHomeOnly": "仅首页",
+ "CategoryDisplayPositionTabOnly": "仅TAB"
+ },
+ "x-enum-varnames": [
+ "CategoryDisplayPositionTabOnly",
+ "CategoryDisplayPositionHomeOnly",
+ "CategoryDisplayPositionBoth"
+ ]
+ },
+ "pubdef.CategorySortType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "x-enum-comments": {
+ "CategorySortTypeAddedTimeAsc": "上架时间升序",
+ "CategorySortTypeAddedTimeDesc": "上架时间降序",
+ "CategorySortTypeLikeAsc": "点赞(收藏)次数升序",
+ "CategorySortTypeLikeDesc": "点赞(收藏)次数降序",
+ "CategorySortTypeSortType": "自定义排序",
+ "CategorySortTypeUpdateTimeAsc": "更新时间升序",
+ "CategorySortTypeUpdateTimeDesc": "更新时间降序",
+ "CategorySortTypeWatchAsc": "观看次数升序",
+ "CategorySortTypeWatchDesc": "观看次数降序"
+ },
+ "x-enum-varnames": [
+ "CategorySortTypeAddedTimeDesc",
+ "CategorySortTypeWatchDesc",
+ "CategorySortTypeLikeDesc",
+ "CategorySortTypeAddedTimeAsc",
+ "CategorySortTypeWatchAsc",
+ "CategorySortTypeLikeAsc",
+ "CategorySortTypeUpdateTimeDesc",
+ "CategorySortTypeUpdateTimeAsc",
+ "CategorySortTypeSortType"
+ ]
+ },
+ "pubdef.CommentObjectType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 4
+ ],
+ "x-enum-comments": {
+ "CommentObjectTypePlaylet": "短剧",
+ "CommentObjectTypeVideo": "视频"
+ },
+ "x-enum-varnames": [
+ "CommentObjectTypeVideo",
+ "CommentObjectTypePlaylet"
+ ]
+ },
+ "pubdef.DisplayMode": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "DisplayModeAvatarList": "头像列表",
+ "DisplayModeBigCover": "大封面列表(一大图两列)",
+ "DisplayModeCardCarousel": "卡片层叠轮播",
+ "DisplayModeDefault": "默认(上图下标题多列)",
+ "DisplayModeImageText": "图文列表(左图右标题多列)"
+ },
+ "x-enum-varnames": [
+ "DisplayModeDefault",
+ "DisplayModeCardCarousel",
+ "DisplayModeAvatarList",
+ "DisplayModeImageText",
+ "DisplayModeBigCover"
+ ]
+ },
+ "pubdef.FakeFlow": {
+ "type": "object",
+ "properties": {
+ "likes": {
+ "description": "假点赞数",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "假观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.Flow": {
+ "type": "object",
+ "properties": {
+ "comments": {
+ "description": "评论条数",
+ "type": "integer"
+ },
+ "likes": {
+ "description": "真实点赞数(收藏总量)",
+ "type": "integer"
+ },
+ "monthLikes": {
+ "description": "近30日收藏量",
+ "type": "integer"
+ },
+ "monthWatchs": {
+ "description": "本月观看次数",
+ "type": "integer"
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "todayWatchs": {
+ "description": "当日观看次数",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "真实观看次数",
+ "type": "integer"
+ },
+ "weekLikes": {
+ "description": "近7日收藏量",
+ "type": "integer"
+ },
+ "weekWatchs": {
+ "description": "本周观看次数",
+ "type": "integer"
+ },
+ "yearLikes": {
+ "description": "近365日收藏量",
+ "type": "integer"
+ },
+ "yearWatchs": {
+ "description": "本年观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.InformationStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "InformationStatusVerifyCancel": "下架",
+ "InformationStatusVerifyOK": "上架",
+ "InformationStatusWaiting": "初始化"
+ },
+ "x-enum-varnames": [
+ "InformationStatusWaiting",
+ "InformationStatusVerifyOK",
+ "InformationStatusVerifyCancel"
+ ]
+ },
+ "pubdef.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeMobile": "手机登陆",
+ "LoginTypeQrCode": "二维码登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeMobile",
+ "LoginTypeQrCode",
+ "LoginTypeAccount"
+ ]
+ },
+ "pubdef.Page": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.SeoValue": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "网站SEO标题",
+ "type": "string"
+ }
+ }
+ },
+ "pubdef.TranslationType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "TranslationTypeBottom": "底部导航",
+ "TranslationTypeSub": "二级导航",
+ "TranslationTypeUrl": "URL模式"
+ },
+ "x-enum-varnames": [
+ "TranslationTypeUrl",
+ "TranslationTypeSub",
+ "TranslationTypeBottom"
+ ]
+ },
+ "pubdef.UserStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "UserStatusDisable": "封禁",
+ "UserStatusNormal": "正常",
+ "UserStatusSlient": "禁言"
+ },
+ "x-enum-varnames": [
+ "UserStatusNormal",
+ "UserStatusDisable",
+ "UserStatusSlient"
+ ]
+ },
+ "questionnairemod.Attachment": {
+ "type": "object",
+ "properties": {
+ "fileType": {
+ "description": "附件类型(img / video)",
+ "type": "string"
+ },
+ "url": {
+ "description": "附件URL地址",
+ "type": "string"
+ }
+ }
+ },
+ "questionnairemod.QuestionnaireModel": {
+ "type": "object",
+ "properties": {
+ "attachments": {
+ "description": "附件列表,最多3个",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/questionnairemod.Attachment"
+ }
+ },
+ "city": {
+ "description": "所在城市(选填)",
+ "type": "string"
+ },
+ "contact": {
+ "description": "联系方式(选填)",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间(自动生成)",
+ "type": "string"
+ },
+ "description": {
+ "description": "问题描述(必填)",
+ "type": "string"
+ },
+ "deviceModel": {
+ "description": "设备型号(选填)",
+ "type": "string"
+ },
+ "deviceType": {
+ "description": "设备类型:iPhone、Android、iPad、其他",
+ "type": "string"
+ },
+ "frequency": {
+ "description": "出现频率:每次都会、经常出现、偶尔出现、仅现一次",
+ "type": "string"
+ },
+ "id": {
+ "description": "唯一标识(MongoDB自动生成)",
+ "type": "string"
+ },
+ "networkType": {
+ "description": "网络类型:WI-FI、5G、4G、其他",
+ "type": "string"
+ },
+ "operator": {
+ "description": "运营商:电信、联通、移动、广电",
+ "type": "string"
+ },
+ "province": {
+ "description": "所在省份(必填)",
+ "type": "string"
+ },
+ "questionType": {
+ "description": "问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题",
+ "type": "string"
+ },
+ "remark": {
+ "description": "备注,处理问卷时填写(选填)",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态:待处理、已处理、拒绝",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间(自动更新)",
+ "type": "string"
+ },
+ "userId": {
+ "description": "App端登录用户ID,0表示匿名",
+ "type": "integer"
+ }
+ }
+ },
+ "richtextcontentmod.RichTextContentModel": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序值",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题(内部)",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "srv_acc.OpLogInfo": {
+ "type": "object",
+ "properties": {
+ "actId": {
+ "description": "操作人Id",
+ "type": "integer"
+ },
+ "actName": {
+ "description": "操作人",
+ "type": "string"
+ },
+ "action": {
+ "description": "操作目的",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "操作时间",
+ "type": "string"
+ },
+ "param": {
+ "description": "操作内容",
+ "type": "string"
+ },
+ "path": {
+ "description": "请求接口",
+ "type": "string"
+ }
+ }
+ },
+ "srv_actress.ActressAddReq": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug,不传则按 SEO 关键词或别名自动生成",
+ "type": "string"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressDelReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressListReq": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "sortType": {
+ "description": "排序方式 0=创建时间 1=评分 2=排序值 3=作品数 4=总观看次数 5=热度值",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/actress.Actress"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_actress.ActressUpdateParam": {
+ "type": "object",
+ "properties": {
+ "alias": {
+ "description": "别名",
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "bornDate": {
+ "description": "出生日期",
+ "type": "string"
+ },
+ "cover": {
+ "description": "封面大图",
+ "type": "string"
+ },
+ "debutDate": {
+ "description": "出道日期",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "meas": {
+ "description": "三围",
+ "allOf": [
+ {
+ "$ref": "#/definitions/actress.Meas"
+ }
+ ]
+ },
+ "office": {
+ "description": "事务所",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "region": {
+ "description": "地区/国家",
+ "type": "string"
+ },
+ "score": {
+ "description": "评分",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "translationCategoryId": {
+ "description": "关联导航分类id",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "srv_actress.ActressUpdateReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "param"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "$ref": "#/definitions/srv_actress.ActressUpdateParam"
+ }
+ }
+ },
+ "srv_commend.CommendReplay": {
+ "type": "object",
+ "properties": {
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "score": {
+ "description": "评分",
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "srv_commend.CommendReviewReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "reviewStatus": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_commend.CommendSearch": {
+ "type": "object",
+ "properties": {
+ "level": {
+ "description": "评论级别",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 0=视频 4=短剧",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CommentObjectType"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "reviewStatus": {
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "srv_commend.CommendSearchResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_commend.PostCommentInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_commend.PostCommentInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "ip": {
+ "description": "评论IP",
+ "type": "string"
+ },
+ "level": {
+ "description": "评论级别",
+ "type": "integer"
+ },
+ "likes": {
+ "description": "点赞数",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectName": {
+ "description": "对象类型名称 视频/短剧",
+ "type": "string"
+ },
+ "objectType": {
+ "description": "对象类型 0=视频 4=短剧",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CommentObjectType"
+ }
+ ]
+ },
+ "parentsId": {
+ "description": "上级评论ID",
+ "type": "integer"
+ },
+ "replyUserName": {
+ "description": "回复名称",
+ "type": "string"
+ },
+ "reviewStatus": {
+ "description": "审核状态 0 未审核 1审核成功 2 审核失败",
+ "type": "integer"
+ },
+ "status": {
+ "description": "评论用户的状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "topRank": {
+ "description": "置顶排序",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "srv_contactus.DelOneContactusReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_contactus.GetContactusListReq": {
+ "type": "object",
+ "properties": {
+ "categoryName": {
+ "description": "分类",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "ip": {
+ "description": "ip",
+ "type": "string"
+ },
+ "name": {
+ "description": "姓名",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_contactus.GetContactusListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/contactusmod.ContactUsModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_contactus.UpdateContactusReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_detailnav.AddDetailNavReq": {
+ "type": "object",
+ "required": [
+ "name",
+ "url"
+ ],
+ "properties": {
+ "name": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "url": {
+ "description": "导航链接",
+ "type": "string"
+ }
+ }
+ },
+ "srv_detailnav.DelDetailNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_detailnav.GetDetailNavListReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_detailnav.GetDetailNavListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/detailnavmod.DetailNavModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_detailnav.UpdateDetailNavParam": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "sort": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ },
+ "url": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_detailnav.UpdateDetailNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "param": {
+ "$ref": "#/definitions/srv_detailnav.UpdateDetailNavParam"
+ }
+ }
+ },
+ "srv_hotnav.AddHotNavReq": {
+ "type": "object",
+ "required": [
+ "title",
+ "url"
+ ],
+ "properties": {
+ "icon": {
+ "description": "图标",
+ "type": "string"
+ },
+ "intro": {
+ "description": "介绍",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ }
+ }
+ },
+ "srv_hotnav.DelHotNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_hotnav.GetHotNavListReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_hotnav.GetHotNavListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/hotnavmod.HotNav"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_hotnav.UpdateHotNavParam": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "type": "string"
+ },
+ "intro": {
+ "type": "string"
+ },
+ "sort": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_hotnav.UpdateHotNavReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "$ref": "#/definitions/srv_hotnav.UpdateHotNavParam"
+ }
+ }
+ },
+ "srv_ipblack.AddIPBlackListReq": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "备注",
+ "type": "string"
+ },
+ "ip": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_ipblack.DelIpBlackListReq": {
+ "type": "object",
+ "properties": {
+ "ip": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_ipblack.GetIPBlackListReq": {
+ "type": "object",
+ "properties": {
+ "ip": {
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_ipblack.GetIPBlackListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ipblackmod.IPBlackModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_questionnaire.HandleReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "description": "问卷ID(ObjectID 字符串)",
+ "type": "string"
+ },
+ "remark": {
+ "description": "备注(选填)",
+ "type": "string"
+ },
+ "status": {
+ "description": "目标状态:processed 已处理 / reject 拒绝",
+ "type": "string"
+ }
+ }
+ },
+ "srv_questionnaire.ListReq": {
+ "type": "object",
+ "properties": {
+ "endTime": {
+ "description": "创建时间范围-结束,Unix 时间戳(秒,选填)",
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "questionType": {
+ "description": "模糊匹配:问题类型(选填)",
+ "type": "string"
+ },
+ "startTime": {
+ "description": "创建时间范围-开始,Unix 时间戳(秒,选填)",
+ "type": "integer"
+ },
+ "status": {
+ "description": "精确匹配:处理状态(选填)",
+ "type": "string"
+ }
+ }
+ },
+ "srv_questionnaire.ListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/questionnairemod.QuestionnaireModel"
+ }
+ },
+ "total": {
+ "description": "满足条件的总条数",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_richtextcontent.DelRichTextContentReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_richtextcontent.GetRichTextContentListReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "srv_richtextcontent.GetRichTextContentResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/richtextcontentmod.RichTextContentModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_richtextcontent.UpdateRichTextContentData": {
+ "type": "object",
+ "properties": {
+ "_id": {
+ "type": "integer"
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "desc": {
+ "description": "标题(app)",
+ "type": "string"
+ },
+ "location": {
+ "description": "位置",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序值",
+ "type": "integer"
+ },
+ "status": {
+ "description": "是否启用",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_richtextcontent.UpdateRichTextContentReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_richtextcontent.UpdateRichTextContentData"
+ }
+ ]
+ }
+ }
+ },
+ "srv_translation.TranslationInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "导航ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "transCategoryId": {
+ "description": "导航分类ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/translationmod.TranslationInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoUpdate": {
+ "type": "object",
+ "properties": {
+ "htmlId": {
+ "description": "富文本ID",
+ "type": "integer"
+ },
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "link": {
+ "description": "外部跳转",
+ "type": "string"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "transCategoryId": {
+ "type": "integer"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_translation.TranslationInfoUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_translation.TranslationInfoUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "导航分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "导航分类名称",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/translationcategorymod.TranslationCategory"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "categoryType": {
+ "description": "导航类型 1=url跳转 2=二级导航 3=底部导航",
+ "type": "integer"
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "跳转URL",
+ "type": "string"
+ }
+ }
+ },
+ "srv_translationcategory.TranslationCategoryUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_translationcategory.TranslationCategoryUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_user.UserListReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "nickName": {
+ "description": "昵称(模糊)",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ }
+ }
+ },
+ "srv_user.UserListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/usermod.User"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_user.UserUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "$ref": "#/definitions/usermod.UserUpdate"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryReq": {
+ "type": "object",
+ "properties": {
+ "displayPosition": {
+ "description": "显示位置 0=仅TAB 1=仅首页 2=两者都显示",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategoryDisplayPosition"
+ }
+ ]
+ },
+ "id": {
+ "description": "分类 ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称(模糊匹配)",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "sortType": {
+ "description": "排序方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategorySortType"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videocategorymod.VideoCategory"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryUpdate": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "displayMode": {
+ "description": "展示方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.DisplayMode"
+ }
+ ]
+ },
+ "displayPosition": {
+ "description": "显示位置 0=仅TAB 1=仅首页 2=两者都显示",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategoryDisplayPosition"
+ }
+ ]
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sortType": {
+ "description": "排序方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategorySortType"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "tags": {
+ "description": "关联视频标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "translationCategoryId": {
+ "description": "导航分类",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videocollection.CollectionMediaInfo": {
+ "type": "object",
+ "properties": {
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "nodes": {
+ "description": "节点",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.Node"
+ }
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videocollection.GetCollectionMediaListReq": {
+ "type": "object",
+ "properties": {
+ "collectionID": {
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocollection.GetCollectionMediaListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_videocollection.CollectionMediaInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocollection.UpdateCollectionMediaRankReq": {
+ "type": "object",
+ "properties": {
+ "collectionID": {
+ "description": "合集ID",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "videoId": {
+ "description": "视频ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocollection.VideoCollectionOptionMediaReq": {
+ "type": "object",
+ "properties": {
+ "collectionID": {
+ "description": "合集ID",
+ "type": "integer"
+ },
+ "isAdd": {
+ "description": "增加还是移除",
+ "type": "boolean"
+ },
+ "videoIds": {
+ "description": "视频ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videocreatermod.VideoCreaterInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "background": {
+ "description": "背景",
+ "type": "string"
+ },
+ "createType": {
+ "description": "创作者类型 0=普通 1=官方",
+ "type": "integer"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "image": {
+ "description": "头像",
+ "type": "string"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.AddVideoCollectionReq": {
+ "type": "object",
+ "properties": {
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "firstTag": {
+ "description": "一级标签",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ }
+ }
+ },
+ "srv_videoinfo.AddVideoReq": {
+ "type": "object",
+ "properties": {
+ "fileId": {
+ "description": "资源id",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "srv_videoinfo.SetFakeCountResp": {
+ "type": "object",
+ "properties": {
+ "matchedCount": {
+ "description": "匹配到的视频数量",
+ "type": "integer"
+ },
+ "modifiedCount": {
+ "description": "实际修改的视频数量",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SetFakeLikesReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,支持单个或批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "likesMax": {
+ "description": "假点赞数随机最大值,包含该值",
+ "type": "integer"
+ },
+ "likesMin": {
+ "description": "假点赞数随机最小值,包含该值",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SetFakeWatchTimesReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,支持单个或批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "watchTimesMax": {
+ "description": "假观看次数随机最大值,包含该值",
+ "type": "integer"
+ },
+ "watchTimesMin": {
+ "description": "假观看次数随机最小值,包含该值",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SetRecommendReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ }
+ }
+ },
+ "srv_videoinfo.SyncMediaFileItem": {
+ "type": "object",
+ "properties": {
+ "bango": {
+ "type": "string"
+ },
+ "coverImg": {
+ "type": "string"
+ },
+ "fieldNameFs": {
+ "type": "string"
+ },
+ "firstTag": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "string"
+ },
+ "keyworld": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "publish": {
+ "type": "string"
+ },
+ "secondTags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "taskId": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "videoTime": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.SyncMediaFileReq": {
+ "type": "object",
+ "properties": {
+ "fileId": {
+ "description": "媒资库影片 ID",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "firstTagMap": {
+ "description": "覆盖一级标签",
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "minVideoSeconds": {
+ "description": "最短时长,0 表示不限制",
+ "type": "integer"
+ },
+ "sourceVersion": {
+ "description": "v1 / v2",
+ "type": "string"
+ },
+ "videoList": {
+ "description": "媒资库搜索结果,优先使用",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_videoinfo.SyncMediaFileItem"
+ }
+ }
+ }
+ },
+ "srv_videoinfo.VideoAddTagsReq": {
+ "type": "object",
+ "required": [
+ "tags",
+ "videoIds"
+ ],
+ "properties": {
+ "tags": {
+ "description": "统一标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "videoIds": {
+ "description": "视频ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoReq": {
+ "type": "object",
+ "properties": {
+ "actressIds": {
+ "description": "女优ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "bango": {
+ "description": "番号,",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "创建时间-截止",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "string"
+ },
+ "mediaType": {
+ "description": "资源类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "optionAccount": {
+ "description": "最后操作人",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "sort": {
+ "description": "排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数。按分类查询且排序方式为排序值时,使用该分类的 categoryRanks 排序",
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "创建时间-开始",
+ "type": "string"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "tag": {
+ "description": "标签",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "videoFileId": {
+ "description": "切片id(切片列表的ID)",
+ "type": "string"
+ },
+ "videoTaskId": {
+ "description": "视频任务id",
+ "type": "string"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.VideoInformation"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoUpdate": {
+ "type": "object",
+ "properties": {
+ "actressIds": {
+ "description": "女优ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "addedTime": {
+ "type": "string"
+ },
+ "bango": {
+ "description": "番号,唯一",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "firstTag": {
+ "description": "一级标签",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "issuedAt": {
+ "description": "发行时间",
+ "type": "string"
+ },
+ "issuedBrand": {
+ "description": "发行商",
+ "type": "string"
+ },
+ "keyworld": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "mediaType": {
+ "description": "资源类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "nodes": {
+ "description": "节点",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.Node"
+ }
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secondUpdatedAt": {
+ "description": "二次修改时间",
+ "type": "string"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "unencryptedImage": {
+ "description": "不加密封面",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videoinfo.VideoInfoUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videoinfo.VideoSetTagsReq": {
+ "type": "object",
+ "required": [
+ "ids"
+ ],
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagIds": {
+ "description": "标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "srv_videotag.HotTagItem": {
+ "type": "object",
+ "properties": {
+ "tagId": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "watchTimes": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.HotTagResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_videotag.HotTagItem"
+ }
+ }
+ }
+ },
+ "srv_videotag.IpStatReq": {
+ "type": "object",
+ "properties": {
+ "todayIP": {
+ "type": "integer"
+ },
+ "yesterdayIP": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videotagmod.VideoTagInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "images": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "translationcategorymod.TranslationCategory": {
+ "type": "object",
+ "properties": {
+ "categoryType": {
+ "description": "导航类型 1:url 2:二级导航 3:底部导航",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.TranslationType"
+ }
+ ]
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "跳转URL",
+ "type": "string"
+ }
+ }
+ },
+ "translationmod.TranslationInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "htmlId": {
+ "description": "富文本ID",
+ "type": "integer"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "link": {
+ "description": "外部跳转",
+ "type": "string"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "transCategoryId": {
+ "description": "导航分类",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "usermod.User": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "age": {
+ "description": "年龄",
+ "type": "integer"
+ },
+ "agentInviteCode": {
+ "description": "被邀请的inviteCode",
+ "type": "string"
+ },
+ "appVer": {
+ "description": "app版本",
+ "type": "string"
+ },
+ "auth": {
+ "description": "是否认证",
+ "type": "boolean"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "backgroud": {
+ "description": "背景图",
+ "type": "string"
+ },
+ "consecutiveLogin": {
+ "description": "连续登陆天数",
+ "type": "integer"
+ },
+ "consumptionTotal": {
+ "description": "金币总消费数量",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "currentVipExpire": {
+ "description": "当前vip过期时间",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示",
+ "type": "string"
+ },
+ "devV2": {
+ "description": "设备唯一设备ID",
+ "type": "string"
+ },
+ "device": {
+ "description": "设备",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "商区码",
+ "type": "string"
+ },
+ "gender": {
+ "description": "性别 0-未知,1-男,2-女",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "introduction": {
+ "description": "简介",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "invites": {
+ "description": "我邀请的人的个数(我邀请他人)",
+ "type": "integer"
+ },
+ "isRegister": {
+ "description": "是否已注册",
+ "type": "boolean"
+ },
+ "lastIp": {
+ "description": "上次登陆IP",
+ "type": "string"
+ },
+ "lastLoginAt": {
+ "description": "上次登陆时间",
+ "type": "string"
+ },
+ "loginType": {
+ "description": "登陆方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "mac": {
+ "description": "设备唯一mac地址",
+ "type": "string"
+ },
+ "modifyUserNameAt": {
+ "description": "上一次修改用户时间",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oriVer": {
+ "description": "注册时的app版本",
+ "type": "string"
+ },
+ "password": {
+ "description": "登陆密码",
+ "type": "string"
+ },
+ "recharge": {
+ "description": "当前充值余额,金币个数",
+ "type": "integer"
+ },
+ "rechargeTotal": {
+ "description": "累计充值额,金币个数",
+ "type": "integer"
+ },
+ "regIp": {
+ "description": "注册IP",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区",
+ "type": "string"
+ },
+ "registerTime": {
+ "type": "string"
+ },
+ "score": {
+ "description": "积分",
+ "type": "integer"
+ },
+ "scoreInvites": {
+ "description": "邀请人获得的积分累计",
+ "type": "integer"
+ },
+ "scoreLvName": {
+ "description": "积分等级名称",
+ "type": "string"
+ },
+ "scoreTotal": {
+ "description": "累计积分",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ },
+ "sysMsgAt": {
+ "description": "获取系统消息时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "系统类型 ios android",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "usermod.UserUpdate": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "age": {
+ "description": "年龄",
+ "type": "integer"
+ },
+ "agentInviteCode": {
+ "description": "被邀请的inviteCode",
+ "type": "string"
+ },
+ "appVer": {
+ "description": "app版本",
+ "type": "string"
+ },
+ "auth": {
+ "description": "是否认证",
+ "type": "boolean"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "backgroud": {
+ "description": "背景图",
+ "type": "string"
+ },
+ "consecutiveLogin": {
+ "description": "连续登陆天数",
+ "type": "integer"
+ },
+ "consumptionTotal": {
+ "description": "金币总消费数量",
+ "type": "integer"
+ },
+ "currentVipExpire": {
+ "description": "当前vip过期时间",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示",
+ "type": "string"
+ },
+ "devV2": {
+ "description": "设备唯一设备ID",
+ "type": "string"
+ },
+ "device": {
+ "description": "设备",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "商区码",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "gender": {
+ "description": "性别 0-未知,1-男,2-女",
+ "type": "integer"
+ },
+ "introduction": {
+ "description": "简介",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "invites": {
+ "description": "我邀请的人的个数(我邀请他人)",
+ "type": "integer"
+ },
+ "isRegister": {
+ "description": "是否已注册",
+ "type": "boolean"
+ },
+ "lastIp": {
+ "description": "上次登陆IP",
+ "type": "string"
+ },
+ "lastLoginAt": {
+ "description": "上次登陆时间",
+ "type": "string"
+ },
+ "loginType": {
+ "description": "登陆方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "mac": {
+ "description": "设备唯一mac地址",
+ "type": "string"
+ },
+ "merchantExpiredAt": {
+ "description": "商家/兼职有效期",
+ "type": "string"
+ },
+ "modifyUserNameAt": {
+ "description": "上一次修改用户时间",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oriVer": {
+ "description": "注册时的app版本",
+ "type": "string"
+ },
+ "password": {
+ "description": "登陆密码",
+ "type": "string"
+ },
+ "recharge": {
+ "description": "当前充值余额,金币个数",
+ "type": "integer"
+ },
+ "rechargeTotal": {
+ "description": "累计充值额,金币个数",
+ "type": "integer"
+ },
+ "regIp": {
+ "description": "注册IP",
+ "type": "string"
+ },
+ "region": {
+ "description": "地区",
+ "type": "string"
+ },
+ "registerTime": {
+ "type": "string"
+ },
+ "score": {
+ "description": "积分",
+ "type": "integer"
+ },
+ "scoreInvites": {
+ "description": "邀请人获得的积分累计",
+ "type": "integer"
+ },
+ "scoreLvName": {
+ "description": "积分等级名称",
+ "type": "string"
+ },
+ "scoreTotal": {
+ "description": "累计积分",
+ "type": "integer"
+ },
+ "showCardExpiredAt": {
+ "description": "最新会员卡过期时间",
+ "type": "string"
+ },
+ "showCardId": {
+ "description": "最新会员卡id",
+ "type": "integer"
+ },
+ "showCardName": {
+ "description": "最新会员卡名称",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ }
+ ]
+ },
+ "sysMsgAt": {
+ "description": "获取系统消息时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "系统类型 ios android",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videocategorymod.VideoCategory": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "displayMode": {
+ "description": "展示方式 0:默认 1:卡片层叠轮播 2:头像列表 3:图文列表 4:大封面列表",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.DisplayMode"
+ }
+ ]
+ },
+ "displayPosition": {
+ "description": "显示位置 0=仅TAB 1=仅首页 2=两者都显示",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategoryDisplayPosition"
+ }
+ ]
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sortType": {
+ "description": "排序方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.CategorySortType"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "tags": {
+ "description": "关联视频标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "translationCategoryId": {
+ "description": "导航分类",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videocreatermod.VideoCreaterInfo": {
+ "type": "object",
+ "properties": {
+ "background": {
+ "description": "背景",
+ "type": "string"
+ },
+ "createType": {
+ "description": "创作者类型 0=普通 1=官方",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "image": {
+ "description": "头像",
+ "type": "string"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "name": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.HevcStatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ -1
+ ],
+ "x-enum-comments": {
+ "HevcStatusFailed": "转码失败",
+ "HevcStatusNone": "未提交转码",
+ "HevcStatusPending": "转码中",
+ "HevcStatusSuccess": "转码成功"
+ },
+ "x-enum-varnames": [
+ "HevcStatusNone",
+ "HevcStatusPending",
+ "HevcStatusSuccess",
+ "HevcStatusFailed"
+ ]
+ },
+ "videoinformationmod.MediaType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "MediaType_AV": "AV视频",
+ "MediaType_Advitise": "广告",
+ "MediaType_Collection": "合集",
+ "MediaType_Video": "视频"
+ },
+ "x-enum-varnames": [
+ "MediaType_Video",
+ "MediaType_Collection",
+ "MediaType_Advitise",
+ "MediaType_AV"
+ ]
+ },
+ "videoinformationmod.Node": {
+ "type": "object",
+ "properties": {
+ "images": {
+ "description": "图片",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "nodeId": {
+ "description": "节点ID",
+ "type": "string"
+ },
+ "text": {
+ "description": "文本",
+ "type": "string"
+ },
+ "title": {
+ "description": "节点标题",
+ "type": "string"
+ },
+ "url": {
+ "description": "链接",
+ "type": "string"
+ },
+ "videos": {
+ "description": "视频信息列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ }
+ }
+ },
+ "videoinformationmod.VideoInfo": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "fileId": {
+ "description": "媒资库ID",
+ "type": "string"
+ },
+ "h265Url": {
+ "description": "H.265 m3u8 播放地址,空字符串表示暂无该编码源",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "H.264 m3u8 播放地址(始终存在)",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.VideoInformation": {
+ "type": "object",
+ "properties": {
+ "actressIds": {
+ "description": "女优ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "audioFile": {
+ "description": "音频文件路径",
+ "type": "string"
+ },
+ "bango": {
+ "description": "番号,唯一",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "desc": {
+ "description": "详细描述",
+ "type": "string"
+ },
+ "fake": {
+ "description": "伪造流量",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.FakeFlow"
+ }
+ ]
+ },
+ "firstTag": {
+ "description": "一级标签",
+ "type": "string"
+ },
+ "hevcFailCount": {
+ "description": "主视频转码累计失败次数",
+ "type": "integer"
+ },
+ "hevcMetaInfo": {
+ "description": "H265 视频元信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoMetaInfo"
+ }
+ ]
+ },
+ "hevcStatus": {
+ "description": "主视频 H265 转码状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.HevcStatus"
+ }
+ ]
+ },
+ "id": {
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐 false-不推荐",
+ "type": "boolean"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "issuedAt": {
+ "description": "发行时间",
+ "type": "string"
+ },
+ "issuedBrand": {
+ "description": "发行商",
+ "type": "string"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "mediaType": {
+ "description": "资源类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "nodes": {
+ "description": "节点",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videoinformationmod.Node"
+ }
+ },
+ "optionAccount": {
+ "description": "最后操作人",
+ "type": "string"
+ },
+ "pageUpdatedAt": {
+ "description": "页面展示更新时间",
+ "type": "string"
+ },
+ "previewVtt": {
+ "description": "雪碧图 vtt",
+ "type": "string"
+ },
+ "real": {
+ "description": "真实流量",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.Flow"
+ }
+ ]
+ },
+ "realUpdateAt": {
+ "description": "播放相关数据更新时间",
+ "type": "string"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secondUpdatedAt": {
+ "description": "二次修改时间",
+ "type": "string"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "smartCover": {
+ "description": "智能封面",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.InformationStatus"
+ }
+ ]
+ },
+ "syncId": {
+ "description": "同步ID",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "unencryptedImage": {
+ "description": "不加密封面",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "uploadUserName": {
+ "description": "上传用户名",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.VideoInfo"
+ }
+ ]
+ },
+ "videoCount": {
+ "description": "合集视频数量",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.VideoMetaInfo": {
+ "type": "object",
+ "properties": {
+ "audioBitrate": {
+ "type": "integer"
+ },
+ "audioChannels": {
+ "type": "integer"
+ },
+ "audioCodec": {
+ "type": "string"
+ },
+ "audioSampleRate": {
+ "type": "integer"
+ },
+ "bitrate": {
+ "type": "integer"
+ },
+ "codec": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "number"
+ },
+ "fps": {
+ "type": "number"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "m3u8Url": {
+ "type": "string"
+ },
+ "pixFmt": {
+ "type": "string"
+ },
+ "profile": {
+ "type": "string"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "videoBitrate": {
+ "type": "integer"
+ },
+ "width": {
+ "type": "integer"
+ }
+ }
+ },
+ "videotagmod.VideoTagInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "images": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "isShowHome": {
+ "description": "是否显示在首页",
+ "type": "boolean"
+ },
+ "mediaTypes": {
+ "description": "作者关联的资源类型 1视频",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "watchTimes": {
+ "description": "预计算:该标签下所有视频的累计播放量",
+ "type": "integer"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.AccOpLogReq": {
+ "type": "object",
+ "properties": {
+ "actId": {
+ "description": "操作人Id",
+ "type": "integer"
+ },
+ "action": {
+ "description": "操作目的",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "开始时间",
+ "type": "string"
+ },
+ "pageNum": {
+ "description": "页码",
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "path": {
+ "description": "请求接口",
+ "type": "string"
+ },
+ "startTime": {
+ "description": "开始时间",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.AccOpLogResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_acc.OpLogInfo"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.BindQurlReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.BindQurlResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MainWebLoginReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ },
+ "code": {
+ "description": "验证码",
+ "type": "string"
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MainWebLoginResp": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ },
+ "accId": {
+ "description": "账户id",
+ "type": "integer"
+ },
+ "qurl": {
+ "description": "二维码",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "roleId": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "token": {
+ "description": "token",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccAddReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账户",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色ID",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccAddResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwAccDelReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccDelResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwAccGetInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccGetInfoResp": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账号",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccGetStatusReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccGetStatusResp": {
+ "type": "object",
+ "properties": {
+ "feedBack": {
+ "description": "禁用原因",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccListReq": {
+ "type": "object",
+ "properties": {
+ "keyWords": {
+ "description": "搜索条件",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态 1正常 2 禁用",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccListResp": {
+ "type": "object",
+ "properties": {
+ "accRec": {
+ "description": "当前页配置记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.MwAccRec"
+ }
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwAccRec": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账号",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "integer"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "feedBack": {
+ "description": "禁用原因",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isBase32": {
+ "description": "// 二步签名\nBase32 string `json:\"base32\"`\n是否已签名",
+ "type": "boolean"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "roleName": {
+ "description": "角色名",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccSaveStatusReq": {
+ "type": "object",
+ "properties": {
+ "feedBack": {
+ "description": "禁用原因",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccSaveStatusResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwAccUpPwdReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccUpReq": {
+ "type": "object",
+ "properties": {
+ "acc": {
+ "description": "账号",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "id": {
+ "description": "账号Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "使能",
+ "type": "boolean"
+ },
+ "limitIP": {
+ "description": "允许访问的IP",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "pwd": {
+ "description": "密码",
+ "type": "string"
+ },
+ "roleId": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwAccUpResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleAddReq": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "isEnb": {
+ "description": "状态",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleAddResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleDelReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleDelResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleDrop": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "name": {
+ "description": "角色名称",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleDropResp": {
+ "type": "object",
+ "properties": {
+ "roleDrop": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.MwRoleDrop"
+ }
+ }
+ }
+ },
+ "web_acc.MwRoleEnableReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleEnableResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleGetAuthReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleGetAuthResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "router": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router"
+ }
+ }
+ }
+ },
+ "web_acc.MwRoleGetInfoReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleGetInfoResp": {
+ "type": "object",
+ "properties": {
+ "roleRec": {
+ "$ref": "#/definitions/web_acc.MwRoleRec"
+ }
+ }
+ },
+ "web_acc.MwRoleListResp": {
+ "type": "object",
+ "properties": {
+ "roleRecs": {
+ "description": "当前页配置记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.MwRoleRec"
+ }
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "web_acc.MwRoleRec": {
+ "type": "object",
+ "properties": {
+ "apiMenus": {
+ "description": "已有权限的api",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "角色Id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "是否启用 false:禁用 true:启用",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "角色名称",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleUpAuthReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "router": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router"
+ }
+ }
+ }
+ },
+ "web_acc.MwRoleUpAuthResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.MwRoleUpReq": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "角色id",
+ "type": "integer"
+ },
+ "isEnb": {
+ "description": "状态",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.MwRoleUpResp": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "description": "是否成功 true:成功 false:未成功",
+ "type": "boolean"
+ }
+ }
+ },
+ "web_acc.Router": {
+ "type": "object",
+ "properties": {
+ "apiUrl": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router_ApiUrl"
+ }
+ },
+ "children": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/web_acc.Router"
+ }
+ },
+ "component": {
+ "type": "string"
+ },
+ "hidden": {
+ "type": "boolean"
+ },
+ "isShow": {
+ "type": "boolean"
+ },
+ "meta": {
+ "$ref": "#/definitions/web_acc.Router_Meta"
+ },
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ },
+ "redirect": {
+ "type": "string"
+ }
+ }
+ },
+ "web_acc.Router_ApiUrl": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "type": "string"
+ },
+ "method": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "status": {
+ "type": "boolean"
+ },
+ "url": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "web_acc.Router_Meta": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "type": "string"
+ },
+ "noCache": {
+ "type": "boolean"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
+ "web_cfg.GetSysConfigReq": {
+ "type": "object"
+ },
+ "web_cfg.GetSysConfigResp": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "dhDescription": {
+ "description": "导航SEO描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "导航SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhNewUrlAddr1": {
+ "description": "导航页-线路1",
+ "type": "string"
+ },
+ "dhNewUrlAddr2": {
+ "description": "导航页-线路2",
+ "type": "string"
+ },
+ "dhNewUrlAddr3": {
+ "description": "导航页-线路3",
+ "type": "string"
+ },
+ "dhSiteName": {
+ "description": "导航SEO网站名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航SEO标题",
+ "type": "string"
+ },
+ "downloadVideoUrl": {
+ "description": "下载视频URL",
+ "type": "string"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "freeVPN": {
+ "description": "免费vpn链接",
+ "type": "string"
+ },
+ "gitAddr": {
+ "description": "git地址",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "imgAppCdn": {
+ "description": "图片CDN(app使用)",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgWebCdn": {
+ "description": "图片CDN(web使用)",
+ "type": "string"
+ },
+ "m3u8DayRateLimitMax": {
+ "description": "每 IP 每天最多不同正片数,\u003c=0 使用默认 80",
+ "type": "integer"
+ },
+ "mediaAppCdn": {
+ "description": "视频CDN(app使用)",
+ "type": "string"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "mediaWebCdn": {
+ "description": "视频CDN(web使用)",
+ "type": "string"
+ },
+ "mp4Addr": {
+ "description": "mp4地址(web使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "网站管理",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "potaoGroup": {
+ "description": "土豆群",
+ "type": "string"
+ },
+ "qq": {
+ "description": "qq群",
+ "type": "string"
+ },
+ "sitemapDefaultImages": {
+ "description": "Sitemap里面默认封面图",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "telegramAdd": {
+ "description": "加入我们tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ },
+ "unencryptedCdn": {
+ "description": "不加密资源使用的cdn",
+ "type": "string"
+ },
+ "useAddress": {
+ "description": "所有使用中的域名",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "version": {
+ "description": "版本号,用于更新乐观锁",
+ "type": "integer"
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部右边)",
+ "type": "string"
+ },
+ "watermarkMediaBottomLeft": {
+ "description": "视频水印文件(底部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "description": "视频水印文件(顶部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTopRight": {
+ "description": "视频水印文件(顶部右边)",
+ "type": "string"
+ },
+ "webAddr": {
+ "description": "网站SEO网站地址",
+ "type": "string"
+ },
+ "webDescription": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "webKeyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webSiteName": {
+ "description": "网站SEO网站名称",
+ "type": "string"
+ },
+ "webTitle": {
+ "description": "SEO",
+ "type": "string"
+ },
+ "weiChart": {
+ "description": "微信群",
+ "type": "string"
+ }
+ }
+ },
+ "web_cfg.SysConfigUpdateReq": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "dhDescription": {
+ "description": "导航SEO描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "导航SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhNewUrlAddr1": {
+ "description": "导航页-线路一",
+ "type": "string"
+ },
+ "dhNewUrlAddr2": {
+ "description": "导航页-线路2",
+ "type": "string"
+ },
+ "dhNewUrlAddr3": {
+ "description": "导航页-线路3",
+ "type": "string"
+ },
+ "dhSiteName": {
+ "description": "导航SEO网站名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航SEO标题",
+ "type": "string"
+ },
+ "downloadVideoUrl": {
+ "description": "下载视频URL",
+ "type": "string"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "gitAddr": {
+ "description": "git地址",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "imgAppCdn": {
+ "description": "图片CDN(app使用)",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgWebCdn": {
+ "description": "图片CDN(web使用)",
+ "type": "string"
+ },
+ "m3u8DayRateLimitMax": {
+ "description": "每 IP 每天最多不同正片数,\u003c=0 使用默认 80",
+ "type": "integer"
+ },
+ "mediaAppCdn": {
+ "description": "视频CDN(app使用)",
+ "type": "string"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "mediaWebCdn": {
+ "description": "视频CDN(web使用)",
+ "type": "string"
+ },
+ "mp4Addr": {
+ "description": "mp4地址(web使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "最新地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "potaoGroup": {
+ "description": "土豆群",
+ "type": "string"
+ },
+ "qq": {
+ "description": "qq群",
+ "type": "string"
+ },
+ "sitemapDefaultImages": {
+ "description": "Sitemap里面默认封面图",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "telegramAdd": {
+ "description": "加入我们tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "translationAddr": {
+ "description": "应用导航",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ },
+ "unencryptedCdn": {
+ "description": "不加密资源使用的cdn",
+ "type": "string"
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部右边)",
+ "type": "string"
+ },
+ "watermarkMediaBottomLeft": {
+ "description": "视频水印文件(底部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "description": "视频水印文件(顶部左边)",
+ "type": "string"
+ },
+ "watermarkMediaTopRight": {
+ "description": "视频水印文件(顶部右边)",
+ "type": "string"
+ },
+ "webAddr": {
+ "description": "网站SEO网站地址",
+ "type": "string"
+ },
+ "webDescription": {
+ "description": "网站SEO描述",
+ "type": "string"
+ },
+ "webKeyworld": {
+ "description": "网站SEO关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "webSiteName": {
+ "description": "网站SEO网站名称",
+ "type": "string"
+ },
+ "webTitle": {
+ "description": "SEO",
+ "type": "string"
+ },
+ "weiChart": {
+ "description": "微信群",
+ "type": "string"
+ }
+ }
+ },
+ "web_cfg.SysConfigUpdateResp": {
+ "type": "object"
+ },
+ "web_commend.CommendDelIpReq": {
+ "type": "object",
+ "properties": {
+ "ip": {
+ "type": "string"
+ }
+ }
+ },
+ "web_commend.CommendDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "web_commend.CommendReviewBatchReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "reviewStatus": {
+ "type": "integer"
+ }
+ }
+ },
+ "web_file.UploadImageResp": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ }
+ },
+ "web_file.UploadImagesResp": {
+ "type": "object",
+ "properties": {
+ "paths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "ApiKeyAuth": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ },
+ "BasicAuth": {
+ "type": "basic"
+ },
+ "OAuth2AccessCode": {
+ "type": "oauth2",
+ "flow": "accessCode",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information"
+ }
+ },
+ "OAuth2Application": {
+ "type": "oauth2",
+ "flow": "application",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Implicit": {
+ "type": "oauth2",
+ "flow": "implicit",
+ "authorizationUrl": "https://example.com/oauth/authorize",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "write": "Grants write access"
+ }
+ },
+ "OAuth2Password": {
+ "type": "oauth2",
+ "flow": "password",
+ "tokenUrl": "https://example.com/oauth/token",
+ "scopes": {
+ "admin": "Grants read and write access to administrative information",
+ "read": "Grants read access",
+ "write": "Grants write access"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/docs/swagger.yaml b/web/docs/swagger.yaml
new file mode 100644
index 0000000..a0e5109
--- /dev/null
+++ b/web/docs/swagger.yaml
@@ -0,0 +1,8867 @@
+basePath: /
+definitions:
+ actress.Actress:
+ properties:
+ alias:
+ description: 别名
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ bornDate:
+ description: 出生日期
+ type: string
+ cover:
+ description: 封面大图
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ debutDate:
+ description: 出道日期
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ hotValue:
+ description: 热度值
+ type: integer
+ id:
+ description: Id
+ type: integer
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ meas:
+ allOf:
+ - $ref: '#/definitions/actress.Meas'
+ description: 三围
+ office:
+ description: 事务所
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ real:
+ allOf:
+ - $ref: '#/definitions/pubdef.Flow'
+ description: 真实流量
+ realUpdateAt:
+ description: 播放相关数据更新时间
+ type: string
+ region:
+ description: 地区/国家
+ type: string
+ score:
+ description: 评分
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ status:
+ description: 是否启用
+ type: boolean
+ translationCategoryId:
+ description: 关联导航分类id
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ works:
+ description: 作品数
+ type: integer
+ type: object
+ actress.Meas:
+ properties:
+ blood:
+ description: 血型
+ type: string
+ bust:
+ description: 胸围 英寸
+ type: integer
+ cup:
+ description: 罩杯(A:1-G:9)1表示A,2表示B,3表示C,4表示D,5表示E,6表示F,7表示G,8表示H,9表示I
+ type: integer
+ dia:
+ description: 直径 毫米(前端可以显示为厘米,如45毫米,显示为4.5厘米,返回后端时,乘以10,保留整数,返回45)
+ type: integer
+ gender:
+ description: 性别 0:女 1:男 2:其他
+ type: integer
+ height:
+ description: 身高 厘米
+ type: integer
+ hip:
+ description: 臀围 英寸
+ type: integer
+ len:
+ description: 长度 毫米(前端可以显示为厘米,如145毫米,显示为14.5厘米,返回后端时,乘以10,保留整数,返回145)
+ type: integer
+ waist:
+ description: 腰围 英寸
+ type: integer
+ weight:
+ description: 体重 公斤
+ type: integer
+ type: object
+ contactusmod.ContactUsModel:
+ properties:
+ categoryName:
+ description: 主旨分类
+ type: string
+ content:
+ description: 内容
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ email:
+ description: 邮件
+ type: string
+ id:
+ description: ID
+ type: string
+ ip:
+ description: 评论IP
+ type: string
+ name:
+ description: 名字
+ type: string
+ status:
+ description: 状态 0=初始 1=已回复 2=不予处理 3=已完结
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ contenttagmod.ContentTagType:
+ enum:
+ - 1
+ - 5
+ type: integer
+ x-enum-comments:
+ ContentTagTypePlaylet: 短剧
+ ContentTagTypeVideo: 视频
+ x-enum-varnames:
+ - ContentTagTypeVideo
+ - ContentTagTypePlaylet
+ detailnavmod.DetailNavModel:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ type: string
+ name:
+ description: 导航名称
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ updatedAt:
+ description: 更新时间
+ type: string
+ url:
+ description: 导航链接
+ type: string
+ type: object
+ hotnavmod.HotNav:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ icon:
+ description: 图标
+ type: string
+ id:
+ description: ID
+ type: integer
+ intro:
+ description: 介绍
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ title:
+ description: 标题
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ url:
+ description: 链接
+ type: string
+ type: object
+ ipblackmod.IPBlackModel:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 备注
+ type: string
+ id:
+ type: string
+ ip:
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ playletchaptermod.PayType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ PayTypeCoin: 金币
+ PayTypeFree: 免费
+ PayTypeVip: VIP
+ x-enum-varnames:
+ - PayTypeFree
+ - PayTypeCoin
+ - PayTypeVip
+ playletchaptermod.WatermarkStatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ type: integer
+ x-enum-comments:
+ WatermarkStatusDone: 已完成
+ WatermarkStatusFailed: 处理失败
+ WatermarkStatusPending: 未处理(待 mediafile/sync)
+ WatermarkStatusProcessing: 处理中(已提交加水印任务,待回写)
+ WatermarkStatusRegistered: 已登记(原片已登记,待 task/add)
+ x-enum-varnames:
+ - WatermarkStatusPending
+ - WatermarkStatusRegistered
+ - WatermarkStatusProcessing
+ - WatermarkStatusDone
+ - WatermarkStatusFailed
+ playletmod.ContentType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ type: integer
+ x-enum-comments:
+ ContentTypeAlbum: 普通短剧/影集(对应 media_type=6)
+ ContentTypeAnime: 动漫(对应 media_type=1)
+ ContentTypeComic: 漫画短剧(对应 media_type=4)
+ ContentTypeManga: 漫画(对应 media_type=2)
+ ContentTypeTheater: 短剧(对应 media_type=5)
+ x-enum-varnames:
+ - ContentTypeTheater
+ - ContentTypeAlbum
+ - ContentTypeComic
+ - ContentTypeAnime
+ - ContentTypeManga
+ playletmod.PayType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ PayTypeCoin: 金币
+ PayTypeFree: 免费
+ PayTypeVip: VIP
+ x-enum-varnames:
+ - PayTypeFree
+ - PayTypeCoin
+ - PayTypeVip
+ playletmod.Status:
+ enum:
+ - 0
+ - 1
+ type: integer
+ x-enum-comments:
+ StatusDown: 下架
+ StatusUp: 上架
+ x-enum-varnames:
+ - StatusDown
+ - StatusUp
+ proto.ContentTagAddReq:
+ properties:
+ name:
+ description: 标签名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,不传则按 SEO 关键词或名称自动生成
+ type: string
+ status:
+ description: 状态 true-启用 false-停用
+ type: boolean
+ types:
+ description: 支持类型 1-视频 5-短剧
+ items:
+ $ref: '#/definitions/contenttagmod.ContentTagType'
+ type: array
+ required:
+ - name
+ - status
+ - types
+ type: object
+ proto.ContentTagAddResp:
+ properties:
+ id:
+ description: ID
+ type: integer
+ type: object
+ proto.ContentTagDelReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ required:
+ - id
+ type: object
+ proto.ContentTagListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.ContentTagOptionItem:
+ properties:
+ bindCount:
+ description: 当前查询类型的绑定数量,不传type时返回0
+ type: integer
+ id:
+ description: ID
+ type: integer
+ name:
+ description: 标签名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ typeNames:
+ description: 支持类型名称
+ items:
+ type: string
+ type: array
+ types:
+ description: 支持类型 1-视频 5-短剧
+ items:
+ $ref: '#/definitions/contenttagmod.ContentTagType'
+ type: array
+ type: object
+ proto.ContentTagOptionResp:
+ properties:
+ list:
+ description: 标签下拉列表
+ items:
+ $ref: '#/definitions/proto.ContentTagOptionItem'
+ type: array
+ type: object
+ proto.ContentTagUpdateReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ name:
+ description: 标签名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,历史数据为空时更新会自动生成
+ type: string
+ status:
+ description: 状态 true-启用 false-停用
+ type: boolean
+ types:
+ description: 支持类型 1-视频 5-短剧
+ items:
+ $ref: '#/definitions/contenttagmod.ContentTagType'
+ type: array
+ required:
+ - id
+ type: object
+ proto.HomeBannerAddReq:
+ properties:
+ image:
+ description: 图片
+ type: string
+ name:
+ description: 名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ status:
+ description: 状态 true-启用 false-停用
+ type: boolean
+ url:
+ description: URL
+ type: string
+ verticalImage:
+ description: 竖版图片
+ type: string
+ required:
+ - image
+ - name
+ - status
+ type: object
+ proto.HomeBannerAddResp:
+ properties:
+ id:
+ description: ID
+ type: integer
+ type: object
+ proto.HomeBannerDelReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ required:
+ - id
+ type: object
+ proto.HomeBannerListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.HomeBannerUpdateReq:
+ properties:
+ id:
+ description: ID
+ type: integer
+ image:
+ description: 图片
+ type: string
+ name:
+ description: 名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ status:
+ description: 状态 true-启用 false-停用
+ type: boolean
+ url:
+ description: URL
+ type: string
+ verticalImage:
+ description: 竖版图片
+ type: string
+ required:
+ - id
+ type: object
+ proto.PlayletAddReq:
+ properties:
+ anchor:
+ description: 作者/演员
+ type: string
+ banner:
+ description: banner图
+ type: string
+ categoryIds:
+ description: 分类ID集合
+ items:
+ type: integer
+ type: array
+ contentType:
+ allOf:
+ - $ref: '#/definitions/playletmod.ContentType'
+ description: 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ cover:
+ description: 封面原图
+ type: string
+ desc:
+ description: 简介
+ type: string
+ isFeatured:
+ description: 是否爆剧,true-是、false-否
+ type: boolean
+ isRecommend:
+ description: 是否推荐,true-推荐、false-不推荐
+ type: boolean
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式,0-免费、1-金币、2-VIP
+ preview:
+ description: 预告
+ type: string
+ recommendRank:
+ description: 推荐排序
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ showOnBanner:
+ description: 是否显示在首页banner
+ type: boolean
+ singleCoins:
+ description: 单集金币,支付方式为1-金币时生效
+ type: integer
+ slug:
+ description: slug,不传则按 SEO 关键词或标题自动生成
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 上架状态,0-下架、1-上架
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ title:
+ description: 标题
+ type: string
+ topRank:
+ description: 置顶排序
+ type: integer
+ totalEpisode:
+ description: 总集数
+ type: integer
+ required:
+ - title
+ type: object
+ proto.PlayletAddResp:
+ properties:
+ id:
+ description: 短剧ID
+ type: integer
+ type: object
+ proto.PlayletBannerSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ showOnBanner:
+ description: 是否显示在首页banner,true-显示、false-不显示
+ type: boolean
+ type: object
+ proto.PlayletCategoryAddReq:
+ properties:
+ icon:
+ description: 分类图标
+ type: string
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ remark:
+ description: 备注
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,不传则按 SEO 关键词或名称自动生成
+ type: string
+ status:
+ description: 启用状态
+ type: boolean
+ required:
+ - name
+ type: object
+ proto.PlayletCategoryAddResp:
+ properties:
+ id:
+ description: 分类ID
+ type: integer
+ type: object
+ proto.PlayletCategoryDelReq:
+ properties:
+ id:
+ description: 分类ID
+ type: integer
+ required:
+ - id
+ type: object
+ proto.PlayletCategoryListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.PlayletCategorySetReq:
+ properties:
+ categoryIds:
+ description: 分类ID集合,可设置多个分类,传空数组表示清空分类,不传表示参数错误
+ items:
+ type: integer
+ type: array
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ type: object
+ proto.PlayletCategoryUpdateReq:
+ properties:
+ icon:
+ description: 分类图标
+ type: string
+ id:
+ description: 分类ID
+ type: integer
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ remark:
+ description: 备注
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,历史数据为空时更新会自动生成
+ type: string
+ status:
+ description: 启用状态
+ type: boolean
+ required:
+ - id
+ type: object
+ proto.PlayletChapterAddReq:
+ properties:
+ currentEpisode:
+ description: 第几集
+ type: integer
+ hash:
+ description: 媒体hash
+ type: string
+ height:
+ description: 视频高度
+ type: integer
+ md5:
+ description: 媒体md5
+ type: string
+ mediaSize:
+ description: 资源大小
+ type: integer
+ mediaSourceId:
+ description: media切片服原片ID,上传正片已创建原片时直接沿用
+ type: string
+ mediaTime:
+ description: 影片长度,单位秒
+ type: integer
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletchaptermod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ playletId:
+ description: 短剧ID
+ type: integer
+ ratio:
+ description: 宽高比
+ type: number
+ singleCoins:
+ description: 单集金币,支付方式为1-金币时生效
+ type: integer
+ title:
+ description: 剧集标题
+ type: string
+ videoUrl:
+ description: 原片视频地址
+ type: string
+ width:
+ description: 视频宽度
+ type: integer
+ required:
+ - currentEpisode
+ - playletId
+ type: object
+ proto.PlayletChapterAddResp:
+ properties:
+ id:
+ description: 剧集ID
+ type: integer
+ type: object
+ proto.PlayletChapterDelReq:
+ properties:
+ ids:
+ description: 剧集ID集合,传1个表示单条删除,传多个表示批量删除
+ items:
+ type: integer
+ type: array
+ type: object
+ proto.PlayletChapterListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.PlayletChapterPayTypeSetReq:
+ properties:
+ ids:
+ description: 剧集ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletchaptermod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ singleCoins:
+ description: 单集金币,支付方式为1-金币时必填且需大于0
+ type: integer
+ type: object
+ proto.PlayletChapterSyncReq:
+ properties:
+ playletIds:
+ description: 短剧ID集合,传1个表示单条同步,传多个表示批量同步
+ items:
+ type: integer
+ type: array
+ required:
+ - playletIds
+ type: object
+ proto.PlayletChapterSyncResp:
+ properties:
+ failedCount:
+ description: 失败数量
+ type: integer
+ list:
+ description: 同步结果集合
+ items:
+ $ref: '#/definitions/proto.PlayletChapterSyncResult'
+ type: array
+ successCount:
+ description: 成功数量
+ type: integer
+ total:
+ description: 同步总数
+ type: integer
+ type: object
+ proto.PlayletChapterSyncResult:
+ properties:
+ newChapterCount:
+ description: 本次新增剧集数量
+ type: integer
+ playletId:
+ description: 短剧ID
+ type: integer
+ reason:
+ description: 失败原因
+ type: string
+ success:
+ description: 是否同步成功
+ type: boolean
+ type: object
+ proto.PlayletChapterUpdateReq:
+ properties:
+ currentEpisode:
+ description: 第几集
+ type: integer
+ hash:
+ description: 媒体hash
+ type: string
+ height:
+ description: 视频高度
+ type: integer
+ id:
+ description: 剧集ID
+ type: integer
+ md5:
+ description: 媒体md5
+ type: string
+ mediaSize:
+ description: 资源大小
+ type: integer
+ mediaTime:
+ description: 影片长度,单位秒
+ type: integer
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletchaptermod.PayType'
+ description: 支付方式 0-免费 1-金币 2-VIP
+ ratio:
+ description: 宽高比
+ type: number
+ singleCoins:
+ description: 单集金币,支付方式为1-金币时生效
+ type: integer
+ title:
+ description: 剧集标题
+ type: string
+ videoUrl:
+ description: 原片视频地址
+ type: string
+ width:
+ description: 视频宽度
+ type: integer
+ required:
+ - id
+ type: object
+ proto.PlayletFeaturedSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ isFeatured:
+ description: 是否爆剧,true-是、false-否
+ type: boolean
+ type: object
+ proto.PlayletListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.PlayletPayTypeSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式,0-免费、1-金币、2-VIP
+ singleCoins:
+ description: 单集金币,支付方式为1-金币时必填且需大于0
+ type: integer
+ type: object
+ proto.PlayletRecommendSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ isRecommend:
+ description: 是否推荐,true-推荐、false-不推荐
+ type: boolean
+ type: object
+ proto.PlayletResourceChapterItem:
+ properties:
+ cover:
+ description: 封面
+ type: string
+ createTime:
+ description: 创建时间
+ type: integer
+ episodeNumber:
+ description: 第几集
+ type: integer
+ hashId:
+ description: 资源hash
+ type: string
+ height:
+ description: 高
+ type: integer
+ id:
+ description: 媒资内容ID
+ type: integer
+ mediaSize:
+ description: 资源大小
+ type: integer
+ playTime:
+ description: 播放时长(秒)
+ type: integer
+ saleCoin:
+ description: 单集售价
+ type: integer
+ title:
+ description: 章节名
+ type: string
+ updateTime:
+ description: 更新时间
+ type: integer
+ videoUrl:
+ description: 播放地址
+ type: string
+ width:
+ description: 宽
+ type: integer
+ type: object
+ proto.PlayletResourceChapterListResp:
+ properties:
+ list:
+ description: 剧集列表
+ items:
+ $ref: '#/definitions/proto.PlayletResourceChapterItem'
+ type: array
+ mediaId:
+ description: 媒资中心资源ID
+ type: integer
+ title:
+ description: 短剧标题
+ type: string
+ total:
+ description: 剧集数量
+ type: integer
+ type: object
+ proto.PlayletResourceListItem:
+ properties:
+ anchor:
+ description: 作者
+ type: string
+ create_time:
+ description: 创建时间
+ type: integer
+ data_version:
+ description: 数据版本
+ type: integer
+ horizontal_cover:
+ description: 横版封面
+ type: string
+ id:
+ type: integer
+ isAdd:
+ description: 是否已加入后台数据
+ type: boolean
+ media_type:
+ description: 媒体类型
+ type: integer
+ sale_coin:
+ description: 整部售价
+ type: integer
+ summary:
+ description: 简介
+ type: string
+ tags_text:
+ description: 标签文字
+ type: string
+ terminal_cdo:
+ description: 上传者
+ type: string
+ title:
+ description: 标题
+ type: string
+ total_episode:
+ description: 总集数
+ type: integer
+ update_status:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ update_time:
+ description: 修改时间
+ type: integer
+ vertical_cover:
+ description: 竖版封面
+ type: string
+ vlogger_id:
+ type: integer
+ type: object
+ proto.PlayletResourceListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.PlayletResourceListItem'
+ type: array
+ total:
+ type: integer
+ type: object
+ proto.PlayletResourceUseReq:
+ properties:
+ ids:
+ description: 媒资中心资源ID集合
+ items:
+ type: integer
+ type: array
+ type: object
+ proto.PlayletStatusSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 上架状态,0-下架、1-上架
+ type: object
+ proto.PlayletTagAddReq:
+ properties:
+ name:
+ type: string
+ rank:
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ status:
+ type: boolean
+ required:
+ - name
+ type: object
+ proto.PlayletTagAddResp:
+ properties:
+ id:
+ type: integer
+ type: object
+ proto.PlayletTagDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ proto.PlayletTagListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.PlayletTagSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ tagIds:
+ description: 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
+ items:
+ type: integer
+ type: array
+ type: object
+ proto.PlayletTagUpdateReq:
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ rank:
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ status:
+ type: boolean
+ required:
+ - id
+ type: object
+ proto.PlayletThemeAddReq:
+ properties:
+ icon:
+ description: 题材图标
+ type: string
+ name:
+ description: 题材名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,不传则按 SEO 关键词或名称自动生成
+ type: string
+ status:
+ description: 上架状态
+ type: boolean
+ required:
+ - name
+ type: object
+ proto.PlayletThemeAddResp:
+ properties:
+ id:
+ description: 题材ID
+ type: integer
+ type: object
+ proto.PlayletThemeDelReq:
+ properties:
+ id:
+ description: 题材ID
+ type: integer
+ required:
+ - id
+ type: object
+ proto.PlayletThemeListPageResp:
+ properties:
+ list:
+ description: 当前页数据
+ pageNum:
+ description: 当前页码
+ type: integer
+ pageSize:
+ description: 每页数量
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.PlayletThemeSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ themeIds:
+ description: 题材ID集合,可设置多个题材
+ items:
+ type: integer
+ type: array
+ type: object
+ proto.PlayletThemeUpdateReq:
+ properties:
+ icon:
+ description: 题材图标
+ type: string
+ id:
+ description: 题材ID
+ type: integer
+ name:
+ description: 题材名称
+ type: string
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,历史数据为空时更新会自动生成
+ type: string
+ status:
+ description: 上架状态
+ type: boolean
+ required:
+ - id
+ type: object
+ proto.PlayletUpdateReq:
+ properties:
+ anchor:
+ description: 作者/演员
+ type: string
+ banner:
+ description: banner图
+ type: string
+ categoryIds:
+ description: 分类ID集合,传空数组表示清空分类
+ items:
+ type: integer
+ type: array
+ contentType:
+ allOf:
+ - $ref: '#/definitions/playletmod.ContentType'
+ description: 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ cover:
+ description: 封面原图
+ type: string
+ desc:
+ description: 简介
+ type: string
+ id:
+ description: 短剧ID
+ type: integer
+ isFeatured:
+ description: 是否爆剧,true-是、false-否
+ type: boolean
+ isRecommend:
+ description: 是否推荐,true-推荐、false-不推荐
+ type: boolean
+ payType:
+ allOf:
+ - $ref: '#/definitions/playletmod.PayType'
+ description: 支付方式,0-免费、1-金币、2-VIP
+ preview:
+ description: 预告
+ type: string
+ recommendRank:
+ description: 推荐排序
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ showOnBanner:
+ description: 是否显示在首页banner
+ type: boolean
+ singleCoins:
+ description: 单集金币,支付方式为1-金币时生效
+ type: integer
+ slug:
+ description: slug,历史数据为空时更新会自动生成
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/playletmod.Status'
+ description: 上架状态,0-下架、1-上架
+ tagIds:
+ description: 标签ID集合,传空数组表示清空标签
+ items:
+ type: integer
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ title:
+ description: 标题
+ type: string
+ topRank:
+ description: 置顶排序
+ type: integer
+ totalEpisode:
+ description: 总集数
+ type: integer
+ required:
+ - id
+ type: object
+ proto.PlayletVirtualLikeSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ maxVirtualCountLike:
+ description: 虚拟点赞数最大值,必须大于等于最小值
+ type: integer
+ minVirtualCountLike:
+ description: 虚拟点赞数最小值,必须大于等于0
+ type: integer
+ type: object
+ proto.PlayletWatermarkResetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条,传多个表示批量
+ items:
+ type: integer
+ type: array
+ type: object
+ proto.SlugSyncReq:
+ properties:
+ maxSize:
+ description: 本次最多处理的空 slug 记录数,默认500,最大2000
+ type: integer
+ type:
+ description: 可选,指定类型;空则按顺序处理全部
+ type: string
+ type: object
+ proto.SlugSyncResp:
+ properties:
+ actressUpdated:
+ description: 本次补齐的女优数
+ type: integer
+ contentTagUpdated:
+ description: 本次补齐的统一标签数
+ type: integer
+ hasMore:
+ description: 是否还有未处理的历史数据
+ type: boolean
+ playletCategoryUpdated:
+ description: 本次补齐的短剧分类数
+ type: integer
+ playletThemeUpdated:
+ description: 本次补齐的短剧主题数
+ type: integer
+ playletUpdated:
+ description: 本次补齐的短剧数
+ type: integer
+ totalUpdated:
+ description: 本次合计补齐数
+ type: integer
+ videoCategoryUpdated:
+ description: 本次补齐的视频分类数
+ type: integer
+ videoCreaterUpdated:
+ description: 本次补齐的作者数
+ type: integer
+ videoUpdated:
+ description: 本次补齐的视频数
+ type: integer
+ type: object
+ pubdef.CategoryDisplayPosition:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ CategoryDisplayPositionBoth: TAB和首页都显示
+ CategoryDisplayPositionHomeOnly: 仅首页
+ CategoryDisplayPositionTabOnly: 仅TAB
+ x-enum-varnames:
+ - CategoryDisplayPositionTabOnly
+ - CategoryDisplayPositionHomeOnly
+ - CategoryDisplayPositionBoth
+ pubdef.CategorySortType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ type: integer
+ x-enum-comments:
+ CategorySortTypeAddedTimeAsc: 上架时间升序
+ CategorySortTypeAddedTimeDesc: 上架时间降序
+ CategorySortTypeLikeAsc: 点赞(收藏)次数升序
+ CategorySortTypeLikeDesc: 点赞(收藏)次数降序
+ CategorySortTypeSortType: 自定义排序
+ CategorySortTypeUpdateTimeAsc: 更新时间升序
+ CategorySortTypeUpdateTimeDesc: 更新时间降序
+ CategorySortTypeWatchAsc: 观看次数升序
+ CategorySortTypeWatchDesc: 观看次数降序
+ x-enum-varnames:
+ - CategorySortTypeAddedTimeDesc
+ - CategorySortTypeWatchDesc
+ - CategorySortTypeLikeDesc
+ - CategorySortTypeAddedTimeAsc
+ - CategorySortTypeWatchAsc
+ - CategorySortTypeLikeAsc
+ - CategorySortTypeUpdateTimeDesc
+ - CategorySortTypeUpdateTimeAsc
+ - CategorySortTypeSortType
+ pubdef.CommentObjectType:
+ enum:
+ - 0
+ - 4
+ type: integer
+ x-enum-comments:
+ CommentObjectTypePlaylet: 短剧
+ CommentObjectTypeVideo: 视频
+ x-enum-varnames:
+ - CommentObjectTypeVideo
+ - CommentObjectTypePlaylet
+ pubdef.DisplayMode:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ type: integer
+ x-enum-comments:
+ DisplayModeAvatarList: 头像列表
+ DisplayModeBigCover: 大封面列表(一大图两列)
+ DisplayModeCardCarousel: 卡片层叠轮播
+ DisplayModeDefault: 默认(上图下标题多列)
+ DisplayModeImageText: 图文列表(左图右标题多列)
+ x-enum-varnames:
+ - DisplayModeDefault
+ - DisplayModeCardCarousel
+ - DisplayModeAvatarList
+ - DisplayModeImageText
+ - DisplayModeBigCover
+ pubdef.FakeFlow:
+ properties:
+ likes:
+ description: 假点赞数
+ type: integer
+ watchTimes:
+ description: 假观看次数
+ type: integer
+ type: object
+ pubdef.Flow:
+ properties:
+ comments:
+ description: 评论条数
+ type: integer
+ likes:
+ description: 真实点赞数(收藏总量)
+ type: integer
+ monthLikes:
+ description: 近30日收藏量
+ type: integer
+ monthWatchs:
+ description: 本月观看次数
+ type: integer
+ showTimes:
+ description: 展示次数
+ type: integer
+ todayWatchs:
+ description: 当日观看次数
+ type: integer
+ watchTimes:
+ description: 真实观看次数
+ type: integer
+ weekLikes:
+ description: 近7日收藏量
+ type: integer
+ weekWatchs:
+ description: 本周观看次数
+ type: integer
+ yearLikes:
+ description: 近365日收藏量
+ type: integer
+ yearWatchs:
+ description: 本年观看次数
+ type: integer
+ type: object
+ pubdef.InformationStatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ InformationStatusVerifyCancel: 下架
+ InformationStatusVerifyOK: 上架
+ InformationStatusWaiting: 初始化
+ x-enum-varnames:
+ - InformationStatusWaiting
+ - InformationStatusVerifyOK
+ - InformationStatusVerifyCancel
+ pubdef.LoginType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ LoginTypeAccount: 账号登陆
+ LoginTypeDevice: 设备登陆
+ LoginTypeMobile: 手机登陆
+ LoginTypeQrCode: 二维码登陆
+ x-enum-varnames:
+ - LoginTypeDevice
+ - LoginTypeMobile
+ - LoginTypeQrCode
+ - LoginTypeAccount
+ pubdef.Page:
+ properties:
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ pubdef.SeoValue:
+ properties:
+ description:
+ description: 网站SEO描述
+ type: string
+ keyworld:
+ description: 网站SEO关键字
+ items:
+ type: string
+ type: array
+ title:
+ description: 网站SEO标题
+ type: string
+ type: object
+ pubdef.TranslationType:
+ enum:
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ TranslationTypeBottom: 底部导航
+ TranslationTypeSub: 二级导航
+ TranslationTypeUrl: URL模式
+ x-enum-varnames:
+ - TranslationTypeUrl
+ - TranslationTypeSub
+ - TranslationTypeBottom
+ pubdef.UserStatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ UserStatusDisable: 封禁
+ UserStatusNormal: 正常
+ UserStatusSlient: 禁言
+ x-enum-varnames:
+ - UserStatusNormal
+ - UserStatusDisable
+ - UserStatusSlient
+ questionnairemod.Attachment:
+ properties:
+ fileType:
+ description: 附件类型(img / video)
+ type: string
+ url:
+ description: 附件URL地址
+ type: string
+ type: object
+ questionnairemod.QuestionnaireModel:
+ properties:
+ attachments:
+ description: 附件列表,最多3个
+ items:
+ $ref: '#/definitions/questionnairemod.Attachment'
+ type: array
+ city:
+ description: 所在城市(选填)
+ type: string
+ contact:
+ description: 联系方式(选填)
+ type: string
+ createdAt:
+ description: 创建时间(自动生成)
+ type: string
+ description:
+ description: 问题描述(必填)
+ type: string
+ deviceModel:
+ description: 设备型号(选填)
+ type: string
+ deviceType:
+ description: 设备类型:iPhone、Android、iPad、其他
+ type: string
+ frequency:
+ description: 出现频率:每次都会、经常出现、偶尔出现、仅现一次
+ type: string
+ id:
+ description: 唯一标识(MongoDB自动生成)
+ type: string
+ networkType:
+ description: 网络类型:WI-FI、5G、4G、其他
+ type: string
+ operator:
+ description: 运营商:电信、联通、移动、广电
+ type: string
+ province:
+ description: 所在省份(必填)
+ type: string
+ questionType:
+ description: 问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题
+ type: string
+ remark:
+ description: 备注,处理问卷时填写(选填)
+ type: string
+ status:
+ description: 处理状态:待处理、已处理、拒绝
+ type: string
+ updatedAt:
+ description: 更新时间(自动更新)
+ type: string
+ userId:
+ description: App端登录用户ID,0表示匿名
+ type: integer
+ type: object
+ richtextcontentmod.RichTextContentModel:
+ properties:
+ content:
+ description: 内容
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 标题(app)
+ type: string
+ id:
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sort:
+ description: 排序值
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ title:
+ description: 标题(内部)
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ srv_acc.OpLogInfo:
+ properties:
+ actId:
+ description: 操作人Id
+ type: integer
+ actName:
+ description: 操作人
+ type: string
+ action:
+ description: 操作目的
+ type: string
+ createdAt:
+ description: 操作时间
+ type: string
+ param:
+ description: 操作内容
+ type: string
+ path:
+ description: 请求接口
+ type: string
+ type: object
+ srv_actress.ActressAddReq:
+ properties:
+ alias:
+ description: 别名
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ bornDate:
+ description: 出生日期
+ type: string
+ cover:
+ description: 封面大图
+ type: string
+ debutDate:
+ description: 出道日期
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ hotValue:
+ description: 热度值
+ type: integer
+ meas:
+ allOf:
+ - $ref: '#/definitions/actress.Meas'
+ description: 三围
+ office:
+ description: 事务所
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ region:
+ description: 地区/国家
+ type: string
+ score:
+ description: 评分
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug,不传则按 SEO 关键词或别名自动生成
+ type: string
+ status:
+ description: 是否启用
+ type: boolean
+ translationCategoryId:
+ description: 关联导航分类id
+ type: integer
+ type: object
+ srv_actress.ActressDelReq:
+ properties:
+ id:
+ type: integer
+ type: object
+ srv_actress.ActressListReq:
+ properties:
+ alias:
+ description: 别名
+ type: string
+ id:
+ description: Id
+ type: integer
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ region:
+ description: 地区/国家
+ type: string
+ sortType:
+ description: 排序方式 0=创建时间 1=评分 2=排序值 3=作品数 4=总观看次数 5=热度值
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ translationCategoryId:
+ description: 关联导航分类id
+ type: integer
+ type: object
+ srv_actress.ActressListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/actress.Actress'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_actress.ActressUpdateParam:
+ properties:
+ alias:
+ description: 别名
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ bornDate:
+ description: 出生日期
+ type: string
+ cover:
+ description: 封面大图
+ type: string
+ debutDate:
+ description: 出道日期
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ hotValue:
+ description: 热度值
+ type: integer
+ meas:
+ allOf:
+ - $ref: '#/definitions/actress.Meas'
+ description: 三围
+ office:
+ description: 事务所
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ region:
+ description: 地区/国家
+ type: string
+ score:
+ description: 评分
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ status:
+ description: 是否启用
+ type: boolean
+ translationCategoryId:
+ description: 关联导航分类id
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ srv_actress.ActressUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ $ref: '#/definitions/srv_actress.ActressUpdateParam'
+ required:
+ - id
+ - param
+ type: object
+ srv_commend.CommendReplay:
+ properties:
+ objectId:
+ description: 对象ID
+ type: integer
+ parentsId:
+ description: 评论ID
+ type: integer
+ score:
+ description: 评分
+ type: integer
+ text:
+ description: 评论内容
+ type: string
+ userName:
+ description: 用户名
+ type: string
+ type: object
+ srv_commend.CommendReviewReq:
+ properties:
+ id:
+ type: integer
+ reviewStatus:
+ type: integer
+ type: object
+ srv_commend.CommendSearch:
+ properties:
+ level:
+ description: 评论级别
+ type: integer
+ objectId:
+ description: 对象ID
+ type: integer
+ objectType:
+ allOf:
+ - $ref: '#/definitions/pubdef.CommentObjectType'
+ description: 对象类型 0=视频 4=短剧
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ reviewStatus:
+ type: integer
+ text:
+ description: 评论内容
+ type: string
+ userName:
+ description: 用户名称
+ type: string
+ type: object
+ srv_commend.CommendSearchResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/srv_commend.PostCommentInfo'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_commend.PostCommentInfo:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ description: 评论ID
+ type: integer
+ ip:
+ description: 评论IP
+ type: string
+ level:
+ description: 评论级别
+ type: integer
+ likes:
+ description: 点赞数
+ type: integer
+ objectId:
+ description: 对象ID
+ type: integer
+ objectName:
+ description: 对象类型名称 视频/短剧
+ type: string
+ objectType:
+ allOf:
+ - $ref: '#/definitions/pubdef.CommentObjectType'
+ description: 对象类型 0=视频 4=短剧
+ parentsId:
+ description: 上级评论ID
+ type: integer
+ replyUserName:
+ description: 回复名称
+ type: string
+ reviewStatus:
+ description: 审核状态 0 未审核 1审核成功 2 审核失败
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserStatus'
+ description: 评论用户的状态
+ text:
+ description: 评论内容
+ type: string
+ topRank:
+ description: 置顶排序
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ userName:
+ description: 用户名称
+ type: string
+ type: object
+ srv_contactus.DelOneContactusReq:
+ properties:
+ id:
+ type: string
+ type: object
+ srv_contactus.GetContactusListReq:
+ properties:
+ categoryName:
+ description: 分类
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ ip:
+ description: ip
+ type: string
+ name:
+ description: 姓名
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_contactus.GetContactusListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/contactusmod.ContactUsModel'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_contactus.UpdateContactusReq:
+ properties:
+ id:
+ type: string
+ status:
+ type: integer
+ type: object
+ srv_detailnav.AddDetailNavReq:
+ properties:
+ name:
+ description: 导航名称
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ url:
+ description: 导航链接
+ type: string
+ required:
+ - name
+ - url
+ type: object
+ srv_detailnav.DelDetailNavReq:
+ properties:
+ id:
+ type: string
+ required:
+ - id
+ type: object
+ srv_detailnav.GetDetailNavListReq:
+ properties:
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_detailnav.GetDetailNavListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/detailnavmod.DetailNavModel'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_detailnav.UpdateDetailNavParam:
+ properties:
+ name:
+ type: string
+ sort:
+ type: integer
+ status:
+ type: boolean
+ url:
+ type: string
+ type: object
+ srv_detailnav.UpdateDetailNavReq:
+ properties:
+ id:
+ type: string
+ param:
+ $ref: '#/definitions/srv_detailnav.UpdateDetailNavParam'
+ required:
+ - id
+ type: object
+ srv_hotnav.AddHotNavReq:
+ properties:
+ icon:
+ description: 图标
+ type: string
+ intro:
+ description: 介绍
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ title:
+ description: 标题
+ type: string
+ url:
+ description: 链接
+ type: string
+ required:
+ - title
+ - url
+ type: object
+ srv_hotnav.DelHotNavReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_hotnav.GetHotNavListReq:
+ properties:
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_hotnav.GetHotNavListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/hotnavmod.HotNav'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_hotnav.UpdateHotNavParam:
+ properties:
+ icon:
+ type: string
+ intro:
+ type: string
+ sort:
+ type: integer
+ status:
+ type: boolean
+ title:
+ type: string
+ url:
+ type: string
+ type: object
+ srv_hotnav.UpdateHotNavReq:
+ properties:
+ id:
+ type: integer
+ param:
+ $ref: '#/definitions/srv_hotnav.UpdateHotNavParam'
+ required:
+ - id
+ type: object
+ srv_ipblack.AddIPBlackListReq:
+ properties:
+ desc:
+ description: 备注
+ type: string
+ ip:
+ type: string
+ type: object
+ srv_ipblack.DelIpBlackListReq:
+ properties:
+ ip:
+ type: string
+ type: object
+ srv_ipblack.GetIPBlackListReq:
+ properties:
+ ip:
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_ipblack.GetIPBlackListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/ipblackmod.IPBlackModel'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_questionnaire.HandleReq:
+ properties:
+ id:
+ description: 问卷ID(ObjectID 字符串)
+ type: string
+ remark:
+ description: 备注(选填)
+ type: string
+ status:
+ description: 目标状态:processed 已处理 / reject 拒绝
+ type: string
+ required:
+ - id
+ - status
+ type: object
+ srv_questionnaire.ListReq:
+ properties:
+ endTime:
+ description: 创建时间范围-结束,Unix 时间戳(秒,选填)
+ type: integer
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ questionType:
+ description: 模糊匹配:问题类型(选填)
+ type: string
+ startTime:
+ description: 创建时间范围-开始,Unix 时间戳(秒,选填)
+ type: integer
+ status:
+ description: 精确匹配:处理状态(选填)
+ type: string
+ type: object
+ srv_questionnaire.ListResp:
+ properties:
+ list:
+ description: 当前页数据
+ items:
+ $ref: '#/definitions/questionnairemod.QuestionnaireModel'
+ type: array
+ total:
+ description: 满足条件的总条数
+ type: integer
+ type: object
+ srv_richtextcontent.DelRichTextContentReq:
+ properties:
+ id:
+ type: integer
+ type: object
+ srv_richtextcontent.GetRichTextContentListReq:
+ properties:
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ title:
+ description: 标题
+ type: string
+ type: object
+ srv_richtextcontent.GetRichTextContentResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/richtextcontentmod.RichTextContentModel'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_richtextcontent.UpdateRichTextContentData:
+ properties:
+ _id:
+ type: integer
+ content:
+ description: 内容
+ type: string
+ desc:
+ description: 标题(app)
+ type: string
+ location:
+ description: 位置
+ items:
+ type: string
+ type: array
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sort:
+ description: 排序值
+ type: integer
+ status:
+ description: 是否启用
+ type: boolean
+ title:
+ description: 标题
+ type: string
+ updatedAt:
+ type: string
+ type: object
+ srv_richtextcontent.UpdateRichTextContentReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_richtextcontent.UpdateRichTextContentData'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_translation.TranslationInfoDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_translation.TranslationInfoReq:
+ properties:
+ id:
+ description: 导航ID
+ type: integer
+ name:
+ description: 导航名称
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ transCategoryId:
+ description: 导航分类ID
+ type: integer
+ type: object
+ srv_translation.TranslationInfoResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/translationmod.TranslationInfo'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_translation.TranslationInfoUpdate:
+ properties:
+ htmlId:
+ description: 富文本ID
+ type: integer
+ isFriendAds:
+ description: 是否是友链广告位
+ type: boolean
+ link:
+ description: 外部跳转
+ type: string
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ transCategoryId:
+ type: integer
+ updatedAt:
+ type: string
+ type: object
+ srv_translation.TranslationInfoUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_translation.TranslationInfoUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_translationcategory.TranslationCategoryDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_translationcategory.TranslationCategoryReq:
+ properties:
+ id:
+ description: 导航分类ID
+ type: integer
+ name:
+ description: 导航分类名称
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_translationcategory.TranslationCategoryResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/translationcategorymod.TranslationCategory'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_translationcategory.TranslationCategoryUpdate:
+ properties:
+ appId:
+ description: appId,唯一id
+ type: integer
+ categoryType:
+ description: 导航类型 1=url跳转 2=二级导航 3=底部导航
+ type: integer
+ icon:
+ description: Icon
+ type: string
+ name:
+ description: 名称
+ type: string
+ rank:
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ updatedAt:
+ type: string
+ urlValue:
+ description: 跳转URL
+ type: string
+ type: object
+ srv_translationcategory.TranslationCategoryUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_translationcategory.TranslationCategoryUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_user.UserListReq:
+ properties:
+ id:
+ description: 用户ID
+ type: integer
+ nickName:
+ description: 昵称(模糊)
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserStatus'
+ description: 状态
+ type: object
+ srv_user.UserListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/usermod.User'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_user.UserUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ $ref: '#/definitions/usermod.UserUpdate'
+ required:
+ - id
+ type: object
+ srv_videocategory.VideoCategoryDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videocategory.VideoCategoryReq:
+ properties:
+ displayPosition:
+ allOf:
+ - $ref: '#/definitions/pubdef.CategoryDisplayPosition'
+ description: 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ id:
+ description: 分类 ID
+ type: integer
+ name:
+ description: 分类名称(模糊匹配)
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ sortType:
+ allOf:
+ - $ref: '#/definitions/pubdef.CategorySortType'
+ description: 排序方式
+ type: object
+ srv_videocategory.VideoCategoryResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/videocategorymod.VideoCategory'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videocategory.VideoCategoryUpdate:
+ properties:
+ desc:
+ description: 描述
+ type: string
+ displayMode:
+ allOf:
+ - $ref: '#/definitions/pubdef.DisplayMode'
+ description: 展示方式
+ displayPosition:
+ allOf:
+ - $ref: '#/definitions/pubdef.CategoryDisplayPosition'
+ description: 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ indexNow:
+ type: integer
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ rank:
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ sortType:
+ allOf:
+ - $ref: '#/definitions/pubdef.CategorySortType'
+ description: 排序方式
+ status:
+ description: 上架状态
+ type: boolean
+ tags:
+ description: 关联视频标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: APP显示
+ type: string
+ translationCategoryId:
+ description: 导航分类
+ type: integer
+ updatedAt:
+ type: string
+ type: object
+ srv_videocollection.CollectionMediaInfo:
+ properties:
+ coverImg:
+ description: 封面
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ id:
+ type: integer
+ nodes:
+ description: 节点
+ items:
+ $ref: '#/definitions/videoinformationmod.Node'
+ type: array
+ rank:
+ description: 排序
+ type: integer
+ remark:
+ description: 备注
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.InformationStatus'
+ description: 帖子上架状态
+ title:
+ description: 帖子标题
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ video:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.VideoInfo'
+ description: 视频
+ type: object
+ srv_videocollection.GetCollectionMediaListReq:
+ properties:
+ collectionID:
+ type: integer
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_videocollection.GetCollectionMediaListResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/srv_videocollection.CollectionMediaInfo'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videocollection.UpdateCollectionMediaRankReq:
+ properties:
+ collectionID:
+ description: 合集ID
+ type: integer
+ rank:
+ description: 排序
+ type: integer
+ videoId:
+ description: 视频ID
+ type: integer
+ type: object
+ srv_videocollection.VideoCollectionOptionMediaReq:
+ properties:
+ collectionID:
+ description: 合集ID
+ type: integer
+ isAdd:
+ description: 增加还是移除
+ type: boolean
+ videoIds:
+ description: 视频ID
+ items:
+ type: integer
+ type: array
+ type: object
+ srv_videocreater.VideoCreaterDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videocreater.VideoCreaterReq:
+ properties:
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_videocreater.VideoCreaterResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/videocreatermod.VideoCreaterInfo'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videocreater.VideoCreaterUpdate:
+ properties:
+ appId:
+ description: appId,唯一id
+ type: integer
+ background:
+ description: 背景
+ type: string
+ createType:
+ description: 创作者类型 0=普通 1=官方
+ type: integer
+ desc:
+ description: 简介
+ type: string
+ image:
+ description: 头像
+ type: string
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ mediaTypes:
+ description: 作者关联的资源类型 1视频
+ items:
+ type: integer
+ type: array
+ name:
+ description: 名称
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ updatedAt:
+ type: string
+ works:
+ description: 作品数量
+ type: integer
+ type: object
+ srv_videoinfo.AddVideoCollectionReq:
+ properties:
+ coverImg:
+ description: 封面
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ firstTag:
+ description: 一级标签
+ type: string
+ keyworld:
+ description: 关键字
+ items:
+ type: string
+ type: array
+ remark:
+ description: 备注
+ type: string
+ secondTags:
+ description: 二级标签标签
+ items:
+ type: string
+ type: array
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: 帖子标题
+ type: string
+ type: object
+ srv_videoinfo.AddVideoReq:
+ properties:
+ fileId:
+ description: 资源id
+ items:
+ type: string
+ type: array
+ type: object
+ srv_videoinfo.SetFakeCountResp:
+ properties:
+ matchedCount:
+ description: 匹配到的视频数量
+ type: integer
+ modifiedCount:
+ description: 实际修改的视频数量
+ type: integer
+ type: object
+ srv_videoinfo.SetFakeLikesReq:
+ properties:
+ ids:
+ description: 视频ID集合,支持单个或批量
+ items:
+ type: integer
+ type: array
+ likesMax:
+ description: 假点赞数随机最大值,包含该值
+ type: integer
+ likesMin:
+ description: 假点赞数随机最小值,包含该值
+ type: integer
+ type: object
+ srv_videoinfo.SetFakeWatchTimesReq:
+ properties:
+ ids:
+ description: 视频ID集合,支持单个或批量
+ items:
+ type: integer
+ type: array
+ watchTimesMax:
+ description: 假观看次数随机最大值,包含该值
+ type: integer
+ watchTimesMin:
+ description: 假观看次数随机最小值,包含该值
+ type: integer
+ type: object
+ srv_videoinfo.SetRecommendReq:
+ properties:
+ ids:
+ description: 视频ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ type: object
+ srv_videoinfo.SyncMediaFileItem:
+ properties:
+ bango:
+ type: string
+ coverImg:
+ type: string
+ fieldNameFs:
+ type: string
+ firstTag:
+ type: string
+ height:
+ type: integer
+ id:
+ type: string
+ keyworld:
+ items:
+ type: string
+ type: array
+ publish:
+ type: string
+ secondTags:
+ items:
+ type: string
+ type: array
+ taskId:
+ type: string
+ title:
+ type: string
+ videoTime:
+ type: integer
+ width:
+ type: integer
+ type: object
+ srv_videoinfo.SyncMediaFileReq:
+ properties:
+ fileId:
+ description: 媒资库影片 ID
+ items:
+ type: string
+ type: array
+ firstTagMap:
+ additionalProperties:
+ type: string
+ description: 覆盖一级标签
+ type: object
+ minVideoSeconds:
+ description: 最短时长,0 表示不限制
+ type: integer
+ sourceVersion:
+ description: v1 / v2
+ type: string
+ videoList:
+ description: 媒资库搜索结果,优先使用
+ items:
+ $ref: '#/definitions/srv_videoinfo.SyncMediaFileItem'
+ type: array
+ type: object
+ srv_videoinfo.VideoAddTagsReq:
+ properties:
+ tags:
+ description: 统一标签ID集合
+ items:
+ type: integer
+ type: array
+ videoIds:
+ description: 视频ID集合
+ items:
+ type: integer
+ type: array
+ required:
+ - tags
+ - videoIds
+ type: object
+ srv_videoinfo.VideoInfoDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videoinfo.VideoInfoReq:
+ properties:
+ actressIds:
+ description: 女优ID
+ items:
+ type: integer
+ type: array
+ bango:
+ description: 番号,
+ type: string
+ categoryId:
+ description: 分类ID
+ type: integer
+ creatorId:
+ description: 作者Id
+ type: integer
+ creatorName:
+ description: 作者名称
+ type: string
+ endTime:
+ description: 创建时间-截止
+ type: string
+ id:
+ type: integer
+ isHot:
+ description: 是否热门
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ isTop:
+ description: 是否置顶
+ type: boolean
+ keyworld:
+ description: 关键字
+ type: string
+ mediaType:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.MediaType'
+ description: 资源类型
+ optionAccount:
+ description: 最后操作人
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ sort:
+ description: 排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数。按分类查询且排序方式为排序值时,使用该分类的 categoryRanks 排序
+ type: integer
+ startTime:
+ description: 创建时间-开始
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.InformationStatus'
+ description: 帖子上架状态
+ tag:
+ description: 标签
+ type: integer
+ title:
+ description: 标题
+ type: string
+ videoFileId:
+ description: 切片id(切片列表的ID)
+ type: string
+ videoTaskId:
+ description: 视频任务id
+ type: string
+ type: object
+ srv_videoinfo.VideoInfoResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/videoinformationmod.VideoInformation'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videoinfo.VideoInfoUpdate:
+ properties:
+ actressIds:
+ description: 女优ID
+ items:
+ type: integer
+ type: array
+ addedTime:
+ type: string
+ bango:
+ description: 番号,唯一
+ type: string
+ categoryId:
+ description: 分类ID
+ type: integer
+ cover:
+ description: 视频封面
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ creatorId:
+ description: 作者Id
+ type: integer
+ creatorName:
+ description: 作者名
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ firstTag:
+ description: 一级标签
+ type: string
+ height:
+ description: 分辨率高(视频才会存在)
+ type: integer
+ indexNow:
+ type: integer
+ isHot:
+ description: 是否热门
+ type: boolean
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ isTop:
+ description: 是否置顶
+ type: boolean
+ issuedAt:
+ description: 发行时间
+ type: string
+ issuedBrand:
+ description: 发行商
+ type: string
+ keyworld:
+ items:
+ type: string
+ type: array
+ mediaType:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.MediaType'
+ description: 资源类型
+ nodes:
+ description: 节点
+ items:
+ $ref: '#/definitions/videoinformationmod.Node'
+ type: array
+ remark:
+ description: 备注
+ type: string
+ secondTags:
+ description: 二级标签标签
+ items:
+ type: string
+ type: array
+ secondUpdatedAt:
+ description: 二次修改时间
+ type: string
+ slug:
+ description: slug
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.InformationStatus'
+ description: 帖子上架状态
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: 帖子标题
+ type: string
+ unencryptedImage:
+ description: 不加密封面
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ video:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.VideoInfo'
+ description: 视频
+ videoTime:
+ description: 影片长度
+ type: integer
+ width:
+ description: 分辨率宽(视频才会存在)
+ type: integer
+ type: object
+ srv_videoinfo.VideoInfoUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_videoinfo.VideoInfoUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_videoinfo.VideoSetTagsReq:
+ properties:
+ ids:
+ description: 视频ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ tagIds:
+ description: 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
+ items:
+ type: integer
+ type: array
+ required:
+ - ids
+ type: object
+ srv_videotag.HotTagItem:
+ properties:
+ tagId:
+ type: integer
+ title:
+ type: string
+ watchTimes:
+ type: integer
+ type: object
+ srv_videotag.HotTagResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/srv_videotag.HotTagItem'
+ type: array
+ type: object
+ srv_videotag.IpStatReq:
+ properties:
+ todayIP:
+ type: integer
+ yesterdayIP:
+ type: integer
+ type: object
+ srv_videotag.VideoTagInfoDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videotag.VideoTagInfoReq:
+ properties:
+ id:
+ type: integer
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ title:
+ type: string
+ type: object
+ srv_videotag.VideoTagInfoResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/videotagmod.VideoTagInfo'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videotag.VideoTagInfoUpdate:
+ properties:
+ appId:
+ description: appId,唯一id
+ type: integer
+ images:
+ description: 背景
+ type: string
+ indexNow:
+ type: integer
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ mediaTypes:
+ description: 作者关联的资源类型 1视频
+ items:
+ type: integer
+ type: array
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ title:
+ description: 帖子标题
+ type: string
+ updatedAt:
+ type: string
+ works:
+ description: 作品数量
+ type: integer
+ type: object
+ translationcategorymod.TranslationCategory:
+ properties:
+ categoryType:
+ allOf:
+ - $ref: '#/definitions/pubdef.TranslationType'
+ description: 导航类型 1:url 2:二级导航 3:底部导航
+ createdAt:
+ description: 创建时间
+ type: string
+ icon:
+ description: Icon
+ type: string
+ id:
+ description: Id
+ type: integer
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ updatedAt:
+ description: 更新时间
+ type: string
+ urlValue:
+ description: 跳转URL
+ type: string
+ type: object
+ translationmod.TranslationInfo:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ htmlId:
+ description: 富文本ID
+ type: integer
+ id:
+ description: Id
+ type: integer
+ isFriendAds:
+ description: 是否是友链广告位
+ type: boolean
+ link:
+ description: 外部跳转
+ type: string
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ transCategoryId:
+ description: 导航分类
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ usermod.User:
+ properties:
+ accountNo:
+ description: 账号
+ type: string
+ age:
+ description: 年龄
+ type: integer
+ agentInviteCode:
+ description: 被邀请的inviteCode
+ type: string
+ appVer:
+ description: app版本
+ type: string
+ auth:
+ description: 是否认证
+ type: boolean
+ avatar:
+ description: 头像
+ type: string
+ backgroud:
+ description: 背景图
+ type: string
+ consecutiveLogin:
+ description: 连续登陆天数
+ type: integer
+ consumptionTotal:
+ description: 金币总消费数量
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ currentVipExpire:
+ description: 当前vip过期时间
+ type: string
+ devID:
+ description: 设备唯一标示
+ type: string
+ devV2:
+ description: 设备唯一设备ID
+ type: string
+ device:
+ description: 设备
+ type: string
+ districtCode:
+ description: 商区码
+ type: string
+ gender:
+ description: 性别 0-未知,1-男,2-女
+ type: integer
+ id:
+ description: ID
+ type: integer
+ introduction:
+ description: 简介
+ type: string
+ inviteCode:
+ description: 邀请码
+ type: string
+ invites:
+ description: 我邀请的人的个数(我邀请他人)
+ type: integer
+ isRegister:
+ description: 是否已注册
+ type: boolean
+ lastIp:
+ description: 上次登陆IP
+ type: string
+ lastLoginAt:
+ description: 上次登陆时间
+ type: string
+ loginType:
+ allOf:
+ - $ref: '#/definitions/pubdef.LoginType'
+ description: 登陆方式
+ mac:
+ description: 设备唯一mac地址
+ type: string
+ modifyUserNameAt:
+ description: 上一次修改用户时间
+ type: string
+ nickName:
+ description: 昵称
+ type: string
+ oriVer:
+ description: 注册时的app版本
+ type: string
+ password:
+ description: 登陆密码
+ type: string
+ recharge:
+ description: 当前充值余额,金币个数
+ type: integer
+ rechargeTotal:
+ description: 累计充值额,金币个数
+ type: integer
+ regIp:
+ description: 注册IP
+ type: string
+ region:
+ description: 地区
+ type: string
+ registerTime:
+ type: string
+ score:
+ description: 积分
+ type: integer
+ scoreInvites:
+ description: 邀请人获得的积分累计
+ type: integer
+ scoreLvName:
+ description: 积分等级名称
+ type: string
+ scoreTotal:
+ description: 累计积分
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserStatus'
+ description: 状态
+ sysMsgAt:
+ description: 获取系统消息时间
+ type: string
+ sysType:
+ description: 系统类型 ios android
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ usermod.UserUpdate:
+ properties:
+ accountNo:
+ description: 账号
+ type: string
+ age:
+ description: 年龄
+ type: integer
+ agentInviteCode:
+ description: 被邀请的inviteCode
+ type: string
+ appVer:
+ description: app版本
+ type: string
+ auth:
+ description: 是否认证
+ type: boolean
+ avatar:
+ description: 头像
+ type: string
+ backgroud:
+ description: 背景图
+ type: string
+ consecutiveLogin:
+ description: 连续登陆天数
+ type: integer
+ consumptionTotal:
+ description: 金币总消费数量
+ type: integer
+ currentVipExpire:
+ description: 当前vip过期时间
+ type: string
+ devID:
+ description: 设备唯一标示
+ type: string
+ devV2:
+ description: 设备唯一设备ID
+ type: string
+ device:
+ description: 设备
+ type: string
+ districtCode:
+ description: 商区码
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ gender:
+ description: 性别 0-未知,1-男,2-女
+ type: integer
+ introduction:
+ description: 简介
+ type: string
+ inviteCode:
+ description: 邀请码
+ type: string
+ invites:
+ description: 我邀请的人的个数(我邀请他人)
+ type: integer
+ isRegister:
+ description: 是否已注册
+ type: boolean
+ lastIp:
+ description: 上次登陆IP
+ type: string
+ lastLoginAt:
+ description: 上次登陆时间
+ type: string
+ loginType:
+ allOf:
+ - $ref: '#/definitions/pubdef.LoginType'
+ description: 登陆方式
+ mac:
+ description: 设备唯一mac地址
+ type: string
+ merchantExpiredAt:
+ description: 商家/兼职有效期
+ type: string
+ modifyUserNameAt:
+ description: 上一次修改用户时间
+ type: string
+ nickName:
+ description: 昵称
+ type: string
+ oriVer:
+ description: 注册时的app版本
+ type: string
+ password:
+ description: 登陆密码
+ type: string
+ recharge:
+ description: 当前充值余额,金币个数
+ type: integer
+ rechargeTotal:
+ description: 累计充值额,金币个数
+ type: integer
+ regIp:
+ description: 注册IP
+ type: string
+ region:
+ description: 地区
+ type: string
+ registerTime:
+ type: string
+ score:
+ description: 积分
+ type: integer
+ scoreInvites:
+ description: 邀请人获得的积分累计
+ type: integer
+ scoreLvName:
+ description: 积分等级名称
+ type: string
+ scoreTotal:
+ description: 累计积分
+ type: integer
+ showCardExpiredAt:
+ description: 最新会员卡过期时间
+ type: string
+ showCardId:
+ description: 最新会员卡id
+ type: integer
+ showCardName:
+ description: 最新会员卡名称
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserStatus'
+ description: 状态
+ sysMsgAt:
+ description: 获取系统消息时间
+ type: string
+ sysType:
+ description: 系统类型 ios android
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ videocategorymod.VideoCategory:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 描述
+ type: string
+ displayMode:
+ allOf:
+ - $ref: '#/definitions/pubdef.DisplayMode'
+ description: 展示方式 0:默认 1:卡片层叠轮播 2:头像列表 3:图文列表 4:大封面列表
+ displayPosition:
+ allOf:
+ - $ref: '#/definitions/pubdef.CategoryDisplayPosition'
+ description: 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ id:
+ description: Id
+ type: integer
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ sortType:
+ allOf:
+ - $ref: '#/definitions/pubdef.CategorySortType'
+ description: 排序方式
+ status:
+ description: 上架状态
+ type: boolean
+ tags:
+ description: 关联视频标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: APP显示
+ type: string
+ translationCategoryId:
+ description: 导航分类
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ videocreatermod.VideoCreaterInfo:
+ properties:
+ background:
+ description: 背景
+ type: string
+ createType:
+ description: 创作者类型 0=普通 1=官方
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ id:
+ type: integer
+ image:
+ description: 头像
+ type: string
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ mediaTypes:
+ description: 作者关联的资源类型 1视频
+ items:
+ type: integer
+ type: array
+ name:
+ description: 帖子标题
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ updatedAt:
+ description: 更新时间
+ type: string
+ works:
+ description: 作品数量
+ type: integer
+ type: object
+ videoinformationmod.HevcStatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ - -1
+ type: integer
+ x-enum-comments:
+ HevcStatusFailed: 转码失败
+ HevcStatusNone: 未提交转码
+ HevcStatusPending: 转码中
+ HevcStatusSuccess: 转码成功
+ x-enum-varnames:
+ - HevcStatusNone
+ - HevcStatusPending
+ - HevcStatusSuccess
+ - HevcStatusFailed
+ videoinformationmod.MediaType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ MediaType_AV: AV视频
+ MediaType_Advitise: 广告
+ MediaType_Collection: 合集
+ MediaType_Video: 视频
+ x-enum-varnames:
+ - MediaType_Video
+ - MediaType_Collection
+ - MediaType_Advitise
+ - MediaType_AV
+ videoinformationmod.Node:
+ properties:
+ images:
+ description: 图片
+ items:
+ type: string
+ type: array
+ nodeId:
+ description: 节点ID
+ type: string
+ text:
+ description: 文本
+ type: string
+ title:
+ description: 节点标题
+ type: string
+ url:
+ description: 链接
+ type: string
+ videos:
+ description: 视频信息列表
+ items:
+ $ref: '#/definitions/videoinformationmod.VideoInfo'
+ type: array
+ type: object
+ videoinformationmod.VideoInfo:
+ properties:
+ cover:
+ description: 视频封面
+ type: string
+ fileId:
+ description: 媒资库ID
+ type: string
+ h265Url:
+ description: H.265 m3u8 播放地址,空字符串表示暂无该编码源
+ type: string
+ height:
+ description: 分辨率高(视频才会存在)
+ type: integer
+ preMp4FileNameFs:
+ description: 预览MP4文件
+ type: string
+ taskId:
+ description: 视频ID(视频才会存在)
+ type: string
+ unencryptedPreMp4:
+ description: 不加密的预览MP4
+ type: string
+ url:
+ description: H.264 m3u8 播放地址(始终存在)
+ type: string
+ videoTime:
+ description: 影片长度
+ type: integer
+ width:
+ description: 分辨率宽(视频才会存在)
+ type: integer
+ type: object
+ videoinformationmod.VideoInformation:
+ properties:
+ actressIds:
+ description: 女优ID
+ items:
+ type: integer
+ type: array
+ addedTime:
+ description: 上架时间
+ type: string
+ audioFile:
+ description: 音频文件路径
+ type: string
+ bango:
+ description: 番号,唯一
+ type: string
+ categoryId:
+ description: 分类ID
+ type: integer
+ coverImg:
+ description: 封面
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ creatorId:
+ description: 作者Id
+ type: integer
+ creatorName:
+ description: 作者名称
+ type: string
+ desc:
+ description: 详细描述
+ type: string
+ fake:
+ allOf:
+ - $ref: '#/definitions/pubdef.FakeFlow'
+ description: 伪造流量
+ firstTag:
+ description: 一级标签
+ type: string
+ hevcFailCount:
+ description: 主视频转码累计失败次数
+ type: integer
+ hevcMetaInfo:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.VideoMetaInfo'
+ description: H265 视频元信息
+ hevcStatus:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.HevcStatus'
+ description: 主视频 H265 转码状态
+ id:
+ type: integer
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ isHot:
+ description: 是否热门
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐 false-不推荐
+ type: boolean
+ isTop:
+ description: 是否置顶
+ type: boolean
+ issuedAt:
+ description: 发行时间
+ type: string
+ issuedBrand:
+ description: 发行商
+ type: string
+ keyworld:
+ description: 关键字
+ items:
+ type: string
+ type: array
+ mediaType:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.MediaType'
+ description: 资源类型
+ nodes:
+ description: 节点
+ items:
+ $ref: '#/definitions/videoinformationmod.Node'
+ type: array
+ optionAccount:
+ description: 最后操作人
+ type: string
+ pageUpdatedAt:
+ description: 页面展示更新时间
+ type: string
+ previewVtt:
+ description: 雪碧图 vtt
+ type: string
+ real:
+ allOf:
+ - $ref: '#/definitions/pubdef.Flow'
+ description: 真实流量
+ realUpdateAt:
+ description: 播放相关数据更新时间
+ type: string
+ remark:
+ description: 备注
+ type: string
+ secondTags:
+ description: 二级标签标签
+ items:
+ type: string
+ type: array
+ secondUpdatedAt:
+ description: 二次修改时间
+ type: string
+ slug:
+ description: slug
+ type: string
+ smartCover:
+ description: 智能封面
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.InformationStatus'
+ description: 帖子上架状态
+ syncId:
+ description: 同步ID
+ type: string
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: 标题
+ type: string
+ unencryptedImage:
+ description: 不加密封面
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ uploadUserName:
+ description: 上传用户名
+ type: string
+ video:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.VideoInfo'
+ description: 视频
+ videoCount:
+ description: 合集视频数量
+ type: integer
+ type: object
+ videoinformationmod.VideoMetaInfo:
+ properties:
+ audioBitrate:
+ type: integer
+ audioChannels:
+ type: integer
+ audioCodec:
+ type: string
+ audioSampleRate:
+ type: integer
+ bitrate:
+ type: integer
+ codec:
+ type: string
+ duration:
+ type: number
+ fps:
+ type: number
+ height:
+ type: integer
+ m3u8Url:
+ type: string
+ pixFmt:
+ type: string
+ profile:
+ type: string
+ size:
+ type: integer
+ videoBitrate:
+ type: integer
+ width:
+ type: integer
+ type: object
+ videotagmod.VideoTagInfo:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ type: integer
+ images:
+ description: 背景
+ type: string
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ isShowHome:
+ description: 是否显示在首页
+ type: boolean
+ mediaTypes:
+ description: 作者关联的资源类型 1视频
+ items:
+ type: integer
+ type: array
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ title:
+ description: 帖子标题
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ watchTimes:
+ description: 预计算:该标签下所有视频的累计播放量
+ type: integer
+ works:
+ description: 作品数量
+ type: integer
+ type: object
+ web_acc.AccOpLogReq:
+ properties:
+ actId:
+ description: 操作人Id
+ type: integer
+ action:
+ description: 操作目的
+ type: string
+ endTime:
+ description: 开始时间
+ type: string
+ pageNum:
+ description: 页码
+ type: integer
+ pageSize:
+ type: integer
+ path:
+ description: 请求接口
+ type: string
+ startTime:
+ description: 开始时间
+ type: string
+ type: object
+ web_acc.AccOpLogResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/srv_acc.OpLogInfo'
+ type: array
+ total:
+ type: integer
+ type: object
+ web_acc.BindQurlReq:
+ properties:
+ acc:
+ description: 账户
+ type: string
+ type: object
+ web_acc.BindQurlResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MainWebLoginReq:
+ properties:
+ acc:
+ description: 账户
+ type: string
+ code:
+ description: 验证码
+ type: string
+ pwd:
+ description: 密码
+ type: string
+ type: object
+ web_acc.MainWebLoginResp:
+ properties:
+ acc:
+ description: 账户
+ type: string
+ accId:
+ description: 账户id
+ type: integer
+ qurl:
+ description: 二维码
+ items:
+ type: integer
+ type: array
+ roleId:
+ description: 角色id
+ type: integer
+ token:
+ description: token
+ type: string
+ type: object
+ web_acc.MwAccAddReq:
+ properties:
+ acc:
+ description: 账户
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ isEnb:
+ description: 使能
+ type: boolean
+ limitIP:
+ description: 允许访问的IP
+ items:
+ type: string
+ type: array
+ name:
+ description: 名称
+ type: string
+ pwd:
+ description: 密码
+ type: string
+ roleId:
+ description: 角色ID
+ type: integer
+ type: object
+ web_acc.MwAccAddResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwAccDelReq:
+ properties:
+ id:
+ description: 账号Id
+ type: integer
+ type: object
+ web_acc.MwAccDelResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwAccGetInfoReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ type: object
+ web_acc.MwAccGetInfoResp:
+ properties:
+ acc:
+ description: 账号
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ id:
+ description: 账号Id
+ type: integer
+ isEnb:
+ description: 使能
+ type: boolean
+ limitIP:
+ description: 允许访问的IP
+ items:
+ type: string
+ type: array
+ pwd:
+ description: 密码
+ type: string
+ roleId:
+ description: 角色Id
+ type: integer
+ userName:
+ description: 用户名
+ type: string
+ type: object
+ web_acc.MwAccGetStatusReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ type: object
+ web_acc.MwAccGetStatusResp:
+ properties:
+ feedBack:
+ description: 禁用原因
+ type: string
+ id:
+ description: 账号Id
+ type: integer
+ isEnb:
+ description: 使能
+ type: boolean
+ userName:
+ description: 用户名
+ type: string
+ type: object
+ web_acc.MwAccListReq:
+ properties:
+ keyWords:
+ description: 搜索条件
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ status:
+ description: 状态 1正常 2 禁用
+ type: integer
+ type: object
+ web_acc.MwAccListResp:
+ properties:
+ accRec:
+ description: 当前页配置记录
+ items:
+ $ref: '#/definitions/web_acc.MwAccRec'
+ type: array
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ web_acc.MwAccRec:
+ properties:
+ acc:
+ description: 账号
+ type: string
+ createdAt:
+ description: 创建时间
+ type: integer
+ email:
+ description: 邮箱
+ type: string
+ feedBack:
+ description: 禁用原因
+ type: string
+ id:
+ description: 账号Id
+ type: integer
+ isBase32:
+ description: |-
+ // 二步签名
+ Base32 string `json:"base32"`
+ 是否已签名
+ type: boolean
+ isEnb:
+ description: 使能
+ type: boolean
+ limitIP:
+ description: 允许访问的IP
+ items:
+ type: string
+ type: array
+ pwd:
+ description: 密码
+ type: string
+ roleId:
+ description: 角色Id
+ type: integer
+ roleName:
+ description: 角色名
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: integer
+ userName:
+ description: 用户名
+ type: string
+ type: object
+ web_acc.MwAccSaveStatusReq:
+ properties:
+ feedBack:
+ description: 禁用原因
+ type: string
+ id:
+ description: 账号Id
+ type: integer
+ isEnb:
+ description: 使能
+ type: boolean
+ userName:
+ description: 用户名
+ type: string
+ type: object
+ web_acc.MwAccSaveStatusResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwAccUpPwdReq:
+ properties:
+ id:
+ description: 账号Id
+ type: integer
+ pwd:
+ description: 密码
+ type: string
+ required:
+ - id
+ type: object
+ web_acc.MwAccUpReq:
+ properties:
+ acc:
+ description: 账号
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ id:
+ description: 账号Id
+ type: integer
+ isEnb:
+ description: 使能
+ type: boolean
+ limitIP:
+ description: 允许访问的IP
+ items:
+ type: string
+ type: array
+ pwd:
+ description: 密码
+ type: string
+ roleId:
+ description: 角色Id
+ type: integer
+ userName:
+ description: 用户名
+ type: string
+ type: object
+ web_acc.MwAccUpResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwRoleAddReq:
+ properties:
+ desc:
+ description: 描述
+ type: string
+ isEnb:
+ description: 状态
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ type: object
+ web_acc.MwRoleAddResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwRoleDelReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ type: object
+ web_acc.MwRoleDelResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwRoleDrop:
+ properties:
+ id:
+ description: 角色Id
+ type: integer
+ name:
+ description: 角色名称
+ type: string
+ type: object
+ web_acc.MwRoleDropResp:
+ properties:
+ roleDrop:
+ description: 是否成功 true:成功 false:未成功
+ items:
+ $ref: '#/definitions/web_acc.MwRoleDrop'
+ type: array
+ type: object
+ web_acc.MwRoleEnableReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ isEnb:
+ description: 状态
+ type: boolean
+ type: object
+ web_acc.MwRoleEnableResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwRoleGetAuthReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ type: object
+ web_acc.MwRoleGetAuthResp:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ router:
+ items:
+ $ref: '#/definitions/web_acc.Router'
+ type: array
+ type: object
+ web_acc.MwRoleGetInfoReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ type: object
+ web_acc.MwRoleGetInfoResp:
+ properties:
+ roleRec:
+ $ref: '#/definitions/web_acc.MwRoleRec'
+ type: object
+ web_acc.MwRoleListResp:
+ properties:
+ roleRecs:
+ description: 当前页配置记录
+ items:
+ $ref: '#/definitions/web_acc.MwRoleRec'
+ type: array
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ web_acc.MwRoleRec:
+ properties:
+ apiMenus:
+ description: 已有权限的api
+ items:
+ type: string
+ type: array
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 描述
+ type: string
+ id:
+ description: 角色Id
+ type: integer
+ isEnb:
+ description: 是否启用 false:禁用 true:启用
+ type: boolean
+ name:
+ description: 角色名称
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ web_acc.MwRoleUpAuthReq:
+ properties:
+ id:
+ description: 角色id
+ type: integer
+ router:
+ items:
+ $ref: '#/definitions/web_acc.Router'
+ type: array
+ type: object
+ web_acc.MwRoleUpAuthResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.MwRoleUpReq:
+ properties:
+ desc:
+ description: 描述
+ type: string
+ id:
+ description: 角色id
+ type: integer
+ isEnb:
+ description: 状态
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ type: object
+ web_acc.MwRoleUpResp:
+ properties:
+ result:
+ description: 是否成功 true:成功 false:未成功
+ type: boolean
+ type: object
+ web_acc.Router:
+ properties:
+ apiUrl:
+ items:
+ $ref: '#/definitions/web_acc.Router_ApiUrl'
+ type: array
+ children:
+ items:
+ $ref: '#/definitions/web_acc.Router'
+ type: array
+ component:
+ type: string
+ hidden:
+ type: boolean
+ isShow:
+ type: boolean
+ meta:
+ $ref: '#/definitions/web_acc.Router_Meta'
+ name:
+ type: string
+ path:
+ type: string
+ redirect:
+ type: string
+ type: object
+ web_acc.Router_ApiUrl:
+ properties:
+ desc:
+ type: string
+ method:
+ items:
+ type: string
+ type: array
+ status:
+ type: boolean
+ url:
+ items:
+ type: string
+ type: array
+ type: object
+ web_acc.Router_Meta:
+ properties:
+ icon:
+ type: string
+ noCache:
+ type: boolean
+ title:
+ type: string
+ type: object
+ web_cfg.GetSysConfigReq:
+ type: object
+ web_cfg.GetSysConfigResp:
+ properties:
+ appAddr:
+ description: APP下载地址(落地页)
+ type: string
+ channelCode:
+ description: 渠道码
+ type: string
+ dhDescription:
+ description: 导航SEO描述
+ type: string
+ dhKeyworld:
+ description: 导航SEO关键字
+ items:
+ type: string
+ type: array
+ dhNewUrlAddr1:
+ description: 导航页-线路1
+ type: string
+ dhNewUrlAddr2:
+ description: 导航页-线路2
+ type: string
+ dhNewUrlAddr3:
+ description: 导航页-线路3
+ type: string
+ dhSiteName:
+ description: 导航SEO网站名称
+ type: string
+ dhTitle:
+ description: 导航SEO标题
+ type: string
+ downloadVideoUrl:
+ description: 下载视频URL
+ type: string
+ enableH265:
+ description: 是否允许客户端使用 H265 地址播放
+ type: boolean
+ foreverUrlAddr:
+ description: 永久地址
+ type: string
+ freeVPN:
+ description: 免费vpn链接
+ type: string
+ gitAddr:
+ description: git地址
+ type: string
+ gmailAddr:
+ description: 邮箱地址
+ type: string
+ id:
+ type: integer
+ imgAppCdn:
+ description: 图片CDN(app使用)
+ type: string
+ imgAppUsCdn:
+ description: 图片CDN(海外app使用)
+ type: string
+ imgWebCdn:
+ description: 图片CDN(web使用)
+ type: string
+ m3u8DayRateLimitMax:
+ description: 每 IP 每天最多不同正片数,<=0 使用默认 80
+ type: integer
+ mediaAppCdn:
+ description: 视频CDN(app使用)
+ type: string
+ mediaAppUsCdn:
+ description: 视频CDN(海外app使用)
+ type: string
+ mediaWebCdn:
+ description: 视频CDN(web使用)
+ type: string
+ mp4Addr:
+ description: mp4地址(web使用)
+ type: string
+ newUrlAddr:
+ description: 网站管理
+ type: string
+ newUrlAddr1:
+ description: 最新地址(备用地址1)
+ type: string
+ newUrlAddr2:
+ description: 最新地址(备用地址2)
+ type: string
+ pdfAddr:
+ description: 回家的路PDF
+ type: string
+ potaoGroup:
+ description: 土豆群
+ type: string
+ qq:
+ description: qq群
+ type: string
+ sitemapDefaultImages:
+ description: Sitemap里面默认封面图
+ items:
+ type: string
+ type: array
+ telegram:
+ description: 商务tg
+ type: string
+ telegramAdd:
+ description: 加入我们tg
+ type: string
+ tgGroup:
+ description: tg群
+ type: string
+ translationAddr:
+ description: 应用导航
+ type: string
+ twitterAddr:
+ description: 推特地址
+ type: string
+ unencryptedCdn:
+ description: 不加密资源使用的cdn
+ type: string
+ useAddress:
+ description: 所有使用中的域名
+ items:
+ type: string
+ type: array
+ version:
+ description: 版本号,用于更新乐观锁
+ type: integer
+ watermarkImage:
+ description: 图片水印文件
+ type: string
+ watermarkMedia:
+ description: 视频水印文件(底部右边)
+ type: string
+ watermarkMediaBottomLeft:
+ description: 视频水印文件(底部左边)
+ type: string
+ watermarkMediaTop:
+ description: 视频水印文件(顶部左边)
+ type: string
+ watermarkMediaTopRight:
+ description: 视频水印文件(顶部右边)
+ type: string
+ webAddr:
+ description: 网站SEO网站地址
+ type: string
+ webDescription:
+ description: 网站SEO描述
+ type: string
+ webKeyworld:
+ description: 网站SEO关键字
+ items:
+ type: string
+ type: array
+ webSiteName:
+ description: 网站SEO网站名称
+ type: string
+ webTitle:
+ description: SEO
+ type: string
+ weiChart:
+ description: 微信群
+ type: string
+ type: object
+ web_cfg.SysConfigUpdateReq:
+ properties:
+ appAddr:
+ description: APP下载地址(落地页)
+ type: string
+ channelCode:
+ description: 渠道码
+ type: string
+ dhDescription:
+ description: 导航SEO描述
+ type: string
+ dhKeyworld:
+ description: 导航SEO关键字
+ items:
+ type: string
+ type: array
+ dhNewUrlAddr1:
+ description: 导航页-线路一
+ type: string
+ dhNewUrlAddr2:
+ description: 导航页-线路2
+ type: string
+ dhNewUrlAddr3:
+ description: 导航页-线路3
+ type: string
+ dhSiteName:
+ description: 导航SEO网站名称
+ type: string
+ dhTitle:
+ description: 导航SEO标题
+ type: string
+ downloadVideoUrl:
+ description: 下载视频URL
+ type: string
+ enableH265:
+ description: 是否允许客户端使用 H265 地址播放
+ type: boolean
+ foreverUrlAddr:
+ description: 永久地址
+ type: string
+ gitAddr:
+ description: git地址
+ type: string
+ gmailAddr:
+ description: 邮箱地址
+ type: string
+ imgAppCdn:
+ description: 图片CDN(app使用)
+ type: string
+ imgAppUsCdn:
+ description: 图片CDN(海外app使用)
+ type: string
+ imgWebCdn:
+ description: 图片CDN(web使用)
+ type: string
+ m3u8DayRateLimitMax:
+ description: 每 IP 每天最多不同正片数,<=0 使用默认 80
+ type: integer
+ mediaAppCdn:
+ description: 视频CDN(app使用)
+ type: string
+ mediaAppUsCdn:
+ description: 视频CDN(海外app使用)
+ type: string
+ mediaWebCdn:
+ description: 视频CDN(web使用)
+ type: string
+ mp4Addr:
+ description: mp4地址(web使用)
+ type: string
+ newUrlAddr:
+ description: 最新地址
+ type: string
+ newUrlAddr1:
+ description: 最新地址(备用地址1)
+ type: string
+ newUrlAddr2:
+ description: 最新地址(备用地址2)
+ type: string
+ pdfAddr:
+ description: 回家的路PDF
+ type: string
+ potaoGroup:
+ description: 土豆群
+ type: string
+ qq:
+ description: qq群
+ type: string
+ sitemapDefaultImages:
+ description: Sitemap里面默认封面图
+ items:
+ type: string
+ type: array
+ telegram:
+ description: 商务tg
+ type: string
+ telegramAdd:
+ description: 加入我们tg
+ type: string
+ tgGroup:
+ description: tg群
+ type: string
+ translationAddr:
+ description: 应用导航
+ type: string
+ twitterAddr:
+ description: 推特地址
+ type: string
+ unencryptedCdn:
+ description: 不加密资源使用的cdn
+ type: string
+ watermarkImage:
+ description: 图片水印文件
+ type: string
+ watermarkMedia:
+ description: 视频水印文件(底部右边)
+ type: string
+ watermarkMediaBottomLeft:
+ description: 视频水印文件(底部左边)
+ type: string
+ watermarkMediaTop:
+ description: 视频水印文件(顶部左边)
+ type: string
+ watermarkMediaTopRight:
+ description: 视频水印文件(顶部右边)
+ type: string
+ webAddr:
+ description: 网站SEO网站地址
+ type: string
+ webDescription:
+ description: 网站SEO描述
+ type: string
+ webKeyworld:
+ description: 网站SEO关键字
+ items:
+ type: string
+ type: array
+ webSiteName:
+ description: 网站SEO网站名称
+ type: string
+ webTitle:
+ description: SEO
+ type: string
+ weiChart:
+ description: 微信群
+ type: string
+ type: object
+ web_cfg.SysConfigUpdateResp:
+ type: object
+ web_commend.CommendDelIpReq:
+ properties:
+ ip:
+ type: string
+ type: object
+ web_commend.CommendDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ web_commend.CommendReviewBatchReq:
+ properties:
+ ids:
+ items:
+ type: integer
+ type: array
+ reviewStatus:
+ type: integer
+ type: object
+ web_file.UploadImageResp:
+ properties:
+ path:
+ type: string
+ type: object
+ web_file.UploadImagesResp:
+ properties:
+ paths:
+ items:
+ type: string
+ type: array
+ type: object
+host: localhost:12667
+info:
+ contact: {}
+ description: 这是站群后台接口服务.
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+ title: 站群后台接口
+ version: "1.0"
+paths:
+ /api/web/acc/add:
+ post:
+ consumes:
+ - application/json
+ description: 账号-添加
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccAddResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-添加
+ tags:
+ - 账号
+ /api/web/acc/del:
+ post:
+ consumes:
+ - application/json
+ description: 账号-删除
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccDelResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-删除
+ tags:
+ - 账号
+ /api/web/acc/getinfo:
+ get:
+ consumes:
+ - application/json
+ description: 账号-获取信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccGetInfoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccGetInfoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-获取信息
+ tags:
+ - 账号
+ /api/web/acc/getstatus:
+ get:
+ consumes:
+ - application/json
+ description: 账号-获取状态
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccGetStatusReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccGetStatusResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-获取状态
+ tags:
+ - 账号
+ /api/web/acc/list:
+ get:
+ consumes:
+ - application/json
+ description: 账号-列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-列表
+ tags:
+ - 账号
+ /api/web/acc/ops:
+ post:
+ consumes:
+ - application/json
+ description: 账号-获取操作记录
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.AccOpLogReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.AccOpLogResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-获取操作记录
+ tags:
+ - 账号
+ /api/web/acc/savestatus:
+ post:
+ consumes:
+ - application/json
+ description: 账号-保存状态
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccSaveStatusReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccSaveStatusResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-保存状态
+ tags:
+ - 账号
+ /api/web/acc/up:
+ post:
+ consumes:
+ - application/json
+ description: 账号-更新
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccUpReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccUpResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-更新
+ tags:
+ - 账号
+ /api/web/acc/uppwd:
+ post:
+ consumes:
+ - application/json
+ description: 账号-更新密码
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwAccUpPwdReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwAccUpPwdReq'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 账号-更新密码
+ tags:
+ - 账号
+ /api/web/actress/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个女优信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_actress.ActressAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个女优信息
+ tags:
+ - 女优管理
+ /api/web/actress/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个女优信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_actress.ActressDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个女优信息
+ tags:
+ - 女优管理
+ /api/web/actress/list:
+ get:
+ consumes:
+ - application/json
+ description: 分组分页获取女优信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_actress.ActressListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_actress.ActressListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取女优信息
+ tags:
+ - 女优管理
+ /api/web/actress/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个女优信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_actress.ActressUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个女优信息
+ tags:
+ - 女优管理
+ /api/web/contactus/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个联系我们
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_contactus.DelOneContactusReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 删除一个联系我们
+ tags:
+ - 联系我们
+ /api/web/contactus/list:
+ post:
+ consumes:
+ - application/json
+ description: 联系我们列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_contactus.GetContactusListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_contactus.GetContactusListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 联系我们列表
+ tags:
+ - 联系我们
+ /api/web/contactus/update:
+ post:
+ consumes:
+ - application/json
+ description: 修改联系我们状态
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_contactus.UpdateContactusReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 修改联系我们状态
+ tags:
+ - 联系我们
+ /api/web/content/tag/add:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 新增统一标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.ContentTagAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.ContentTagAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 新增统一标签 -- by 王鸣
+ tags:
+ - web统一标签相关接口
+ /api/web/content/tag/del:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 删除统一标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.ContentTagDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除统一标签 -- by 王鸣
+ tags:
+ - web统一标签相关接口
+ /api/web/content/tag/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询统一标签配置
+ parameters:
+ - description: 标签ID
+ in: query
+ name: id
+ type: integer
+ - description: 标签名称,支持模糊查询
+ in: query
+ name: name
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 状态 true-启用 false-停用
+ in: query
+ name: status
+ type: boolean
+ - description: 内容类型 1-视频 5-短剧
+ enum:
+ - 1
+ - 5
+ in: query
+ name: type
+ type: integer
+ x-enum-comments:
+ ContentTagTypePlaylet: 短剧
+ ContentTagTypeVideo: 视频
+ x-enum-varnames:
+ - ContentTagTypeVideo
+ - ContentTagTypePlaylet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.ContentTagListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 统一标签列表 -- by 王鸣
+ tags:
+ - web统一标签相关接口
+ /api/web/content/tag/option:
+ get:
+ consumes:
+ - application/json
+ description: 查询启用的统一标签下拉列表,传type时按支持类型过滤
+ parameters:
+ - description: 内容类型 1-视频 5-短剧,不传返回全部启用标签
+ enum:
+ - 1
+ - 5
+ in: query
+ name: type
+ type: integer
+ x-enum-comments:
+ ContentTagTypePlaylet: 短剧
+ ContentTagTypeVideo: 视频
+ x-enum-varnames:
+ - ContentTagTypeVideo
+ - ContentTagTypePlaylet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.ContentTagOptionResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 统一标签下拉列表 -- by 王鸣
+ tags:
+ - web统一标签相关接口
+ /api/web/content/tag/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新统一标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.ContentTagUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新统一标签 -- by 王鸣
+ tags:
+ - web统一标签相关接口
+ /api/web/detailnav/add:
+ post:
+ consumes:
+ - application/json
+ description: 详情页导航配置-增加
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_detailnav.AddDetailNavReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 详情页导航配置-增加
+ tags:
+ - 详情页导航配置
+ /api/web/detailnav/del:
+ post:
+ consumes:
+ - application/json
+ description: 详情页导航配置-删除
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_detailnav.DelDetailNavReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 详情页导航配置-删除
+ tags:
+ - 详情页导航配置
+ /api/web/detailnav/list:
+ post:
+ consumes:
+ - application/json
+ description: 详情页导航配置-列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_detailnav.GetDetailNavListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_detailnav.GetDetailNavListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 详情页导航配置-列表
+ tags:
+ - 详情页导航配置
+ /api/web/detailnav/update:
+ post:
+ consumes:
+ - application/json
+ description: 详情页导航配置-更新
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_detailnav.UpdateDetailNavReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 详情页导航配置-更新
+ tags:
+ - 详情页导航配置
+ /api/web/file/uploadImg:
+ post:
+ consumes:
+ - application/json
+ description: 图片上传
+ parameters:
+ - description: 文件
+ in: formData
+ name: file
+ required: true
+ type: file
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_file.UploadImageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 图片上传
+ tags:
+ - 文件上传
+ /api/web/file/uploadImgs:
+ post:
+ consumes:
+ - application/json
+ description: 图片批量上传
+ parameters:
+ - description: 文件
+ in: formData
+ name: file[]
+ required: true
+ type: file
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_file.UploadImagesResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 图片批量上传
+ tags:
+ - 文件上传
+ /api/web/home/banner/add:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 新增首页banner请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.HomeBannerAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeBannerAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 新增首页banner -- by 王鸣
+ tags:
+ - web首页banner相关接口
+ /api/web/home/banner/del:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 删除首页banner请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.HomeBannerDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除首页banner -- by 王鸣
+ tags:
+ - web首页banner相关接口
+ /api/web/home/banner/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询首页banner配置
+ parameters:
+ - description: 首页 banner ID
+ in: query
+ name: id
+ type: integer
+ - description: 名称,支持模糊查询
+ in: query
+ name: name
+ type: string
+ - description: 页码,默认1
+ in: query
+ name: pageNum
+ type: integer
+ - description: 每页数量,默认10,最大100
+ in: query
+ name: pageSize
+ type: integer
+ - description: 状态 true-启用 false-停用
+ in: query
+ name: status
+ type: boolean
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomeBannerListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 首页banner列表 -- by 王鸣
+ tags:
+ - web首页banner相关接口
+ /api/web/home/banner/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新首页banner请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.HomeBannerUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新首页banner -- by 王鸣
+ tags:
+ - web首页banner相关接口
+ /api/web/hotnav/add:
+ post:
+ consumes:
+ - application/json
+ description: 热门导航配置-增加
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_hotnav.AddHotNavReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 热门导航配置-增加
+ tags:
+ - 热门导航配置
+ /api/web/hotnav/del:
+ post:
+ consumes:
+ - application/json
+ description: 热门导航配置-删除
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_hotnav.DelHotNavReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 热门导航配置-删除
+ tags:
+ - 热门导航配置
+ /api/web/hotnav/list:
+ post:
+ consumes:
+ - application/json
+ description: 热门导航配置-列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_hotnav.GetHotNavListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_hotnav.GetHotNavListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 热门导航配置-列表
+ tags:
+ - 热门导航配置
+ /api/web/hotnav/update:
+ post:
+ consumes:
+ - application/json
+ description: 热门导航配置-更新
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_hotnav.UpdateHotNavReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 热门导航配置-更新
+ tags:
+ - 热门导航配置
+ /api/web/ipblack/add:
+ post:
+ consumes:
+ - application/json
+ description: IP黑名单-增加
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_ipblack.AddIPBlackListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: IP黑名单-增加
+ tags:
+ - IP黑名单
+ /api/web/ipblack/del:
+ post:
+ consumes:
+ - application/json
+ description: IP黑名单-删除
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_ipblack.DelIpBlackListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: IP黑名单-删除
+ tags:
+ - IP黑名单
+ /api/web/ipblack/list:
+ post:
+ consumes:
+ - application/json
+ description: IP黑名单-列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_ipblack.GetIPBlackListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_ipblack.GetIPBlackListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: IP黑名单-列表
+ tags:
+ - IP黑名单
+ /api/web/login/acc:
+ post:
+ consumes:
+ - application/json
+ description: 登录-登录
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MainWebLoginReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MainWebLoginResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 登录-登录
+ tags:
+ - 登录
+ /api/web/login/bind:
+ post:
+ consumes:
+ - application/json
+ description: 登录-绑定二维码
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.BindQurlReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.BindQurlResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 登录-绑定二维码
+ tags:
+ - 登录
+ /api/web/login/slogin:
+ post:
+ consumes:
+ - multipart/form-data
+ - application/json
+ description: 管理员sso登陆
+ parameters:
+ - description: 账号
+ in: formData
+ name: name
+ required: true
+ type: string
+ - description: 密码
+ in: formData
+ name: password
+ required: true
+ type: string
+ produces:
+ - application/json
+ - text/html
+ responses:
+ "200":
+ description: '{"msg": "管理员信息""}'
+ schema:
+ type: string
+ "400":
+ description: '{"msg": "操作失败"}'
+ schema:
+ type: string
+ summary: 管理员sso登陆
+ tags:
+ - web-单点登陆
+ /api/web/playlet/add:
+ post:
+ consumes:
+ - application/json
+ description: 新增 playlet 集合短剧数据,支持分类、题材、标签、支付方式、推荐和 banner 配置
+ parameters:
+ - description: 新增短剧请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端新增短剧 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/banner/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 showOnBanner 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧是否显示在首页banner请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletBannerSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧是否显示在首页banner -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/category/add:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 新增短剧分类请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletCategoryAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletCategoryAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端新增短剧分类 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/category/del:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 删除短剧分类请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletCategoryDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端删除短剧分类 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/category/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧分类
+ parameters:
+ - description: 分类ID
+ in: query
+ name: id
+ type: integer
+ - description: 分类名称,支持模糊查询
+ in: query
+ name: name
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 启用状态
+ in: query
+ name: status
+ type: boolean
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletCategoryListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端短剧分类列表 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/category/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 categoryIds 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧分类请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletCategorySetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧分类 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/category/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新短剧分类请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletCategoryUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端更新短剧分类 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/add:
+ post:
+ consumes:
+ - application/json
+ description: 新增 playlet_chapter 集合短剧剧集数据
+ parameters:
+ - description: 新增短剧剧集请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端新增短剧剧集 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/del:
+ post:
+ consumes:
+ - application/json
+ description: 根据剧集ID集合删除 playlet_chapter 记录,仅原创短剧支持删除
+ parameters:
+ - description: 单/批量删除短剧剧集请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量删除短剧剧集
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询 playlet_chapter 集合短剧剧集数据,默认按集数升序返回
+ parameters:
+ - description: 剧集ID
+ in: query
+ name: id
+ type: integer
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 短剧ID
+ in: query
+ name: playletId
+ type: integer
+ - description: 水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ in: query
+ name: watermarkStatus
+ type: integer
+ x-enum-comments:
+ WatermarkStatusDone: 已完成
+ WatermarkStatusFailed: 处理失败
+ WatermarkStatusPending: 未处理(待 mediafile/sync)
+ WatermarkStatusProcessing: 处理中(已提交加水印任务,待回写)
+ WatermarkStatusRegistered: 已登记(原片已登记,待 task/add)
+ x-enum-varnames:
+ - WatermarkStatusPending
+ - WatermarkStatusRegistered
+ - WatermarkStatusProcessing
+ - WatermarkStatusDone
+ - WatermarkStatusFailed
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端短剧剧集列表 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/pay-type/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据剧集ID集合批量更新 playlet_chapter 集合 payType 和 singleCoins 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧剧集支付方式请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterPayTypeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧剧集支付方式 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/sync:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合从媒资中心同步新增剧集,playletIds传1个表示单条同步,传多个表示批量同步,允许已完结短剧补集或修正状态
+ parameters:
+ - description: 手动单/批量同步短剧剧集请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterSyncReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterSyncResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端手动单/批量同步短剧剧集
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/update:
+ post:
+ consumes:
+ - application/json
+ description: 按剧集ID编辑 playlet_chapter 集合短剧剧集数据,只更新请求中传入的字段
+ parameters:
+ - description: 编辑短剧剧集请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端编辑短剧剧集 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/featured/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 isFeatured 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧是否爆剧请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletFeaturedSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧是否爆剧 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/like/virtual/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 virtualCountLike 字段,每条短剧从最小值和最大值之间随机取不重复数值入库
+ parameters:
+ - description: 单/批量随机设置短剧虚拟点赞数请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletVirtualLikeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧虚拟点赞数 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧列表
+ parameters:
+ - description: 分类ID
+ in: query
+ name: categoryId
+ type: integer
+ - description: 内容类型,0-短剧、1-普通短剧/影集、2-漫画短剧、3-动漫、4-漫画
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ in: query
+ name: contentType
+ type: integer
+ x-enum-comments:
+ ContentTypeAlbum: 普通短剧/影集(对应 media_type=6)
+ ContentTypeAnime: 动漫(对应 media_type=1)
+ ContentTypeComic: 漫画短剧(对应 media_type=4)
+ ContentTypeManga: 漫画(对应 media_type=2)
+ ContentTypeTheater: 短剧(对应 media_type=5)
+ x-enum-varnames:
+ - ContentTypeTheater
+ - ContentTypeAlbum
+ - ContentTypeComic
+ - ContentTypeAnime
+ - ContentTypeManga
+ - description: 短剧ID
+ in: query
+ name: id
+ type: integer
+ - description: 是否爆剧,true-是、false-否
+ in: query
+ name: isFeatured
+ type: boolean
+ - description: 是否原创:true只查原创,false只查非原创,不传查全部
+ in: query
+ name: isOriginal
+ type: boolean
+ - description: 是否推荐,true-推荐、false-不推荐
+ in: query
+ name: isRecommend
+ type: boolean
+ - description: 最后操作人名称,支持模糊查询
+ in: query
+ name: operatorName
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 支付方式,0-免费、1-金币、2-VIP
+ enum:
+ - 0
+ - 1
+ - 2
+ in: query
+ name: payType
+ type: integer
+ x-enum-comments:
+ PayTypeCoin: 金币
+ PayTypeFree: 免费
+ PayTypeVip: VIP
+ x-enum-varnames:
+ - PayTypeFree
+ - PayTypeCoin
+ - PayTypeVip
+ - description: 是否显示在首页banner,true-显示、false-不显示
+ in: query
+ name: showOnBanner
+ type: boolean
+ - description: 上架状态,0-下架、1-上架
+ enum:
+ - 0
+ - 1
+ in: query
+ name: status
+ type: integer
+ x-enum-comments:
+ StatusDown: 下架
+ StatusUp: 上架
+ x-enum-varnames:
+ - StatusDown
+ - StatusUp
+ - description: 标签ID
+ in: query
+ name: tagId
+ type: integer
+ - description: 题材ID
+ in: query
+ name: themeId
+ type: integer
+ - description: 短剧标题,支持模糊查询
+ in: query
+ name: title
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端短剧列表 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/pay-type/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 payType/singleCoins 字段
+ parameters:
+ - description: 单/批量设置短剧默认支付方式请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletPayTypeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧默认支付方式 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/recommend/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧是否推荐请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletRecommendSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧是否推荐 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/status/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 status 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧上下架请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletStatusSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧上下架 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/tag/add:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 新增标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletTagAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletTagAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端新增标签 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/tag/del:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 删除标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletTagDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端删除标签 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/tag/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧标签
+ parameters:
+ - in: query
+ name: id
+ type: integer
+ - in: query
+ name: name
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - in: query
+ name: status
+ type: boolean
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletTagListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端标签列表 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/tag/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 tagIds 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletTagSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧标签 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/tag/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新标签请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletTagUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端更新标签 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/add:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 新增题材请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端新增题材 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/del:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 删除题材请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端删除题材 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧题材
+ parameters:
+ - description: 题材ID
+ in: query
+ name: id
+ type: integer
+ - description: 题材名称,支持模糊查询
+ in: query
+ name: name
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 上架状态
+ in: query
+ name: status
+ type: boolean
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端题材列表 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 单/批量设置短剧题材请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端单/批量设置短剧题材 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新题材请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端更新题材 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/update:
+ post:
+ consumes:
+ - application/json
+ description: 按短剧ID编辑 playlet 集合短剧数据,只更新请求中传入的字段
+ parameters:
+ - description: 编辑短剧请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端编辑短剧 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/watermark/reset:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合重置剧集水印状态,保留已有水印地址以便换水印期间继续观看
+ parameters:
+ - description: 批量重置短剧水印请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletWatermarkResetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端按短剧ID批量重置剧集水印 -- by 王鸣
+ tags:
+ - web短剧相关接口
+ /api/web/post/comment/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个评论
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_commend.CommendDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 删除一个评论
+ tags:
+ - 评论
+ /api/web/post/comment/list:
+ post:
+ consumes:
+ - application/json
+ description: 评论列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_commend.CommendSearch'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_commend.CommendSearchResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 评论列表
+ tags:
+ - 评论
+ /api/web/post/comment/replay:
+ post:
+ consumes:
+ - application/json
+ description: 回复评论
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_commend.CommendReplay'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 回复评论
+ tags:
+ - 评论
+ /api/web/post/comment/review:
+ post:
+ consumes:
+ - application/json
+ description: 审核评论
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_commend.CommendReviewReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 审核评论
+ tags:
+ - 评论
+ /api/web/post/comment/reviewBatch:
+ post:
+ consumes:
+ - application/json
+ description: 批量审核评论
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_commend.CommendReviewBatchReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 批量审核评论
+ tags:
+ - 评论
+ /api/web/post/comment/user/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个用户所有评论
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_commend.CommendDelIpReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 删除一个用户所有评论
+ tags:
+ - 评论
+ /api/web/questionnaire/handle:
+ post:
+ consumes:
+ - application/json
+ description: 只有状态为"待处理"的问卷才允许更新
+ parameters:
+ - description: 处理参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_questionnaire.HandleReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 处理问卷
+ tags:
+ - 问卷调查
+ /api/web/questionnaire/list:
+ post:
+ consumes:
+ - application/json
+ description: 支持分页及条件筛选(question_type、status、时间范围)
+ parameters:
+ - description: 查询参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_questionnaire.ListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_questionnaire.ListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询问卷列表
+ tags:
+ - 问卷调查
+ /api/web/resource/chapters:
+ get:
+ consumes:
+ - application/json
+ description: 按媒资ID查询剧集信息,复用入库时的 mediaSync 接口
+ parameters:
+ - description: 媒资中心资源ID
+ in: query
+ name: id
+ required: true
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletResourceChapterListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端短剧媒资剧集列表 -- by 王鸣
+ tags:
+ - web媒资库相关接口
+ /api/web/resource/import:
+ post:
+ consumes:
+ - application/json
+ description: 提交媒资入库任务,后台从媒资中心同步短剧详情并写入短剧和章节集合
+ parameters:
+ - description: 短剧媒资入库请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletResourceUseReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端短剧媒资入库 -- by 王鸣
+ tags:
+ - web媒资库相关接口
+ /api/web/resource/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧媒资列表
+ parameters:
+ - description: 媒体类型 1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧
+ in: query
+ name: media_type
+ type: integer
+ - description: 页码
+ in: query
+ name: pageNumber
+ type: integer
+ - description: 个数
+ in: query
+ name: pageSize
+ type: integer
+ - description: 同步类型 1-全量同步 2-增量同步
+ in: query
+ name: sync_type
+ type: integer
+ - description: 标签
+ in: query
+ name: tag
+ type: string
+ - description: 标题
+ in: query
+ name: title
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletResourceListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端短剧媒资列表 -- by 王鸣
+ tags:
+ - web媒资库相关接口
+ /api/web/richtextcontent/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个富文本内容
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/richtextcontentmod.RichTextContentModel'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个富文本内容
+ tags:
+ - 富文本内容
+ /api/web/richtextcontent/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个富文本内容
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_richtextcontent.DelRichTextContentReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个富文本内容
+ tags:
+ - 富文本内容
+ /api/web/richtextcontent/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取富文本内容
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_richtextcontent.GetRichTextContentListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_richtextcontent.GetRichTextContentResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取富文本内容
+ tags:
+ - 富文本内容
+ /api/web/richtextcontent/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个富文本内容
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_richtextcontent.UpdateRichTextContentReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个富文本内容
+ tags:
+ - 富文本内容
+ /api/web/role/add:
+ post:
+ consumes:
+ - application/json
+ description: 角色-添加
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleAddResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-添加
+ tags:
+ - 角色
+ /api/web/role/del:
+ post:
+ consumes:
+ - application/json
+ description: 角色-删除
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleDelResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-删除
+ tags:
+ - 角色
+ /api/web/role/drop:
+ post:
+ consumes:
+ - application/json
+ description: 角色-下拉列表
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleDropResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-下拉列表
+ tags:
+ - 角色
+ /api/web/role/enable:
+ post:
+ consumes:
+ - application/json
+ description: 角色-使能
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleEnableReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleEnableResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-使能
+ tags:
+ - 角色
+ /api/web/role/getauth:
+ get:
+ consumes:
+ - application/json
+ description: 角色-获取权限
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleGetAuthReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleGetAuthResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-获取权限
+ tags:
+ - 角色
+ /api/web/role/getinfo:
+ get:
+ consumes:
+ - application/json
+ description: 角色-获取信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleGetInfoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleGetInfoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-获取信息
+ tags:
+ - 角色
+ /api/web/role/list:
+ get:
+ consumes:
+ - application/json
+ description: 角色-列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/pubdef.Page'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-列表
+ tags:
+ - 角色
+ /api/web/role/up:
+ post:
+ consumes:
+ - application/json
+ description: 角色-更新
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleUpReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleUpResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-更新
+ tags:
+ - 角色
+ /api/web/role/upauth:
+ get:
+ consumes:
+ - application/json
+ description: 角色-更新权限
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleUpAuthReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_acc.MwRoleUpAuthResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 角色-更新权限
+ tags:
+ - 角色
+ /api/web/slug/sync:
+ post:
+ consumes:
+ - application/json
+ description: 为视频、分类、女优、作者、短剧、统一标签等空 slug 按 SEO 关键词或名称自动生成。可重复调用,hasMore 为 true
+ 时继续请求。type 为空处理全部,也可指定 video/videoCategory/actress/videoCreater/playlet/playletCategory/playletTheme/contentTag。
+ parameters:
+ - description: 同步参数
+ in: body
+ name: param
+ schema:
+ $ref: '#/definitions/proto.SlugSyncReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.SlugSyncResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 同步历史空 slug
+ tags:
+ - web slug相关接口
+ /api/web/stat/analytics:
+ post:
+ consumes:
+ - application/json
+ description: 统计数据
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_translationcategory.TranslationCategoryResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 统计数据
+ tags:
+ - 获取统计数据
+ /api/web/stat/daily/analytics:
+ post:
+ consumes:
+ - application/json
+ description: 统计数据
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_translationcategory.TranslationCategoryResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 统计数据
+ tags:
+ - 获取统计数据
+ /api/web/syscfg/get:
+ get:
+ consumes:
+ - application/json
+ description: 获取系统配置
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_cfg.GetSysConfigReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_cfg.GetSysConfigResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 获取系统配置
+ tags:
+ - 系统配置
+ /api/web/syscfg/update:
+ post:
+ consumes:
+ - application/json
+ description: 修改系统配置
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/web_cfg.SysConfigUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/web_cfg.SysConfigUpdateResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 修改系统配置
+ tags:
+ - 系统配置
+ /api/web/translation/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个导航跳转信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/translationmod.TranslationInfo'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个导航跳转信息
+ tags:
+ - 导航跳转信息
+ /api/web/translation/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个导航跳转信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_translation.TranslationInfoDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个导航跳转信息
+ tags:
+ - 导航跳转信息
+ /api/web/translation/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取导航跳转信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_translation.TranslationInfoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_translation.TranslationInfoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取导航跳转信息
+ tags:
+ - 导航跳转信息
+ /api/web/translation/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个导航跳转信息
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_translation.TranslationInfoUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个导航跳转信息
+ tags:
+ - 导航跳转信息
+ /api/web/translationcategory/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个导航分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/translationcategorymod.TranslationCategory'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个导航分类
+ tags:
+ - 导航分类
+ /api/web/translationcategory/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个导航分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_translationcategory.TranslationCategoryDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个导航分类
+ tags:
+ - 导航分类
+ /api/web/translationcategory/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取导航分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_translationcategory.TranslationCategoryReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_translationcategory.TranslationCategoryResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取导航分类
+ tags:
+ - 导航分类
+ /api/web/translationcategory/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个导航分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_translationcategory.TranslationCategoryUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个导航分类
+ tags:
+ - 导航分类
+ /api/web/user/list:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 查询参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_user.UserListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_user.UserListResp'
+ security:
+ - ApiKeyAuth: []
+ summary: 查询用户列表
+ tags:
+ - 用户管理
+ /api/web/user/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_user.UserUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新用户信息
+ tags:
+ - 用户管理
+ /api/web/videocategory/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个视频分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/videocategorymod.VideoCategory'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个视频分类
+ tags:
+ - 视频分类
+ /api/web/videocategory/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个视频分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocategory.VideoCategoryDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个视频分类
+ tags:
+ - 视频分类
+ /api/web/videocategory/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取视频分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocategory.VideoCategoryReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videocategory.VideoCategoryResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取视频分类
+ tags:
+ - 视频分类
+ /api/web/videocategory/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个视频分类
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocategory.VideoCategoryUpdate'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个视频分类
+ tags:
+ - 视频分类
+ /api/web/videocollection/media/list:
+ post:
+ consumes:
+ - application/json
+ description: 获取合集视频列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocollection.GetCollectionMediaListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videocollection.GetCollectionMediaListResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 获取合集视频列表
+ tags:
+ - 合集管理
+ /api/web/videocollection/media/option:
+ post:
+ consumes:
+ - application/json
+ description: 修改合集里的视频列表
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocollection.VideoCollectionOptionMediaReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 修改合集里的视频列表
+ tags:
+ - 合集管理
+ /api/web/videocollection/media/rank:
+ post:
+ consumes:
+ - application/json
+ description: 修改合集里的视频排序
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocollection.UpdateCollectionMediaRankReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 修改合集里的视频排序
+ tags:
+ - 合集管理
+ /api/web/videocreater/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个视频作者
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/videocreatermod.VideoCreaterInfo'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个视频作者
+ tags:
+ - 视频作者
+ /api/web/videocreater/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个视频作者
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocreater.VideoCreaterDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个视频作者
+ tags:
+ - 视频作者
+ /api/web/videocreater/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取视频作者
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocreater.VideoCreaterReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videocreater.VideoCreaterResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取视频作者
+ tags:
+ - 视频作者
+ /api/web/videocreater/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个视频作者
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videocreater.VideoCreaterUpdate'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个视频作者
+ tags:
+ - 视频作者
+ /api/web/videoinfo/addTags:
+ post:
+ consumes:
+ - application/json
+ description: 根据视频ID集合批量追加统一标签,已存在的标签不会重复追加
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.VideoAddTagsReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 批量追加视频标签 -- by 王鸣
+ tags:
+ - 视频管理
+ /api/web/videoinfo/addcoll:
+ post:
+ consumes:
+ - application/json
+ description: 新增一个视频合集
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.AddVideoCollectionReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 新增一个视频合集
+ tags:
+ - 视频管理
+ /api/web/videoinfo/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个视频管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.VideoInfoDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个视频管理
+ tags:
+ - 视频管理
+ /api/web/videoinfo/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取视频管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.VideoInfoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videoinfo.VideoInfoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取视频管理
+ tags:
+ - 视频管理
+ /api/web/videoinfo/recommend/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据视频ID集合批量更新 video_information 集合 isRecommend 字段,传1个ID表示单条设置,传多个ID表示批量设置
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.SetRecommendReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 单/批量设置视频是否推荐 -- by 王鸣
+ tags:
+ - 视频管理
+ /api/web/videoinfo/setFakeLikes:
+ post:
+ consumes:
+ - application/json
+ description: 根据ids批量随机设置视频fake.likes,随机范围包含最小值和最大值
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.SetFakeLikesReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videoinfo.SetFakeCountResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 单/批量设置视频假点赞数 --by 旺鸣
+ tags:
+ - 视频管理
+ /api/web/videoinfo/setFakeWatchTimes:
+ post:
+ consumes:
+ - application/json
+ description: 根据ids批量随机设置视频fake.watchTimes,随机范围包含最小值和最大值
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.SetFakeWatchTimesReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videoinfo.SetFakeCountResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 单/批量设置视频假观看次数 --by 旺鸣
+ tags:
+ - 视频管理
+ /api/web/videoinfo/sync:
+ post:
+ consumes:
+ - application/json
+ description: 从切片列表同步多个视频
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.AddVideoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 从切片列表同步多个视频
+ tags:
+ - 视频管理
+ /api/web/videoinfo/syncMediaFile:
+ post:
+ consumes:
+ - application/json
+ description: 从媒资库直同步到视频列表,后续切片同步时按 mediaSourceId 覆盖
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.SyncMediaFileReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 从媒资库直同步到视频列表
+ tags:
+ - 视频管理
+ /api/web/videoinfo/tag/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据视频ID集合批量覆盖 video_information 集合 tags 字段,传1个ID表示单条设置,传多个ID表示批量设置,tagIds传空数组表示清空标签
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.VideoSetTagsReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 单/批量设置视频标签 -- by 王鸣
+ tags:
+ - 视频管理
+ /api/web/videoinfo/transcode:
+ post:
+ responses: {}
+ summary: 提交视频 H265 转码
+ tags:
+ - 视频管理
+ /api/web/videoinfo/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个视频管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videoinfo.VideoInfoUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个视频管理
+ tags:
+ - 视频管理
+ /api/web/videotag/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个标签管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/videotagmod.VideoTagInfo'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个标签管理
+ tags:
+ - 标签管理
+ /api/web/videotag/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个标签管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videotag.VideoTagInfoDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个标签管理
+ tags:
+ - 标签管理
+ /api/web/videotag/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取标签管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videotag.VideoTagInfoReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videotag.VideoTagInfoResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取标签管理
+ tags:
+ - 标签管理
+ /api/web/videotag/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个标签管理
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videotag.VideoTagInfoUpdate'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个标签管理
+ tags:
+ - 标签管理
+ /wbapi/hjsq/hottag:
+ get:
+ consumes:
+ - application/json
+ description: 根据视频累计播放量聚合返回热门标签列表
+ parameters:
+ - description: 返回条数,默认10,最大1000
+ in: query
+ name: limit
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videotag.HotTagResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ summary: 查询播放量最多的标签
+ tags:
+ - 标签管理
+ /wbapi/hjsq/ipstat:
+ get:
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videotag.IpStatReq'
+ summary: 查询今日和昨日去重访客IP数
+ tags:
+ - IP统计
+schemes:
+- http
+- https
+securityDefinitions:
+ ApiKeyAuth:
+ in: header
+ name: Authorization
+ type: apiKey
+ BasicAuth:
+ type: basic
+ OAuth2AccessCode:
+ authorizationUrl: https://example.com/oauth/authorize
+ flow: accessCode
+ scopes:
+ admin: Grants read and write access to administrative information
+ tokenUrl: https://example.com/oauth/token
+ type: oauth2
+ OAuth2Application:
+ flow: application
+ scopes:
+ admin: Grants read and write access to administrative information
+ write: Grants write access
+ tokenUrl: https://example.com/oauth/token
+ type: oauth2
+ OAuth2Implicit:
+ authorizationUrl: https://example.com/oauth/authorize
+ flow: implicit
+ scopes:
+ admin: Grants read and write access to administrative information
+ write: Grants write access
+ type: oauth2
+ OAuth2Password:
+ flow: password
+ scopes:
+ admin: Grants read and write access to administrative information
+ read: Grants read access
+ write: Grants write access
+ tokenUrl: https://example.com/oauth/token
+ type: oauth2
+swagger: "2.0"
diff --git a/web/main.go b/web/main.go
new file mode 100644
index 0000000..964dc70
--- /dev/null
+++ b/web/main.go
@@ -0,0 +1,66 @@
+//go:generate swag init --parseDependency --parseDepth 4
+package main
+
+import (
+ "flag"
+ "os"
+ "videoweb/pub/common/srvversion"
+ "videoweb/web/server"
+)
+
+// @title 站群后台接口
+// @version 1.0
+// @description 这是站群后台接口服务.
+// @termsOfService
+
+// @contact.name
+// @contact.url
+// @contact.email
+
+// @license.name Apache 2.0
+// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
+
+// @host localhost:12667
+// @BasePath /
+// @Schemes http https
+
+// @securityDefinitions.basic BasicAuth
+
+// @securityDefinitions.apikey ApiKeyAuth
+// @in header
+// @name Authorization
+
+// @securitydefinitions.oauth2.application OAuth2Application
+// @tokenUrl https://example.com/oauth/token
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.implicit OAuth2Implicit
+// @authorizationUrl https://example.com/oauth/authorize
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.password OAuth2Password
+// @tokenUrl https://example.com/oauth/token
+// @scope.read Grants read access
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
+// @tokenUrl https://example.com/oauth/token
+// @authorizationUrl https://example.com/oauth/authorize
+// @scope.admin Grants read and write access to administrative information
+
+func cmdLine() {
+ info := flag.Bool("info", false, "show serivce build info")
+ //flagHander := flag.String("handler", "rpc", "set commuic ation type example rpc or mdns, default is rpc")
+ flag.Parse()
+ if *info {
+ srvversion.Info() //内部会自动退进程.
+ }
+}
+
+func main() {
+ cmdLine()
+ server.Run(os.Args)
+}
diff --git a/web/middleware/api_security/body_dump.go b/web/middleware/api_security/body_dump.go
new file mode 100644
index 0000000..a9d000e
--- /dev/null
+++ b/web/middleware/api_security/body_dump.go
@@ -0,0 +1,32 @@
+package api_security
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+func BodyDump() gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+ // fmt.Println("=========================== 1")
+
+ // fmt.Printf("%+v\n", ctx.Request.Header)
+
+ // data, e := ioutil.ReadAll(ctx.Request.Body)
+ // if e != nil {
+ // ctx.AbortWithStatusJSON(500, "protosign: read data failed")
+ // return
+ // }
+
+ // fmt.Println("=========================== 3")
+ // fmt.Println(hex.Dump(data))
+
+ // fmt.Println("=========================== 4")
+ // ctx.Request.Body = &signReader{
+ // *bytes.NewReader(data),
+ // ctx.Request.Body,
+ // }
+ // fmt.Println("=========================== 5")
+ // ctx.Next()
+ // fmt.Println("=========================== 6")
+
+ }
+}
diff --git a/web/middleware/api_security/consts.go b/web/middleware/api_security/consts.go
new file mode 100644
index 0000000..0bec3ca
--- /dev/null
+++ b/web/middleware/api_security/consts.go
@@ -0,0 +1,5 @@
+package api_security
+
+const XProtoSign = "X-Proto-Sign"
+const XProtoNonce = "X-Proto-Nonce"
+const XProtoTimestamp = "X-Proto-Timestamp"
diff --git a/web/middleware/api_security/genkey.go b/web/middleware/api_security/genkey.go
new file mode 100644
index 0000000..6f072e9
--- /dev/null
+++ b/web/middleware/api_security/genkey.go
@@ -0,0 +1,25 @@
+package api_security
+
+import (
+ "crypto/sha256"
+ "math/rand"
+)
+
+func GenerateKey(secret string) []byte {
+ key := sha256.Sum256([]byte(secret))
+ return key[:]
+}
+
+func GenerateIV(secret, salt string) []byte {
+ return GenerateKey(secret + salt)[:16]
+}
+
+const strPool = "012345679qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM"
+
+func GenerateIVSalt() string {
+ var n [16]byte
+ for i := 0; i < 16; i++ {
+ n[i] = byte(strPool[rand.Intn(len(strPool))])
+ }
+ return string(n[:])
+}
diff --git a/web/middleware/api_security/genkey_test.go b/web/middleware/api_security/genkey_test.go
new file mode 100644
index 0000000..261c7be
--- /dev/null
+++ b/web/middleware/api_security/genkey_test.go
@@ -0,0 +1,16 @@
+package api_security
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestGenerateSalt(t *testing.T) {
+ for i := 0; i < 5; i++ {
+ fmt.Println(GenerateIVSalt())
+ }
+}
+
+func BenchmarkGenerateSalt(b *testing.B) {
+ GenerateIV("asdfasdfasdf", GenerateIVSalt())
+}
diff --git a/web/middleware/api_security/sign.go b/web/middleware/api_security/sign.go
new file mode 100644
index 0000000..0596407
--- /dev/null
+++ b/web/middleware/api_security/sign.go
@@ -0,0 +1,95 @@
+package api_security
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "sort"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+type signReader struct {
+ bytes.Reader
+ RC io.ReadCloser
+}
+
+func (thiz *signReader) Close() error {
+ return thiz.RC.Close()
+}
+
+func calcSign(kv map[string][]byte) string {
+ keys := make([]string, 0, len(kv))
+ for k := range kv {
+ keys = append(keys, k)
+ }
+
+ sort.Strings(keys)
+
+ h := sha256.New()
+ for _, k := range keys {
+ _, _ = h.Write([]byte(k))
+ _, _ = h.Write(kv[k])
+ }
+
+ return strings.ToLower(hex.EncodeToString(h.Sum(nil)))
+}
+
+func ProtoSign(secret []byte,
+ shouldCrypto ShouldCryptoChecker) gin.HandlerFunc {
+ return func(ctx *gin.Context) {
+
+ if shouldCrypto != nil && !shouldCrypto(ctx) {
+ ctx.Next()
+ return
+ }
+
+ sign := ctx.GetHeader(XProtoSign)
+ nonce := ctx.GetHeader(XProtoNonce)
+ ts := ctx.GetHeader(XProtoTimestamp)
+
+ if len(sign) != 64 || len(nonce) < 10 || len(ts) < 6 {
+ ctx.AbortWithStatusJSON(500, "protosign: argument invalid")
+ return
+ }
+
+ data, e := ioutil.ReadAll(ctx.Request.Body)
+ if e != nil {
+ ctx.AbortWithStatusJSON(500, "protosign: read data failed")
+ return
+ }
+
+ kv := make(map[string][]byte)
+ kv[XProtoNonce] = []byte(nonce)
+ kv[XProtoTimestamp] = []byte(ts)
+ kv["_METHOD_"] = []byte(ctx.Request.Method)
+ kv["_PATH_"] = []byte(ctx.Request.URL.Path)
+ kv["_SECRET_"] = secret
+
+ queries := map[string][]string(ctx.Request.URL.Query())
+ for k, v := range queries {
+ if len(v) > 0 {
+ kv[fmt.Sprintf("_QUERY_%s_", strings.ToUpper(k))] = []byte(v[0])
+ }
+ }
+
+ if len(data) > 0 {
+ kv["_BODY_"] = []byte(data)
+ }
+
+ if calcSign(kv) != sign {
+ ctx.AbortWithStatusJSON(500, "protosign: verify sign failed")
+ return
+ }
+
+ ctx.Request.Body = &signReader{
+ *bytes.NewReader(data),
+ ctx.Request.Body,
+ }
+ ctx.Next()
+ }
+}
diff --git a/web/middleware/api_security/sign_test.go b/web/middleware/api_security/sign_test.go
new file mode 100644
index 0000000..14e1cbc
--- /dev/null
+++ b/web/middleware/api_security/sign_test.go
@@ -0,0 +1,16 @@
+package api_security
+
+import (
+ "fmt"
+ "net/url"
+ "testing"
+)
+
+func TestUrl(t *testing.T) {
+
+ u, _ := url.Parse("http://a.b.com/api/a/b/c?a=%20123")
+ fmt.Printf("path: %s\n", u.Path)
+ fmt.Printf("rawpath: %s\n", u.RawQuery)
+ fmt.Printf("escapedpath: %s\n", u.EscapedPath())
+
+}
diff --git a/web/middleware/api_security/types.go b/web/middleware/api_security/types.go
new file mode 100644
index 0000000..9bbc702
--- /dev/null
+++ b/web/middleware/api_security/types.go
@@ -0,0 +1,38 @@
+package api_security
+
+import (
+ "crypto/cipher"
+ "io"
+
+ "github.com/gin-gonic/gin"
+)
+
+type ShouldCryptoChecker func(ctx *gin.Context) bool
+
+type cryptoReader struct {
+ S cipher.Stream
+ RC io.ReadCloser
+}
+
+func (r *cryptoReader) Read(dst []byte) (n int, err error) {
+ n, err = r.RC.Read(dst)
+ r.S.XORKeyStream(dst[:n], dst[:n])
+ return
+}
+
+func (r *cryptoReader) Close() error {
+ return r.RC.Close()
+}
+
+type cryptoWriter struct {
+ gin.ResponseWriter
+ writer *cipher.StreamWriter
+}
+
+func (g *cryptoWriter) WriteString(s string) (int, error) {
+ return g.writer.Write([]byte(s))
+}
+
+func (g *cryptoWriter) Write(data []byte) (int, error) {
+ return g.writer.Write(data)
+}
diff --git a/web/middleware/apilimitHandler/aiplimit.go b/web/middleware/apilimitHandler/aiplimit.go
new file mode 100644
index 0000000..a413e7e
--- /dev/null
+++ b/web/middleware/apilimitHandler/aiplimit.go
@@ -0,0 +1,84 @@
+package apilimitHandler
+
+import (
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+type counter struct {
+ Count int
+ WindowStart time.Time
+}
+
+type RouteHourlyLimiter struct {
+ mu sync.Mutex
+ limit int
+ window time.Duration
+ counters map[string]*counter
+}
+
+func NewRouteHourlyLimiter(limit int) *RouteHourlyLimiter {
+ l := &RouteHourlyLimiter{
+ limit: limit,
+ window: time.Hour,
+ counters: make(map[string]*counter),
+ }
+ go l.cleaner()
+ return l
+}
+
+func (l *RouteHourlyLimiter) Middleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // 用 FullPath 做“接口维度”,例如 /api/user/:id
+ route := c.FullPath()
+ if route == "" {
+ // 路由未匹配时兜底
+ route = c.Request.URL.Path
+ }
+
+ now := time.Now()
+
+ l.mu.Lock()
+ defer l.mu.Unlock()
+
+ item, ok := l.counters[route]
+ if !ok || now.Sub(item.WindowStart) >= l.window {
+ l.counters[route] = &counter{
+ Count: 1,
+ WindowStart: now,
+ }
+ c.Next()
+ return
+ }
+
+ if item.Count >= l.limit {
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
+ "code": http.StatusTooManyRequests,
+ "message": "rate limit exceeded: max 100 requests per route per hour",
+ })
+ return
+ }
+
+ item.Count++
+ c.Next()
+ }
+}
+
+// 定时清理过期窗口,避免 map 无限增长
+func (l *RouteHourlyLimiter) cleaner() {
+ ticker := time.NewTicker(10 * time.Minute)
+ defer ticker.Stop()
+
+ for now := range ticker.C {
+ l.mu.Lock()
+ for k, v := range l.counters {
+ if now.Sub(v.WindowStart) >= l.window {
+ delete(l.counters, k)
+ }
+ }
+ l.mu.Unlock()
+ }
+}
diff --git a/web/middleware/auth/http.go b/web/middleware/auth/http.go
new file mode 100644
index 0000000..c59be6b
--- /dev/null
+++ b/web/middleware/auth/http.go
@@ -0,0 +1,298 @@
+package auth
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "sync"
+ "videoweb/pub/common"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/oplog"
+ "videoweb/web/webg"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+var (
+ RoleApiMenus = map[int64][]string{} // 角色Api
+ mutex sync.RWMutex
+)
+
+var (
+ tokener = utils.NewTokenRedis()
+)
+
+const (
+ AccId = "ACC_ID"
+ roleAuthAdrr = "/web/role/getauth"
+)
+
+type APIInfo struct {
+ Api string
+ Desc string
+}
+
+var (
+ // 白名单,不需要验证token的api
+ whiteList = []string{
+ "/swagger",
+ "/api/web/file/uploadImg",
+ "/api/web/file/uploadImgs",
+ "/api/web/media/videoBody",
+ "/api/web/media/videoOk",
+ "/api/web/m3u8",
+ "/api/web/uploader/m3u8",
+ "/api/web/enkey",
+ "/api/web/login/acc",
+ "/api/web/login/bind",
+ "/api/web/login/slogin",
+ "/api/web/stat/media/list",
+ "/api/web/stat/analytics",
+ "/api/web/stat/daily/analytics",
+ "/api/web/sync/video/list",
+ "/api/web/sync/videocollection/list",
+ "/api/web/sync/category/list",
+ "/api/web/sync/creator/list",
+ "/api/web/sync/tag/list",
+ "/api/web/contentexcel",
+ "/api/web/videoinfo/fillZeroFakeFlow",
+ "/wbapi",
+ }
+)
+
+// 读取角色信息
+func GetRoleApiMenus(ctx context.Context, accid int64) (roles []string, err error) {
+ mutex.RLock()
+ roles, ok := RoleApiMenus[accid]
+ mutex.RUnlock()
+ if !ok {
+ accInfo, dataErr := mwacc.FindOneAccByFilter(nil, bson.M{mwacc.FN.MwAcc.Id: accid})
+ if dataErr != nil || accInfo.Id == 0 {
+ return roles, dataErr
+ }
+
+ roleInfo, roleErr := mwacc.FindOneRoleByFilter(ctx, bson.M{mwacc.FN.MwRole.Id: accInfo.RoleId})
+
+ if roleErr != nil || roleInfo.Id == 0 {
+ return roles, roleErr
+ }
+
+ if roleInfo.Name == "超级管理员" {
+ return nil, nil
+ }
+
+ mutex.Lock()
+ RoleApiMenus[accid] = roleInfo.ApiMenus
+ mutex.Unlock()
+ roles = roleInfo.ApiMenus
+ }
+ return
+}
+
+// 是否角色api
+func HasRoleApi(path string, apis []string) bool {
+ apimap := make(map[string]struct{})
+ for _, v := range apis {
+ apimap[v] = struct{}{}
+ }
+ _, ok := apimap[path]
+ return ok
+}
+
+// 角色权限
+func handleRoles(ctx *gin.Context) (int, error) {
+ path := ctx.Request.URL.Path
+ if strings.HasPrefix(path, roleAuthAdrr) {
+ return 0, nil
+ }
+
+ accid := ctx.GetInt(AccId)
+ apis, err := GetRoleApiMenus(ctx, int64(accid))
+ if err != nil {
+ return int(errors.RoleErr), err
+ }
+ if apis == nil {
+ return 0, nil
+ }
+ has := HasRoleApi(path, apis)
+ if !has {
+ return int(errors.RoleErr), srvdef.New(srvdef.RoleErr, (srvdef.RoleErr).Msg(), "")
+ }
+ return 0, nil
+}
+
+// 楼风经纪人权限
+func handleLouFengBossRoles(ctx *gin.Context) (int, error) {
+ path := ctx.Request.URL.Path
+ if strings.HasPrefix(path, roleAuthAdrr) {
+ return 0, nil
+ }
+
+ accid := ctx.GetInt64(AccId)
+ accInfo, dataErr := mwacc.FindOneAccByFilter(nil, bson.M{mwacc.FN.MwAcc.Id: accid})
+ if dataErr != nil || accInfo.Id == 0 {
+ return int(errors.RoleErr), dataErr
+ }
+
+ return 0, nil
+}
+
+// 验证后台账号IP
+func authMwIP(ctx *gin.Context) (int, error) {
+
+ cIP := ctx.ClientIP()
+ accid := ctx.GetInt64(AccId)
+ accInfo, dataErr := mwacc.FindOneAccByFilter(nil, bson.M{mwacc.FN.MwAcc.Id: accid})
+ if dataErr != nil || accInfo.Id == 0 {
+ return int(errors.RoleErr), dataErr
+ }
+ if len(accInfo.LimitIP) != 0 {
+ for _, v := range accInfo.LimitIP {
+ if v == cIP {
+ return 0, nil
+ }
+ }
+ return int(errors.LimitIPErr), dataErr
+ } else {
+ return int(errors.LimitIPErr), dataErr
+ }
+
+ // return 0, nil
+}
+
+// 验证
+func Auth(ctx *gin.Context) {
+ //for test
+ //ctx.Set(AccId, int64(1000))
+ //return
+ p := ctx.Request.URL.Path
+ if strings.Contains(p, "swagger") {
+ return
+ }
+ for _, w := range whiteList {
+ if strings.HasPrefix(ctx.Request.URL.Path, w) {
+ return
+ }
+ }
+ if v, ok := common.ApiSetMaps[p]; ok {
+ fmt.Println(v.IsWhite)
+ if v.IsWhite {
+ return
+ }
+ }
+
+ doToken := func() (int, error) {
+ checkToken := ctx.Request.Header.Get("Authorization")
+ // 去掉Bearer,如果有
+ checkToken = utils.StripBearerPrefixFromTokenString(checkToken)
+
+ if checkToken == "" {
+ return http.StatusUnauthorized, srvdef.New(srvdef.NoToken, (srvdef.NoToken).Msg(), "")
+ }
+
+ has, err := mwacc.CheckToken(ctx, checkToken)
+ if err != nil {
+ return http.StatusUnauthorized, err
+ }
+ if !has {
+ return http.StatusUnauthorized, srvdef.New(srvdef.ReLogin, (srvdef.ReLogin).Msg(), "")
+ }
+
+ userId, _, err := utils.ParseUserToken(checkToken, webg.Conf.JwtKey)
+ if err != nil {
+ return http.StatusUnauthorized, srvdef.New(srvdef.InvalidToken, (srvdef.InvalidToken).Msg(), "")
+ }
+ // log.Info("Auth test", log.Any("checkToken", checkToken), log.Any("userId", userId))
+ ctx.Set(AccId, userId)
+ return 0, nil
+ }
+
+ statusCode, err := doToken()
+ if err != nil || statusCode != 0 {
+ ctx.AbortWithStatusJSON(statusCode, err)
+ return
+ }
+
+ roleCode, err := handleRoles(ctx)
+ if err != nil || roleCode != 0 {
+ ctx.AbortWithStatusJSON(http.StatusForbidden, err)
+ }
+
+ roleCode1, err1 := handleLouFengBossRoles(ctx)
+ if err1 != nil || roleCode1 != 0 {
+ ctx.AbortWithStatusJSON(http.StatusForbidden, err)
+ }
+
+ // authIPCode, err2 := authMwIP(ctx)
+ // if err2 != nil || authIPCode != 0 {
+ // ctx.AbortWithStatusJSON(http.StatusForbidden, err)
+ // }
+}
+
+// func userAgentHandler(ctx *gin.Context) {
+// userAgent := ctx.Request.Header.Get("User-Agent")
+// if userAgent != "" {
+// fields := strings.Fields(userAgent)
+// curVersion := fields[len(fields)-1]
+// if strings.HasPrefix(curVersion, "version:") {
+// versionNum := strings.Split(curVersion, ":")[1]
+// ctx.Set("VERSION", versionNum)
+// } else {
+// ctx.Set("VERSION", "")
+// }
+// }
+// if strings.Contains(userAgent, "Android") {
+// ctx.Set("SYSTEM", "android")
+// }
+// if strings.Contains(userAgent, "iPhone") {
+// ctx.Set("SYSTEM", "ios")
+// }
+
+// // 增加 不签名,不加密 开关
+// }
+
+// 操作日志
+func OperateLog(ctx *gin.Context) {
+ desc := ""
+ find := false
+ for _, w := range LogList {
+ if ctx.Request.URL.Path == w.Api {
+ find = true
+ desc = w.Desc
+ }
+ }
+ if !find {
+ ctx.Next()
+ return
+ }
+
+ accId := ctx.GetInt64(AccId)
+ rawData, err := ctx.GetRawData()
+ ip := ctx.ClientIP()
+ if err != nil {
+ ctx.AbortWithStatusJSON(400, err)
+ }
+
+ log.Info("OperateLog Test", log.Any("vmap.Path", ctx.Request.URL.Path))
+
+ err = oplog.AddLog(ctx, ctx.Request.URL.Path, desc, accId, string(rawData), ip)
+ if err != nil {
+ ctx.AbortWithStatusJSON(int(errors.OperateLog), err)
+ return
+ }
+
+ ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData)) // 将取出来的执行条件放回ctx中
+ ctx.Next()
+}
+
+func GetAccount(ctx *gin.Context) int64 {
+ return ctx.GetInt64(AccId)
+}
diff --git a/web/middleware/auth/log_list.go b/web/middleware/auth/log_list.go
new file mode 100644
index 0000000..3587164
--- /dev/null
+++ b/web/middleware/auth/log_list.go
@@ -0,0 +1,94 @@
+package auth
+
+var LogList = []APIInfo{
+ {"/api/web/acc/add", "账号-添加"},
+ {"/api/web/acc/del", "账号-删除"},
+ {"/api/web/acc/up", "账号-更新"},
+ {"/api/web/acc/savestatus", "账号-保存状态"},
+ {"/api/web/acc/uppwd", "账号-更新密码"},
+ {"/api/web/login/acc", "登录-登录"},
+ {"/api/web/login/slogin", "管理员sso登陆"},
+ {"/api/web/role/add", "角色-添加"},
+ {"/api/web/role/del", "角色-删除"},
+ {"/api/web/role/up", "角色-更新"},
+ {"/api/web/role/enable", "角色-使能"},
+ {"/api/web/role/upauth", "角色-更新权限"},
+ {"/api/web/syscfg/update", "修改系统配置"},
+ {"/api/web/syscfg/appkey", "设置媒资库appKey"},
+ {"/api/web/post/comment/del", "删除一个评论"},
+ {"/api/web/post/comment/user/del", "删除一个用户所有评论"},
+ {"/api/web/post/comment/replay", "回复评论"},
+ {"/api/web/post/comment/reviewBatch", "批量审核评论"},
+ {"/api/web/post/comment/review", "审核评论"},
+ {"/api/web/ipblack/add", "IP黑名单-增加"},
+ {"/api/web/ipblack/del", "IP黑名单-删除"},
+ {"/api/web/richtextcontent/add", "添加一个富文本内容"},
+ {"/api/web/richtextcontent/del", "删除一个富文本内容"},
+ {"/api/web/richtextcontent/update", "更新一个富文本内容"},
+ {"/api/web/translationcategory/add", "添加一个导航分类"},
+ {"/api/web/translationcategory/del", "删除一个导航分类"},
+ {"/api/web/translationcategory/update", "更新一个导航分类"},
+ {"/api/web/translation/add", "添加一个导航跳转信息"},
+ {"/api/web/translation/del", "删除一个导航跳转信息"},
+ {"/api/web/translation/update", "更新一个导航跳转信息"},
+ {"/api/web/videocategory/add", "添加一个视频分类"},
+ {"/api/web/videocategory/del", "删除一个视频分类"},
+ {"/api/web/videocategory/update", "更新一个视频分类"},
+ {"/api/web/videocreater/add", "添加一个视频作者"},
+ {"/api/web/videocreater/del", "删除一个视频作者"},
+ {"/api/web/videocreater/update", "更新一个视频作者"},
+ {"/api/web/videotag/add", "添加一个标签管理"},
+ {"/api/web/videotag/del", "删除一个标签管理"},
+ {"/api/web/videotag/update", "更新一个标签管理"},
+ {"/api/web/videoinfo/del", "删除一个视频管理"},
+ {"/api/web/videoinfo/update", "更新一个视频管理"},
+ {"/api/web/videoinfo/recommend/set", "视频-设置推荐"},
+ {"/api/web/videoinfo/heat/set", "视频-设置热度"},
+ {"/api/web/videocollection/media/option", "修改合集里的视频列表"},
+ {"/api/web/videocollection/media/rank", "修改合集里的视频排序"},
+ {"/api/web/videoinfo/addcoll", "新增一个视频合集"},
+ {"/api/web/videoinfo/addTags", "视频-批量追加标签"},
+ {"/api/web/videoinfo/fillZeroFakeFlow", "批量填充低播放量/低收藏视频假数据"},
+ {"/api/web/actress/add", "添加一个女优信息"},
+ {"/api/web/actress/del", "删除一个女优信息"},
+ {"/api/web/actress/update", "更新一个女优信息"},
+ {"/api/web/playlet/add", "短剧-新增"},
+ {"/api/web/playlet/update", "短剧-编辑"},
+ {"/api/web/playlet/del", "短剧-删除"},
+ {"/api/web/playlet/status/set", "短剧-设置上下架"},
+ {"/api/web/playlet/recommend/set", "短剧-设置推荐"},
+ {"/api/web/playlet/featured/set", "短剧-设置爆剧"},
+ {"/api/web/playlet/banner/set", "短剧-设置banner展示"},
+ {"/api/web/playlet/theme/set", "短剧-设置题材"},
+ {"/api/web/playlet/tag/set", "短剧-设置标签"},
+ {"/api/web/playlet/like/virtual/set", "短剧-设置虚拟点赞数"},
+ {"/api/web/playlet/browse/virtual/set", "短剧-设置虚拟观看数"},
+ {"/api/web/playlet/heat/virtual/set", "短剧-设置虚拟热度"},
+ {"/api/web/playlet/pay-type/set", "短剧-设置支付方式"},
+ {"/api/web/playlet/watermark/reset", "短剧-重置水印"},
+ {"/api/web/playlet/chapter/add", "短剧剧集-新增"},
+ {"/api/web/playlet/chapter/update", "短剧剧集-更新"},
+ {"/api/web/playlet/chapter/del", "短剧剧集-删除"},
+ {"/api/web/playlet/chapter/pay-type/set", "短剧剧集-设置支付方式"},
+ {"/api/web/playlet/chapter/sync", "短剧剧集-手动同步"},
+ {"/api/web/playlet/category/set", "短剧-设置分类"},
+ {"/api/web/playlet/category/add", "短剧分类-新增"},
+ {"/api/web/playlet/category/update", "短剧分类-更新"},
+ {"/api/web/playlet/category/del", "短剧分类-删除"},
+ {"/api/web/playlet/home-config/add", "短剧首页配置-新增"},
+ {"/api/web/playlet/home-config/update", "短剧首页配置-更新"},
+ {"/api/web/playlet/home-config/del", "短剧首页配置-删除"},
+ {"/api/web/playlet/theme/add", "短剧题材-新增"},
+ {"/api/web/playlet/theme/update", "短剧题材-更新"},
+ {"/api/web/playlet/theme/del", "短剧题材-删除"},
+ {"/api/web/home/banner/add", "首页banner-新增"},
+ {"/api/web/home/banner/update", "首页banner-更新"},
+ {"/api/web/home/banner/del", "首页banner-删除"},
+ {"/api/web/announcement/add", "公告-增加"},
+ {"/api/web/announcement/del", "公告-删除"},
+ {"/api/web/announcement/update", "公告-更新"},
+ {"/api/web/content/tag/add", "统一标签-新增"},
+ {"/api/web/content/tag/update", "统一标签-更新"},
+ {"/api/web/content/tag/del", "统一标签-删除"},
+ {"/api/web/slug/sync", "同步历史空slug"},
+}
diff --git a/web/middleware/cors/cors.go b/web/middleware/cors/cors.go
new file mode 100644
index 0000000..c8519b8
--- /dev/null
+++ b/web/middleware/cors/cors.go
@@ -0,0 +1,135 @@
+package cors
+
+import (
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+type Options struct {
+ Origin *[]string // 传入空指针,表示允许"*", 传入空数组表示禁止跨域
+ Methods []string // Methods
+ AllowedHeaders []string // AllowHeaders
+ ExposedHeaders []string // exposeHeader
+ Credentials bool // cookie
+ MaxAge int64 // 缓存MaxAge
+ PreflightContinue bool // 在遇到Options的时候,继续,而不是返回请求
+ OptionsSuccessStatus int // Options时,返回的状态码,默认204
+}
+
+func configureOrigin(o *Options, c *gin.Context) map[string]string {
+ reqOrigin := c.GetHeader("Origin")
+ headers := make(map[string]string)
+ if o.Origin == nil {
+ headers["Access-Control-Allow-Origin"] = "*"
+ } else {
+ isAllowed := false
+ for _, allowed := range *o.Origin {
+ if allowed == reqOrigin {
+ isAllowed = true
+ break
+ }
+ }
+ if isAllowed {
+ headers["Access-Control-Allow-Origin"] = reqOrigin
+ } else {
+ headers["Access-Control-Allow-Origin"] = strconv.FormatBool(false)
+ }
+ headers["Vary"] = "Origin"
+ }
+ return headers
+}
+
+func configureMethods(o *Options) map[string]string {
+ headers := make(map[string]string)
+ if len(o.Methods) > 0 {
+ headers["Access-Control-Allow-Methods"] = strings.ToUpper(strings.Join(o.Methods, ","))
+ }
+ return headers
+}
+
+func configureCredentials(o *Options) map[string]string {
+ headers := make(map[string]string)
+ if o.Credentials {
+ headers["Access-Control-Allow-Credentials"] = strconv.FormatBool(true)
+ }
+ return headers
+}
+
+func configureAllowedHeaders(o *Options, c *gin.Context) map[string]string {
+ headers := make(map[string]string)
+ if len(o.AllowedHeaders) > 0 {
+ headers["Access-Control-Allow-Headers"] = strings.Join(o.AllowedHeaders, ",")
+ }
+ return headers
+}
+
+func configureExposedHeaders(o *Options) map[string]string {
+ var headers = make(map[string]string)
+ if len(o.ExposedHeaders) > 0 {
+ headers["Access-Control-Expose-Headers"] = strings.Join(o.ExposedHeaders, ",")
+ }
+ return headers
+}
+
+func configureMaxAge(o *Options) map[string]string {
+ var headers = make(map[string]string)
+ if o.MaxAge >= 0 {
+ headers["Access-Control-Max-Age"] = strconv.FormatInt(o.MaxAge, 10)
+ }
+ return headers
+}
+
+func mergeMap(m ...map[string]string) map[string]string {
+ r := make(map[string]string)
+ for _, t := range m {
+ for k, v := range t {
+ r[k] = v
+ }
+ }
+ return r
+}
+
+func applyHeader(c *gin.Context, h map[string]string) {
+ for k, v := range h {
+ c.Header(k, v)
+ }
+}
+
+// Cors 跨域处理
+func Cors(o *Options) gin.HandlerFunc {
+ if o.OptionsSuccessStatus == 0 {
+ o.OptionsSuccessStatus = http.StatusNoContent
+ }
+ return func(c *gin.Context) {
+ method := c.Request.Method
+ // c.Header("Access-Control-Allow-Origin", "*")
+ // c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token,Content-Length,Etag,Content-Range,Accept-Ranges,User-Agent,Range,Bucket,Content-Disposition,Signature,X-Forwarded-For,X-Real-Ip")
+ if method == "OPTIONS" {
+ h := mergeMap(
+ configureOrigin(o, c),
+ configureCredentials(o),
+ configureMethods(o),
+ configureAllowedHeaders(o, c),
+ configureMaxAge(o),
+ configureExposedHeaders(o),
+ )
+ applyHeader(c, h)
+ if o.PreflightContinue {
+ c.Next()
+ } else {
+ c.Header("Content-Length", "0")
+ c.AbortWithStatus(o.OptionsSuccessStatus)
+ }
+ } else {
+ h := mergeMap(
+ configureOrigin(o, c),
+ configureCredentials(o),
+ configureExposedHeaders(o),
+ )
+ applyHeader(c, h)
+ c.Next()
+ }
+ }
+}
diff --git a/web/middleware/ipblacklist/ipblacklist.go b/web/middleware/ipblacklist/ipblacklist.go
new file mode 100644
index 0000000..62bded1
--- /dev/null
+++ b/web/middleware/ipblacklist/ipblacklist.go
@@ -0,0 +1,16 @@
+package ipblacklist
+
+import (
+ "errors"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "videoweb/web/service/srv_ipblack"
+)
+
+func IpBlackListMiddleware(ctx *gin.Context) {
+ ip := ctx.ClientIP()
+ if srv_ipblack.CheckIp(ip) {
+ ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
+ return
+ }
+}
\ No newline at end of file
diff --git a/web/middleware/limitHandler/login.go b/web/middleware/limitHandler/login.go
new file mode 100644
index 0000000..a7a21d2
--- /dev/null
+++ b/web/middleware/limitHandler/login.go
@@ -0,0 +1,120 @@
+package limitHandler
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+ "videoweb/pub/common"
+ httpUtil "videoweb/pub/common/http"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/webg"
+
+ "github.com/gin-gonic/gin"
+ "github.com/patrickmn/go-cache"
+)
+
+var limitURI = []string{
+ "/api/web/login/slogin",
+ "/api/web/login/acc",
+}
+
+var (
+ tgBotToken = "2013237388:AAFwvKvEmQfEvcI7wt_64Zkp9KPfgHo8sow"
+ tgGroupId = "-1003735177636"
+)
+
+var (
+ limitDuration = time.Minute
+ limitCount = 5
+)
+
+var memCache = cache.New(1*time.Minute, 10*time.Minute)
+
+// LoginLimit 登陆频率限制:按账号维度限制,不依赖 ClientIP(可被伪造)。
+func LoginLimit(ctx *gin.Context) {
+ path := ctx.Request.URL.Path
+
+ check := false
+ for _, p := range limitURI {
+ if strings.HasPrefix(path, p) {
+ check = true
+ break
+ }
+ }
+ if !check {
+ ctx.Next()
+ return
+ }
+
+ // 测试环境不限制
+ if webg.Conf.RunMod != "prod" {
+ ctx.Next()
+ return
+ }
+
+ var params map[string]interface{}
+ var acc string
+ if ctx.Request.Method == http.MethodPost {
+ raw, _ := ctx.GetRawData()
+ ctx.Request.Body = io.NopCloser(bytes.NewBuffer(raw))
+ if err := json.Unmarshal(raw, ¶ms); err == nil {
+ if a, ok := params["acc"].(string); ok {
+ acc = strings.TrimSpace(a)
+ }
+ }
+ }
+
+ // 无账号时无法按账号限流,交由后续登录逻辑处理参数错误
+ if acc == "" {
+ ctx.Next()
+ return
+ }
+
+ key := fmt.Sprintf("web-limit:%s:%s", path, acc)
+
+ count := 1
+ if aCount, ok := memCache.Get(key); ok {
+ if n, ok2 := aCount.(int); ok2 {
+ count = n + 1
+ }
+ }
+ memCache.Set(key, count, limitDuration)
+
+ if count > limitCount {
+ go func(acc, path string, count int, params map[string]interface{}) {
+ message := fmt.Sprintf(
+ "[91porn精选]\n登陆请求频率过高!\n时间: %s\n请求地址: %s\n账号: %s\n请求参数: %+v\n错误次数: %d",
+ time.Now().Format("2006-01-02 15:04:05"),
+ path,
+ acc,
+ params,
+ count,
+ )
+ // 请求地址/参数为不可信输入,去掉非法 UTF-8 字节;不用 parse_mode,避免 Markdown 把内容解析成实体后报错
+ message = strings.ToValidUTF8(message, "")
+
+ url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tgBotToken)
+ req := map[string]interface{}{
+ "chat_id": tgGroupId,
+ "text": message,
+ }
+ jsonBytes, err := json.Marshal(req)
+ if err != nil {
+ return
+ }
+ header := map[string]string{"Content-Type": "application/json"}
+ resp, _ := httpUtil.Post(url, header, bytes.NewReader(jsonBytes))
+ _ = resp.Data
+ }(acc, path, count, params)
+
+ common.ServerJSON(ctx, srvdef.ErrReqForbidden, nil)
+ ctx.Abort()
+ return
+ }
+
+ ctx.Next()
+}
diff --git a/web/middleware/validator/default-validator.go b/web/middleware/validator/default-validator.go
new file mode 100644
index 0000000..daf78ee
--- /dev/null
+++ b/web/middleware/validator/default-validator.go
@@ -0,0 +1,81 @@
+package validator
+
+import (
+ "reflect"
+ "strings"
+ "sync"
+
+ "github.com/gin-gonic/gin/binding"
+ "github.com/go-playground/locales/en"
+ ut "github.com/go-playground/universal-translator"
+ "gopkg.in/go-playground/validator.v9"
+ en_translations "gopkg.in/go-playground/validator.v9/translations/en"
+)
+
+var (
+ ValidatorUni *ut.UniversalTranslator
+)
+
+type defaultValidator struct {
+ once sync.Once
+ validate *validator.Validate
+}
+
+var v binding.StructValidator = &defaultValidator{}
+
+func init() {
+ v.Engine()
+}
+
+func (v *defaultValidator) ValidateStruct(obj interface{}) error {
+
+ if kindOfData(obj) == reflect.Struct {
+
+ v.lazyinit()
+
+ if err := v.validate.Struct(obj); err != nil {
+ return error(err)
+ }
+ }
+
+ return nil
+}
+
+func (v *defaultValidator) Engine() interface{} {
+ v.lazyinit()
+ return v.validate
+}
+
+func (v *defaultValidator) lazyinit() {
+ v.once.Do(func() {
+ v.validate = validator.New()
+
+ // 依旧使用bingding tag兼容v8
+ v.validate.SetTagName("binding")
+
+ // 错误信息转换
+ en := en.New()
+ ValidatorUni = ut.New(en, en)
+ trans, _ := ValidatorUni.GetTranslator("en")
+ en_translations.RegisterDefaultTranslations(v.validate, trans)
+
+ v.validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
+ name := strings.SplitN(fld.Tag.Get("comment"), ",", 2)[0]
+ if name == "-" {
+ return ""
+ }
+ return name
+ })
+ })
+}
+
+func kindOfData(data interface{}) reflect.Kind {
+
+ value := reflect.ValueOf(data)
+ valueType := value.Kind()
+
+ if valueType == reflect.Ptr {
+ valueType = value.Elem().Kind()
+ }
+ return valueType
+}
diff --git a/web/middleware/validator/v9.go b/web/middleware/validator/v9.go
new file mode 100644
index 0000000..4e2e38e
--- /dev/null
+++ b/web/middleware/validator/v9.go
@@ -0,0 +1,11 @@
+package validator
+
+import (
+ "github.com/gin-gonic/gin/binding"
+)
+
+// 替换gin 默认v8验证器
+// 改为v9
+func init() {
+ binding.Validator = new(defaultValidator)
+}
diff --git a/web/router/actress.go b/web/router/actress.go
new file mode 100644
index 0000000..fda8b14
--- /dev/null
+++ b/web/router/actress.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/web/api/web_actress"
+)
+
+func actressRouter(router *gin.RouterGroup) {
+ post := router.Group("/actress")
+ {
+ post.GET("/list", web_actress.Get)
+ post.POST("/update", web_actress.Update)
+ post.POST("/del", web_actress.Del)
+ post.POST("/add", web_actress.Add)
+ }
+}
diff --git a/web/router/announcement.go b/web/router/announcement.go
new file mode 100644
index 0000000..9efc443
--- /dev/null
+++ b/web/router/announcement.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "videoweb/web/api/web_announcement"
+
+ "github.com/gin-gonic/gin"
+)
+
+func announcementRouter(router *gin.RouterGroup) {
+ group := router.Group("/announcement")
+ {
+ group.POST("/list", web_announcement.AnnouncementList)
+ group.POST("/add", web_announcement.AnnouncementAdd)
+ group.POST("/del", web_announcement.AnnouncementDel)
+ group.POST("/update", web_announcement.AnnouncementUpdate)
+ }
+}
diff --git a/web/router/app.go b/web/router/app.go
new file mode 100644
index 0000000..8c1fad2
--- /dev/null
+++ b/web/router/app.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/web/api/web_app"
+)
+
+func appSyncRouter(router *gin.RouterGroup) {
+ sync := router.Group("/sync")
+ {
+ sync.POST("/video/list", web_app.AppSyncReq)
+ sync.POST("/videocollection/list", web_app.AppCollectionSyncReq)
+ sync.POST("/category/list", web_app.AppGetVideoCategory)
+ sync.POST("/creator/list", web_app.AppGetVideoCreator)
+ sync.POST("/tag/list", web_app.AppGetVideoTag)
+ }
+}
\ No newline at end of file
diff --git a/web/router/ather.go b/web/router/ather.go
new file mode 100644
index 0000000..9d71290
--- /dev/null
+++ b/web/router/ather.go
@@ -0,0 +1,23 @@
+package router
+
+import (
+ webanalytics "videoweb/web/api/web_analytics"
+ "videoweb/web/api/web_cfg"
+
+ "github.com/gin-gonic/gin"
+)
+
+func syscfgRouter(router *gin.RouterGroup) {
+ system := router.Group("/syscfg")
+ {
+ system.GET("/get", web_cfg.GetCfg)
+ system.POST("/update", web_cfg.UpdateCfg)
+ system.POST("/appkey", web_cfg.SetAppKey)
+ }
+
+ stat := router.Group("/stat")
+ {
+ stat.GET("/analytics", webanalytics.GetAnalyticsStat)
+ stat.GET("/daily/analytics", webanalytics.GetAnalyticsDailyStat)
+ }
+}
diff --git a/web/router/commend.go b/web/router/commend.go
new file mode 100644
index 0000000..537056c
--- /dev/null
+++ b/web/router/commend.go
@@ -0,0 +1,19 @@
+package router
+
+import (
+ "videoweb/web/api/web_commend"
+
+ "github.com/gin-gonic/gin"
+)
+
+func commentRouter(router *gin.RouterGroup) {
+ group := router.Group("/post")
+ {
+ group.POST("/comment/list", web_commend.SearchManyCommend)
+ group.POST("/comment/replay", web_commend.ReplayPostComment)
+ group.POST("/comment/del", web_commend.DelCommend)
+ group.POST("/comment/user/del", web_commend.DelUserCommend)
+ group.POST("/comment/reviewBatch", web_commend.ReviewCommendBatch)
+ group.POST("/comment/review", web_commend.ReviewComment)
+ }
+}
\ No newline at end of file
diff --git a/web/router/contactus.go b/web/router/contactus.go
new file mode 100644
index 0000000..fc51f01
--- /dev/null
+++ b/web/router/contactus.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "videoweb/web/api/web_contactus"
+
+ "github.com/gin-gonic/gin"
+)
+
+func contactusRouter(router *gin.RouterGroup) {
+ group := router.Group("/contactus")
+ {
+ group.POST("/list", web_contactus.ContactusList)
+ group.POST("/del", web_contactus.Del)
+ group.POST("/update", web_contactus.Update)
+ }
+}
diff --git a/web/router/content_tag.go b/web/router/content_tag.go
new file mode 100644
index 0000000..6af382f
--- /dev/null
+++ b/web/router/content_tag.go
@@ -0,0 +1,18 @@
+package router
+
+import (
+ "videoweb/web/api/web_contenttag"
+
+ "github.com/gin-gonic/gin"
+)
+
+func contentTagRouter(router *gin.RouterGroup) {
+ group := router.Group("/content/tag")
+ {
+ group.GET("/list", web_contenttag.List)
+ group.GET("/option", web_contenttag.Option)
+ group.POST("/add", web_contenttag.Add)
+ group.POST("/update", web_contenttag.Update)
+ group.POST("/del", web_contenttag.Del)
+ }
+}
diff --git a/web/router/contentexcel.go b/web/router/contentexcel.go
new file mode 100644
index 0000000..ea6f9a7
--- /dev/null
+++ b/web/router/contentexcel.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/web/api/web_contentexcel"
+
+ "github.com/gin-gonic/gin"
+)
+
+func contentExcelRouter(rootRouter *gin.RouterGroup) {
+ group := rootRouter.Group("/contentexcel")
+ {
+ group.GET("/export", web_contentexcel.Export)
+ group.POST("/import", web_contentexcel.Import)
+ }
+}
diff --git a/web/router/detailnav.go b/web/router/detailnav.go
new file mode 100644
index 0000000..3f96d12
--- /dev/null
+++ b/web/router/detailnav.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "videoweb/web/api/web_detailnav"
+
+ "github.com/gin-gonic/gin"
+)
+
+func detailNavRouter(router *gin.RouterGroup) {
+ group := router.Group("/detailnav")
+ {
+ group.POST("/list", web_detailnav.DetailNavList)
+ group.POST("/add", web_detailnav.DetailNavAdd)
+ group.POST("/del", web_detailnav.DetailNavDel)
+ group.POST("/update", web_detailnav.DetailNavUpdate)
+ }
+}
diff --git a/web/router/file.go b/web/router/file.go
new file mode 100644
index 0000000..b2d5541
--- /dev/null
+++ b/web/router/file.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "videoweb/web/api/web_file"
+
+ "github.com/gin-gonic/gin"
+)
+
+func fileRouter(router *gin.RouterGroup) {
+ // 文件上传
+ file := router.Group("/file")
+ {
+ file.POST("/uploadImg", web_file.UploadImage)
+ file.POST("/uploadImgs", web_file.UploadImages)
+ }
+}
diff --git a/web/router/gin.go b/web/router/gin.go
new file mode 100644
index 0000000..16b224f
--- /dev/null
+++ b/web/router/gin.go
@@ -0,0 +1,122 @@
+package router
+
+import (
+ "fmt"
+ "net/http"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+ "videoweb/web/middleware/auth"
+ "videoweb/web/middleware/cors"
+ "videoweb/web/middleware/ipblacklist"
+ "videoweb/web/middleware/limitHandler"
+ "videoweb/web/webg"
+
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+
+ "github.com/DeanThompson/ginpprof"
+ "github.com/gin-gonic/gin"
+
+ _ "videoweb/web/docs"
+)
+
+func Init() (engine *gin.Engine) {
+
+ // 设置启动模式
+ switch webg.Conf.RunMod {
+ case "dev":
+ gin.SetMode(gin.DebugMode)
+ case "test":
+ gin.SetMode(gin.TestMode)
+ case "prod":
+ gin.SetMode(gin.ReleaseMode)
+ default:
+ gin.SetMode(gin.DebugMode)
+ }
+
+ // 初始化gin引擎
+ engine = gin.Default()
+ engine.HandleMethodNotAllowed = true
+ engine.MaxMultipartMemory = 60 << 20
+
+ engine.NoMethod(noMethod)
+ engine.NoRoute(noRoute)
+
+ // 注册中间件
+ registeMiddleware(engine)
+ wbVideoDataRouter(engine)
+ registerRouter(engine)
+ log.Info("setGinSwaggerxxxxx")
+
+ if webg.Conf.RunMod != "prod" {
+ log.Info("setGinSwagger")
+ setGinSwagger(engine)
+ log.Info("setGinSwagger end")
+ }
+
+ // 是否开启监控服务
+ if webg.Conf.Pprof {
+ ginpprof.Wrap(engine)
+ }
+
+ return
+}
+
+func noMethod(c *gin.Context) {
+ c.JSON(http.StatusMethodNotAllowed, errors.MakeWithMsg(errors.MethodNotFound, "MethodNotFound"))
+}
+
+func noRoute(c *gin.Context) {
+ c.JSON(http.StatusNotFound, errors.MakeWithMsg(errors.ApiNotFound, "ApiNotFound"))
+}
+
+func registeMiddleware(engine *gin.Engine) {
+ engine.Use(gin.Recovery())
+ engine.Use(cors.Cors(&cors.Options{
+ Origin: nil, // "*"
+ Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete},
+ AllowedHeaders: []string{"Content-Length", "Content-Type", "Authorization", "X-User-Agent", useragent.SDKCommonHeader},
+ ExposedHeaders: []string{"Content-Disposition", "Refresh-Authorization"},
+ OptionsSuccessStatus: http.StatusOK,
+ }))
+ engine.Use(ipblacklist.IpBlackListMiddleware)
+ engine.Use(auth.Auth)
+ engine.Use(limitHandler.LoginLimit)
+ engine.Use(auth.OperateLog)
+}
+
+// 设置swag 接口信息
+func setGinSwagger(router *gin.Engine) {
+ url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", webg.Conf.Host, webg.Conf.ListenAddr))
+ router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
+}
+
+func registerRouter(router *gin.Engine) {
+ rootRouter := router.Group("/api/web")
+ {
+ MainWebRouter(rootRouter)
+ syscfgRouter(rootRouter)
+ fileRouter(rootRouter)
+ videoRouter(rootRouter)
+ translationRouter(rootRouter)
+ richTextContent(rootRouter)
+ commentRouter(rootRouter)
+ videoCollectionRouter(rootRouter)
+ ipBlackRouter(rootRouter)
+ detailNavRouter(rootRouter)
+ mediaResourceRouter(rootRouter)
+ playletRouter(rootRouter)
+ homeBannerRouter(rootRouter)
+ contentTagRouter(rootRouter)
+ slugRouter(rootRouter)
+ hotNavRouter(rootRouter)
+ appSyncRouter(rootRouter)
+ actressRouter(rootRouter)
+ contactusRouter(rootRouter)
+ announcementRouter(rootRouter)
+ userRouter(rootRouter)
+ questionnaireRouter(rootRouter)
+ contentExcelRouter(rootRouter)
+ }
+}
diff --git a/web/router/home_banner.go b/web/router/home_banner.go
new file mode 100644
index 0000000..38d9975
--- /dev/null
+++ b/web/router/home_banner.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "videoweb/web/api/web_homebanner"
+
+ "github.com/gin-gonic/gin"
+)
+
+func homeBannerRouter(router *gin.RouterGroup) {
+ group := router.Group("/home/banner")
+ {
+ group.GET("/list", web_homebanner.List)
+ group.POST("/add", web_homebanner.Add)
+ group.POST("/update", web_homebanner.Update)
+ group.POST("/del", web_homebanner.Del)
+ }
+}
diff --git a/web/router/hotnav.go b/web/router/hotnav.go
new file mode 100644
index 0000000..98f5046
--- /dev/null
+++ b/web/router/hotnav.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "videoweb/web/api/web_hotnav"
+
+ "github.com/gin-gonic/gin"
+)
+
+func hotNavRouter(router *gin.RouterGroup) {
+ group := router.Group("/hotnav")
+ {
+ group.POST("/list", web_hotnav.HotNavList)
+ group.POST("/add", web_hotnav.HotNavAdd)
+ group.POST("/del", web_hotnav.HotNavDel)
+ group.POST("/update", web_hotnav.HotNavUpdate)
+ }
+}
diff --git a/web/router/ipblack.go b/web/router/ipblack.go
new file mode 100644
index 0000000..1a0963f
--- /dev/null
+++ b/web/router/ipblack.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/web/api/web_ipblack"
+)
+
+func ipBlackRouter(router *gin.RouterGroup) {
+ // 文件上传
+ group := router.Group("/ipblack")
+ {
+ group.POST("/list", web_ipblack.IpBlackList)
+ group.POST("/add", web_ipblack.IpBlackAdd)
+ group.POST("/del", web_ipblack.IpBlackDel)
+ }
+}
\ No newline at end of file
diff --git a/web/router/mediaresource.go b/web/router/mediaresource.go
new file mode 100644
index 0000000..ff5e0d2
--- /dev/null
+++ b/web/router/mediaresource.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "videoweb/web/api/web_mediaresource"
+
+ "github.com/gin-gonic/gin"
+)
+
+func mediaResourceRouter(router *gin.RouterGroup) {
+ group := router.Group("/resource")
+ {
+ group.GET("/list", web_mediaresource.ResourceList)
+ group.GET("/chapters", web_mediaresource.ResourceChapters)
+ group.POST("/import", web_mediaresource.ResourceImport)
+ }
+}
diff --git a/web/router/playlet.go b/web/router/playlet.go
new file mode 100644
index 0000000..5908d42
--- /dev/null
+++ b/web/router/playlet.go
@@ -0,0 +1,52 @@
+package router
+
+import (
+ "videoweb/web/api/web_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+func playletRouter(router *gin.RouterGroup) {
+ group := router.Group("/playlet")
+ {
+ group.GET("/list", web_playlet.List)
+ group.POST("/add", web_playlet.Add)
+ group.POST("/update", web_playlet.Update)
+ group.POST("/del", web_playlet.Del)
+ group.POST("/status/set", web_playlet.SetStatus)
+ group.POST("/recommend/set", web_playlet.SetRecommend)
+ group.POST("/featured/set", web_playlet.SetFeatured)
+ group.POST("/banner/set", web_playlet.SetBanner)
+ group.POST("/category/set", web_playlet.SetCategory)
+ group.POST("/theme/set", web_playlet.SetTheme)
+ group.POST("/tag/set", web_playlet.SetTag)
+ group.POST("/like/virtual/set", web_playlet.SetVirtualLike)
+ group.POST("/browse/virtual/set", web_playlet.SetVirtualBrowse)
+ group.POST("/heat/virtual/set", web_playlet.SetVirtualHeat)
+ group.POST("/pay-type/set", web_playlet.SetPayType)
+ group.POST("/watermark/reset", web_playlet.ResetWatermark)
+ group.GET("/chapter/list", web_playlet.ChapterList)
+ group.POST("/chapter/add", web_playlet.ChapterAdd)
+ group.POST("/chapter/update", web_playlet.ChapterUpdate)
+ group.POST("/chapter/del", web_playlet.ChapterDel)
+ group.POST("/chapter/sync", web_playlet.ChapterSync)
+ group.POST("/chapter/pay-type/set", web_playlet.ChapterSetPayType)
+ group.GET("/category/list", web_playlet.CategoryList)
+ group.POST("/category/add", web_playlet.CategoryAdd)
+ group.POST("/category/update", web_playlet.CategoryUpdate)
+ group.POST("/category/del", web_playlet.CategoryDel)
+ group.GET("/tag/list", web_playlet.TagList)
+ group.POST("/tag/add", web_playlet.TagAdd)
+ group.POST("/tag/update", web_playlet.TagUpdate)
+ group.POST("/tag/del", web_playlet.TagDel)
+ group.GET("/theme/list", web_playlet.ThemeList)
+ group.POST("/theme/add", web_playlet.ThemeAdd)
+ group.POST("/theme/update", web_playlet.ThemeUpdate)
+ group.POST("/theme/del", web_playlet.ThemeDel)
+ group.GET("/home-config/list", web_playlet.HomeConfigList)
+ group.GET("/home-config/topic/options", web_playlet.HomeConfigTopicOptions)
+ group.POST("/home-config/add", web_playlet.HomeConfigAdd)
+ group.POST("/home-config/update", web_playlet.HomeConfigUpdate)
+ group.POST("/home-config/del", web_playlet.HomeConfigDel)
+ }
+}
diff --git a/web/router/questionnaire.go b/web/router/questionnaire.go
new file mode 100644
index 0000000..0652c89
--- /dev/null
+++ b/web/router/questionnaire.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/web/api/web_questionnaire"
+
+ "github.com/gin-gonic/gin"
+)
+
+func questionnaireRouter(router *gin.RouterGroup) {
+ group := router.Group("/questionnaire")
+ {
+ group.POST("/list", web_questionnaire.List)
+ group.POST("/handle", web_questionnaire.Handle)
+ }
+}
diff --git a/web/router/richtextcontent.go b/web/router/richtextcontent.go
new file mode 100644
index 0000000..d20055e
--- /dev/null
+++ b/web/router/richtextcontent.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/web/api/web_richtextcontent"
+)
+
+func richTextContent(router *gin.RouterGroup) {
+ // 文件上传
+ file := router.Group("/richtextcontent")
+ {
+ file.POST("/list", web_richtextcontent.List)
+ file.POST("/add", web_richtextcontent.Add)
+ file.POST("/del", web_richtextcontent.Del)
+ file.POST("/update", web_richtextcontent.Update)
+ }
+}
\ No newline at end of file
diff --git a/web/router/slug.go b/web/router/slug.go
new file mode 100644
index 0000000..26c9132
--- /dev/null
+++ b/web/router/slug.go
@@ -0,0 +1,14 @@
+package router
+
+import (
+ "videoweb/web/api/web_slug"
+
+ "github.com/gin-gonic/gin"
+)
+
+func slugRouter(router *gin.RouterGroup) {
+ group := router.Group("/slug")
+ {
+ group.POST("/sync", web_slug.SyncSlug)
+ }
+}
diff --git a/web/router/systerm.go b/web/router/systerm.go
new file mode 100644
index 0000000..0f12204
--- /dev/null
+++ b/web/router/systerm.go
@@ -0,0 +1,42 @@
+package router
+
+import (
+ "videoweb/web/api/web_acc"
+
+ "github.com/gin-gonic/gin"
+)
+
+// 主后台路由
+func MainWebRouter(router *gin.RouterGroup) {
+ login := router.Group("/login")
+ {
+ login.POST("/acc", web_acc.AccLogin)
+ login.POST("/bind", web_acc.BindQurl)
+ login.POST("/slogin", web_acc.SLogin)
+ }
+
+ role := router.Group("/role")
+ {
+ role.GET("/list", web_acc.RoleGetList)
+ role.POST("/add", web_acc.RoleAdd)
+ role.POST("/del", web_acc.RoleDel)
+ role.GET("/getinfo", web_acc.RoleGetInfo)
+ role.POST("/up", web_acc.RoleUp)
+ role.POST("/enable", web_acc.RoleEnable)
+ role.POST("/drop", web_acc.RoleGetDrop)
+ role.GET("/getauth", web_acc.RoleGetAuth)
+ role.POST("/upauth", web_acc.RoleUpAuth)
+ }
+ acc := router.Group("/acc")
+ {
+ acc.GET("/list", web_acc.AccGetList)
+ acc.POST("/add", web_acc.AccAdd)
+ acc.POST("/del", web_acc.AccDel)
+ acc.GET("/getinfo", web_acc.AccGetInfo)
+ acc.POST("/up", web_acc.AccUp)
+ acc.GET("/getstatus", web_acc.AccGetStatus)
+ acc.POST("/savestatus", web_acc.AccSaveStatus)
+ acc.POST("/ops", web_acc.GetAccOpRecords)
+ acc.POST("/uppwd", web_acc.AccUpdatePwd)
+ }
+}
diff --git a/web/router/translation.go b/web/router/translation.go
new file mode 100644
index 0000000..651a07f
--- /dev/null
+++ b/web/router/translation.go
@@ -0,0 +1,24 @@
+package router
+
+import (
+ "videoweb/web/api/web_translation"
+
+ "github.com/gin-gonic/gin"
+)
+
+func translationRouter(router *gin.RouterGroup) {
+ translation := router.Group("/translation")
+ {
+ translation.POST("/list", web_translation.GetTranslation)
+ translation.POST("/update", web_translation.UpdateOnetTranslation)
+ translation.POST("/del", web_translation.DelOnetTranslation)
+ translation.POST("/add", web_translation.AddOnetTranslation)
+ }
+ translationcategory := router.Group("/translationcategory")
+ {
+ translationcategory.POST("/list", web_translation.GetTranslationCategory)
+ translationcategory.POST("/update", web_translation.UpdateOnetTranslationCategory)
+ translationcategory.POST("/del", web_translation.DelOnetTranslationCategory)
+ translationcategory.POST("/add", web_translation.AddOnetTranslationCategory)
+ }
+}
diff --git a/web/router/user.go b/web/router/user.go
new file mode 100644
index 0000000..4cb9b0f
--- /dev/null
+++ b/web/router/user.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/web/api/web_user"
+
+ "github.com/gin-gonic/gin"
+)
+
+func userRouter(router *gin.RouterGroup) {
+ user := router.Group("/user")
+ {
+ user.POST("/list", web_user.GetUserList)
+ user.POST("/update", web_user.UpdateUser)
+ }
+}
diff --git a/web/router/video.go b/web/router/video.go
new file mode 100644
index 0000000..ebb8e6f
--- /dev/null
+++ b/web/router/video.go
@@ -0,0 +1,54 @@
+package router
+
+import (
+ "videoweb/web/api/web_video"
+
+ "github.com/gin-gonic/gin"
+)
+
+func videoRouter(router *gin.RouterGroup) {
+ advitise := router.Group("/videoinfo")
+ {
+ advitise.POST("/list", web_video.GetVideoInfo)
+ advitise.POST("/update", web_video.UpdateOneVideoInfo)
+ advitise.POST("/del", web_video.DelOneVideoInfo)
+ advitise.POST("/sync", web_video.SyncVideoInfo)
+ advitise.POST("/syncMediaFile", web_video.SyncVideoFromMediaFile)
+ advitise.POST("/addcoll", web_video.AddOneVideoCollection)
+ advitise.POST("/addTags", web_video.AddVideoTags)
+ advitise.POST("/tag/set", web_video.SetVideoTags)
+ advitise.POST("/recommend/set", web_video.SetVideoRecommend)
+ advitise.POST("/setFakeLikes", web_video.SetVideoFakeLikes)
+ advitise.POST("/setFakeWatchTimes", web_video.SetVideoFakeWatchTimes)
+ advitise.POST("/heat/set", web_video.SetVideoHeat)
+ advitise.POST("/fillZeroFakeFlow", web_video.FillZeroVideoFakeFlow)
+ }
+ videocategory := router.Group("/videocategory")
+ {
+ videocategory.POST("/list", web_video.GetVideoCategory)
+ videocategory.POST("/update", web_video.UpdateOneVideoCategory)
+ videocategory.POST("/del", web_video.DelOneVideoCategory)
+ videocategory.POST("/add", web_video.AddOneVideoCategory)
+ }
+ videotag := router.Group("/videotag")
+ {
+ videotag.POST("/list", web_video.GetVideoTagInfo)
+ videotag.POST("/update", web_video.UpdateOneVideoTagInfo)
+ videotag.POST("/del", web_video.DelOneVideoTagInfo)
+ videotag.POST("/add", web_video.AddOneVideoTagInfo)
+ }
+
+ videocreater := router.Group("/videocreater")
+ {
+ videocreater.POST("/list", web_video.GetVideoCreater)
+ videocreater.POST("/update", web_video.UpdateOneVideoCreater)
+ videocreater.POST("/del", web_video.DelOneVideoCreater)
+ videocreater.POST("/add", web_video.AddOneVideoCreater)
+ }
+
+ router.GET("/m3u8/*key", web_video.Play)
+ router.GET("/transm3u8/*key", web_video.TranscodeM3u8)
+ router.GET("/enkey", web_video.Enkey)
+ router.GET("/lsjenkey", web_video.LsjEnkey)
+ router.POST("/videoinfo/transcode", web_video.TranscodeVideo)
+}
diff --git a/web/router/videocollection.go b/web/router/videocollection.go
new file mode 100644
index 0000000..fc96605
--- /dev/null
+++ b/web/router/videocollection.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "github.com/gin-gonic/gin"
+ "videoweb/web/api/web_videocollection"
+)
+
+func videoCollectionRouter(router *gin.RouterGroup) {
+ group := router.Group("/videocollection")
+ {
+ group.POST("/media/list", web_videocollection.GetVideoCollectionMediaList)
+ group.POST("/media/option", web_videocollection.UpdateVideoCollectionMediaList)
+ group.POST("/media/rank", web_videocollection.UpdateVideoCollectionMediaRank)
+ }
+}
\ No newline at end of file
diff --git a/web/router/wbvideodata.go b/web/router/wbvideodata.go
new file mode 100644
index 0000000..69581e8
--- /dev/null
+++ b/web/router/wbvideodata.go
@@ -0,0 +1,18 @@
+package router
+
+import (
+ "videoweb/web/api/web_video"
+ "videoweb/web/middleware/apilimitHandler"
+
+ "github.com/gin-gonic/gin"
+)
+
+func wbVideoDataRouter(r *gin.Engine) {
+ limiter := apilimitHandler.NewRouteHourlyLimiter(100)
+ router := r.Group("/wbapi/hjsq")
+ {
+ router.Use(limiter.Middleware())
+ router.GET("/hottag", web_video.GetVideoPlayData)
+ router.GET("/ipstat", web_video.GetIpStatData)
+ }
+}
diff --git a/web/server/server.go b/web/server/server.go
new file mode 100644
index 0000000..33b70fd
--- /dev/null
+++ b/web/server/server.go
@@ -0,0 +1,211 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+ "videoweb/pub/common/laosiji"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvversion"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models"
+ "videoweb/pub/mongos"
+ "videoweb/pub/redis"
+ "videoweb/pub/seo/googleindex"
+ "videoweb/pub/storage/proxy"
+ "videoweb/web/router"
+ "videoweb/web/service"
+ "videoweb/web/webg"
+
+ "gopkg.in/yaml.v2"
+)
+
+func init() {
+ time.Local, _ = time.LoadLocation("Asia/Shanghai")
+}
+func Run(args []string) {
+ showVersion()
+ if err := initConfig(); err != nil {
+ log.Error("init config err", log.E(err))
+ os.Exit(1)
+ return
+ }
+
+ shopDb := mongos.NewManager(webg.Conf.Mongodb.Addr)
+ if err := shopDb.Init(); err != nil {
+ log.Error("connect shopDb err", log.E(err))
+ os.Exit(1)
+ }
+ mongos.Shop = shopDb
+
+ shopLog := mongos.NewManager(webg.Conf.Mongodb.LogAddr)
+ if err := shopLog.Init(); err != nil {
+ log.Error("connect shopLog err", log.E(err))
+ os.Exit(1)
+ }
+ mongos.ShopLog = shopLog
+
+ if err := redis.Init(redis.Options{
+ Addr: webg.Conf.Redis.Addr,
+ Psw: webg.Conf.Redis.Passwd,
+ Db: webg.Conf.Redis.Db,
+ }); err != nil {
+ log.Error("redis connect err", log.E(err))
+ os.Exit(1)
+ }
+
+ if err := initCollections(); err != nil {
+ os.Exit(1)
+ return
+ }
+
+ if err := initElasticSearch(); err != nil {
+ log.Error("init ES failed", log.E(err))
+ os.Exit(1)
+ }
+ //初始化tg机器人
+ if webg.Conf.TgToken != "" {
+ //tgbot.InitTgBot(webg.Conf.TgToken, "[汁乎Web]")
+ }
+
+ proxy.InitBucketVideoAddress(webg.Conf.BucketVideoAddress, webg.Conf.BucketVideoAddressV2)
+ initLaosiji()
+ mongos.StartLid()
+ initData()
+ initSeoPush()
+ dataReport.Init(dataReport.Config{
+ ApiUrl: webg.Conf.DataReport.Url,
+ RunMod: webg.Conf.RunMod,
+ })
+
+ initHttpServer()
+
+ shutdownHandler()
+}
+
+func showVersion() {
+ log.Info(fmt.Sprintf("sevice version:%s", srvversion.SERVICE_VERSION))
+ log.Info(fmt.Sprintf("git version:%s", srvversion.GIT_VERSION))
+ log.Info(fmt.Sprintf("build date:%s", srvversion.BUILD_DATE))
+ log.Info(fmt.Sprintf("build ip:%s", srvversion.BUILD_IP))
+}
+
+func initConfig() error {
+
+ buf, err := ioutil.ReadFile("conf/web.yaml")
+ if err != nil {
+ return err
+ }
+
+ if err = yaml.Unmarshal(buf, &webg.Conf); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// mongo表
+func initCollections() error {
+ return nil
+}
+
+// 数据
+func initData() {
+ models.Init()
+ service.Start()
+}
+
+// 搜索引擎推送
+func initSeoPush() {
+ if webg.Conf.RunMod != "prod" {
+ return
+ }
+ googleindex.Init(context.Background())
+}
+
+func initElasticSearch() error {
+ es, err := esmanager.NewManager(webg.Conf.Es.Addr,
+ webg.Conf.Es.User, webg.Conf.Es.Pass)
+ if err != nil {
+ log.Info("new es failed", log.Any("err", err))
+ return err
+ }
+ if err = es.ClientConnect(); err != nil {
+ log.Info("connect es failed", log.Any("err", err))
+ return err
+ }
+ esmanager.Instance = es
+ log.Info("es:Init Ok!")
+ return nil
+}
+
+func initHttpServer() {
+
+ addr := fmt.Sprintf(":%s", webg.Conf.ListenAddr)
+ srv := &http.Server{
+ Addr: addr,
+ Handler: router.Init(),
+ }
+
+ go func() {
+ log.Info("服务已启动", log.Any("地址", addr))
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatal("listen:", log.E(err))
+ }
+ }()
+}
+
+func shutdownHandler() {
+ quit := make(chan os.Signal)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Kill)
+ <-quit
+ log.Warn("Shutdown... server")
+ service.Stop()
+
+ done := make(chan struct{})
+ go func() {
+ mongos.Shop.DisConnect()
+ mongos.ShopLog.DisConnect()
+ redis.Disconnect()
+ // srv_resource.ClosePrefetch()
+ done <- struct{}{}
+ }()
+ select {
+ case <-time.After(5 * time.Second):
+ log.Warn("shutdown timeout....")
+ case <-done:
+ log.Info("graceful shutdown")
+ }
+}
+
+func initLaosiji() {
+ if webg.Conf.LsjMediaConf.ApiUrl == "" {
+ return
+ }
+ cli := laosiji.New(webg.Conf.LsjMediaConf.ApiUrl, webg.Conf.LsjMediaConf.AppId, webg.Conf.LsjMediaConf.ApiKey)
+ d := laosiji.NewDomains(cli)
+ laosiji.LaosijiClient = cli
+ laosiji.LaosijiDomains = d
+
+ hr := webg.Conf.LsjMediaConf.DomainsRefreshHr
+ if hr <= 0 {
+ hr = 3
+ }
+ d.StartAutoRefresh(time.Duration(hr) * time.Hour)
+
+ go func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if _, err := d.Refresh(ctx); err != nil {
+ log.Error("Init Laosiji domains 首次拉取失败(后台重试,不影响启动)", log.E(err))
+ } else {
+ log.Info("Init Laosiji OK...")
+ }
+ }()
+}
diff --git a/web/service/services.go b/web/service/services.go
new file mode 100644
index 0000000..52d465c
--- /dev/null
+++ b/web/service/services.go
@@ -0,0 +1,21 @@
+package service
+
+import (
+ "videoweb/web/service/srv_ipblack"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/service/srv_search"
+ "videoweb/web/skd"
+)
+
+func Start() {
+ skd.Start()
+ srv_ipblack.Init()
+ srv_mediaresource.Start()
+ srv_search.Start()
+}
+
+func Stop() {
+ skd.Stop()
+ srv_mediaresource.Stop()
+ srv_search.Stop()
+}
diff --git a/web/service/srv_acc/acc.go b/web/service/srv_acc/acc.go
new file mode 100644
index 0000000..1e91db4
--- /dev/null
+++ b/web/service/srv_acc/acc.go
@@ -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
+}
diff --git a/web/service/srv_acc/define.go b/web/service/srv_acc/define.go
new file mode 100644
index 0000000..a4430ba
--- /dev/null
+++ b/web/service/srv_acc/define.go
@@ -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"` // 开始时间
+}
diff --git a/web/service/srv_acc/helper.go b/web/service/srv_acc/helper.go
new file mode 100644
index 0000000..367b617
--- /dev/null
+++ b/web/service/srv_acc/helper.go
@@ -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
+}
diff --git a/web/service/srv_actress/actress.go b/web/service/srv_actress/actress.go
new file mode 100644
index 0000000..8b2e22d
--- /dev/null
+++ b/web/service/srv_actress/actress.go
@@ -0,0 +1,165 @@
+package srv_actress
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func ActressList(ctx context.Context, req *ActressListReq) (*ActressListResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[actress.FN.Actress.Id] = *req.Id
+ }
+ if req.TranslationCategoryId != nil && *req.TranslationCategoryId > 0 {
+ filter[actress.FN.Actress.TranslationCategoryId] = *req.TranslationCategoryId
+ }
+ if req.Region != nil && len(*req.Region) > 0 {
+ filter[actress.FN.Actress.Region] = bson.M{"$regex": *req.Region}
+ }
+ if req.Status != nil {
+ filter[actress.FN.Actress.Status] = *req.Status
+ }
+ if req.Alias != nil && *req.Alias != "" {
+ filter[actress.FN.Actress.Alias] = bson.M{"$regex": *req.Alias}
+ }
+ count, err := actress.CountActresss(ctx, filter)
+ if err != nil {
+ log.Warn("ActressList CountActresss", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ resp := &ActressListResp{
+ Total: 0,
+ List: make([]*actress.Actress, 0),
+ }
+ if count == 0 {
+ return resp, nil
+ }
+ opts := &options.FindOptions{}
+ opts.SetLimit(req.PageSize)
+ opts.SetSkip((req.PageNum - 1) * req.PageSize)
+ sort := bson.M{actress.FN.Actress.CreatedAt: -1}
+ switch req.SortType {
+ case 1:
+ sort = bson.M{actress.FN.Actress.Score: -1}
+ case 2:
+ sort = bson.M{actress.FN.Actress.Rank: -1}
+ case 3:
+ sort = bson.M{actress.FN.Actress.Works: -1}
+ case 4:
+ sort = bson.M{actress.FN.Actress.Real.WatchTimes: -1}
+ case 5:
+ sort = bson.M{actress.FN.Actress.HotValue: -1, actress.FN.Actress.Id: -1}
+ }
+ opts.SetSort(sort)
+ list, err := actress.FindManyActresss(ctx, filter, opts)
+ if err != nil {
+ log.Warn("ActressList FindManyActresss", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ resp.Total = count
+ resp.List = list
+ return resp, nil
+}
+
+func UpdateActress(ctx context.Context, req *ActressUpdateReq) *srvdef.SrvErr {
+ current, err := actress.FindOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id})
+ if err != nil {
+ log.Warn("UpdateActress", log.Any("id", req.Id), log.E(err))
+ return err
+ }
+ alias := current.Alias
+ if req.Param.Alias != nil {
+ alias = *req.Param.Alias
+ }
+ keywords := current.SeoInfo.Keyworld
+ if req.Param.SeoInfo != nil {
+ keywords = req.Param.SeoInfo.Keyworld
+ }
+ exists := func(s string) bool {
+ doc, _ := actress.FindOneActress(ctx, slugx.Filter(actress.FN.Actress.Slug, actress.FN.Actress.Id, s, req.Id))
+ return doc != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Param.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, alias), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ req.Param.Slug = &slug
+ } else {
+ req.Param.Slug = nil
+ }
+ now := time.Now()
+ req.Param.UpdatedAt = &now
+ return actress.UpdateOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id}, bson.M{"$set": req.Param})
+}
+
+func AddActress(ctx context.Context, req *ActressAddReq) *srvdef.SrvErr {
+ if len(req.Alias) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "alias 不能为空", "alias 不能为空")
+ }
+ _, err := actress.FindOneActress(ctx, bson.M{
+ actress.FN.Actress.Alias: req.Alias,
+ })
+ if err != nil && err.Code != srvdef.ErrNoData {
+ log.Warn("AddActress", log.Any("req", req), log.E(err))
+ return err
+ }
+ if err == nil {
+ log.Warn("AddActress Re", log.Any("req", req))
+ return srvdef.New(srvdef.RepeatActress)
+ }
+
+ now := time.Now()
+ info := &actress.Actress{
+ Id: mongos.Lid.Actress.Get(),
+ Alias: req.Alias,
+ Slug: req.Slug,
+ SeoInfo: req.SeoInfo,
+ Avatar: req.Avatar,
+ Cover: req.Cover,
+ Desc: req.Desc,
+ TranslationCategoryId: req.TranslationCategoryId,
+ Meas: req.Meas,
+ Office: req.Office,
+ DebutDate: req.DebutDate,
+ BornDate: req.BornDate,
+ Region: req.Region,
+ Score: req.Score,
+ Rank: req.Rank,
+ HotValue: req.HotValue,
+ Status: req.Status,
+ Works: 0,
+ Real: pubdef.Flow{},
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ exists := func(s string) bool {
+ doc, _ := actress.FindOneActress(ctx, slugx.Filter(actress.FN.Actress.Slug, actress.FN.Actress.Id, s, 0))
+ return doc != nil
+ }
+ if serr := slugx.Ensure(&info.Slug, utils.SlugSourceFromKeywordsOrTitle(info.SeoInfo.Keyworld, info.Alias), exists); serr != nil {
+ return serr
+ }
+ return actress.InsertOneActress(ctx, info)
+}
+
+func DeleteActress(ctx context.Context, req *ActressDelReq) *srvdef.SrvErr {
+ _, err := actress.FindOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id})
+ if err != nil {
+ log.Warn("DeleteActress", log.Any("id", req.Id), log.E(err))
+ return err
+ }
+ _, err = actress.DelOneActress(ctx, bson.M{actress.FN.Actress.Id: req.Id})
+ return err
+}
diff --git a/web/service/srv_actress/define.go b/web/service/srv_actress/define.go
new file mode 100644
index 0000000..f299945
--- /dev/null
+++ b/web/service/srv_actress/define.go
@@ -0,0 +1,69 @@
+package srv_actress
+
+import (
+ "time"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/pubdef"
+)
+
+type ActressListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"` // Id
+ Alias *string `form:"alias" json:"alias"` // 别名
+ TranslationCategoryId *int64 `form:"translationCategoryId" json:"translationCategoryId"` // 关联导航分类id
+ Region *string `form:"region" json:"region"` // 地区/国家
+ Status *bool `form:"status" json:"status" ` // 是否启用
+ SortType int64 `form:"sortType" json:"sortType"` // 排序方式 0=创建时间 1=评分 2=排序值 3=作品数 4=总观看次数 5=热度值
+}
+type ActressListResp struct {
+ Total int64 `json:"total"`
+ List []*actress.Actress `json:"list"`
+}
+
+type ActressUpdateReq struct {
+ Id int64 `json:"id,omitempty" binding:"required"`
+ Param ActressUpdateParam `json:"param" binding:"required"`
+}
+
+type ActressUpdateParam struct {
+ Alias *string `bson:"alias,omitempty" json:"alias"` // 别名
+ Slug *string `json:"slug" bson:"slug,omitempty"` // slug
+ Avatar *string `bson:"avatar,omitempty" json:"avatar"` // 头像
+ Cover *string `bson:"cover,omitempty" json:"cover"` // 封面大图
+ Desc *string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ TranslationCategoryId *int64 `bson:"translationCategoryId,omitempty" json:"translationCategoryId"` // 关联导航分类id
+ Meas *actress.Meas `bson:"meas,omitempty" json:"meas"` // 三围
+ Office *string `json:"office" bson:"office,omitempty"` // 事务所
+ DebutDate *time.Time `bson:"debutDate,omitempty" json:"debutDate" ` // 出道日期
+ BornDate *time.Time `bson:"bornDate,omitempty" json:"bornDate"` // 出生日期
+ Region *string `json:"region" bson:"region,omitempty"` // 地区/国家
+ Score *string `json:"score" bson:"score,omitempty"` // 评分
+ Rank *int64 `json:"rank" bson:"rank,omitempty"` // 排序
+ HotValue *int64 `json:"hotValue" bson:"hotValue,omitempty"` // 热度值
+ Status *bool `json:"status" bson:"status,omitempty"` // 是否启用
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ UpdatedAt *time.Time `json:"updatedAt" bson:"updatedAt,omitempty"` // 更新时间
+}
+
+type ActressAddReq struct {
+ Alias string `bson:"alias" json:"alias"` // 别名
+ Slug string `json:"slug" bson:"slug"` // slug,不传则按 SEO 关键词或别名自动生成
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Avatar string `bson:"avatar" json:"avatar"` // 头像
+ Cover string `bson:"cover" json:"cover"` // 封面大图
+ Desc string `bson:"desc" json:"desc"` // 详细描述
+ TranslationCategoryId int64 `bson:"translationCategoryId" json:"translationCategoryId"` // 关联导航分类id
+ Meas actress.Meas `bson:"meas" json:"meas"` // 三围
+ Office string `json:"office" bson:"office"` // 事务所
+ DebutDate time.Time `bson:"debutDate" json:"debutDate" ` // 出道日期
+ BornDate time.Time `bson:"bornDate" json:"bornDate"` // 出生日期
+ Region string `json:"region" bson:"region"` // 地区/国家
+ Score string `json:"score" bson:"score"` // 评分
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ HotValue int64 `json:"hotValue" bson:"hotValue"` // 热度值
+ Status bool `json:"status" bson:"status"` // 是否启用
+}
+
+type ActressDelReq struct {
+ Id int64 `json:"id"`
+}
diff --git a/web/service/srv_announcement/announcement.go b/web/service/srv_announcement/announcement.go
new file mode 100644
index 0000000..ff2f21e
--- /dev/null
+++ b/web/service/srv_announcement/announcement.go
@@ -0,0 +1,111 @@
+package srv_announcement
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/announcementmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func GetAnnouncementList(ctx context.Context, req *GetAnnouncementListReq) (*GetAnnouncementListResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.Title != nil && *req.Title != "" {
+ filter[announcementmod.FN.Announcement.Title] = bson.M{"$regex": *req.Title, "$options": "i"}
+ }
+ if req.Status != nil {
+ filter[announcementmod.FN.Announcement.Status] = *req.Status
+ }
+
+ count, serr := announcementmod.CountAnnouncement(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ if count == 0 {
+ return &GetAnnouncementListResp{
+ Total: 0,
+ List: make(announcementmod.AnnouncementList, 0),
+ }, nil
+ }
+
+ opts := options.FindOptions{}
+ opts.SetSort(bson.D{
+ {Key: announcementmod.FN.Announcement.Sort, Value: -1},
+ {Key: announcementmod.FN.Announcement.CreatedAt, Value: -1},
+ })
+ opts.SetLimit(req.PageSize)
+ opts.SetSkip((req.PageNum - 1) * req.PageSize)
+
+ list, serr := announcementmod.FindManyAnnouncement(ctx, filter, &opts)
+ if serr != nil {
+ return nil, serr
+ }
+
+ return &GetAnnouncementListResp{
+ Total: count,
+ List: list,
+ }, nil
+}
+
+func AddAnnouncement(ctx context.Context, req *AddAnnouncementReq) *srvdef.SrvErr {
+ now := time.Now()
+ status := true
+ if req.Status != nil {
+ status = *req.Status
+ }
+ doc := &announcementmod.Announcement{
+ Id: mongos.Lid.Announcement.Get(),
+ Title: req.Title,
+ Content: req.Content,
+ Sort: req.Sort,
+ Status: status,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if err := announcementmod.InsertOneAnnouncement(ctx, doc); err != nil {
+ return err
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
+
+func UpdateAnnouncement(ctx context.Context, req *UpdateAnnouncementReq) *srvdef.SrvErr {
+ filter := bson.M{announcementmod.FN.Announcement.Id: req.Id}
+ updateFields := bson.M{
+ announcementmod.FN.Announcement.UpdatedAt: time.Now(),
+ }
+ if req.Param.Title != nil {
+ updateFields[announcementmod.FN.Announcement.Title] = *req.Param.Title
+ }
+ if req.Param.Content != nil {
+ updateFields[announcementmod.FN.Announcement.Content] = *req.Param.Content
+ }
+ if req.Param.Sort != nil {
+ updateFields[announcementmod.FN.Announcement.Sort] = *req.Param.Sort
+ }
+ if req.Param.Status != nil {
+ updateFields[announcementmod.FN.Announcement.Status] = *req.Param.Status
+ }
+ if err := announcementmod.UpdateOneAnnouncement(ctx, filter, bson.M{"$set": updateFields}); err != nil {
+ return err
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
+
+func DelAnnouncement(ctx context.Context, req *DelAnnouncementReq) *srvdef.SrvErr {
+ filter := bson.M{announcementmod.FN.Announcement.Id: req.Id}
+ count, serr := announcementmod.DelOneAnnouncement(ctx, filter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrAnnouncementNotExsit, "", "")
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
diff --git a/web/service/srv_announcement/define.go b/web/service/srv_announcement/define.go
new file mode 100644
index 0000000..27e6817
--- /dev/null
+++ b/web/service/srv_announcement/define.go
@@ -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"`
+}
diff --git a/web/service/srv_app/app.go b/web/service/srv_app/app.go
new file mode 100644
index 0000000..73be387
--- /dev/null
+++ b/web/service/srv_app/app.go
@@ -0,0 +1,71 @@
+package srv_app
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+type AppCategoryInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ Title string `json:"title" bson:"title"` // APP显示
+ Desc string `json:"desc" bson:"desc"` // 描述
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+}
+
+type AppCreatorInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Name string `json:"name" bson:"name"` // 帖子标题
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+}
+
+type AppTagInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+}
+
+// AppVideoInfoSyncReq APP数据同步请求
+type AppVideoInfoSyncReq struct {
+ AppId int64 `json:"appId" bson:"appId"`
+ LastSyncTime time.Time `json:"lastSyncTime" bson:"lastSyncTime"` // 最后同步时间
+ PageSize int64 `json:"pageSize" bson:"pageSize"` // 页大小
+}
+
+type AppVideoCollectionSyncReq struct {
+ AppId int64 `json:"appId" bson:"appId"`
+ CollectionId []int64 `json:"collectionId" bson:"collectionId"`
+}
+
+type AppVideoCollectionSyncInfo struct {
+ CollectionID int64 `json:"collectionId" bson:"collectionId"` // 合集ID
+ VideoId int64 `json:"videoId" bson:"videoId"` // 视频ID
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+}
+
+type AppVideoSyncInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ MediaType videoinformationmod.MediaType `json:"mediaType" bson:"mediaType"` // 资源类型
+ AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 帖子上架时间
+ CreatorId int64 `json:"creatorId" bson:"creatorId"` // 作者Id
+ CreatorName string `json:"creatorName" bson:"creatorName"` // 作者名称
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Nodes []videoinformationmod.Node `json:"nodes" bson:"nodes"` // 节点
+ Status pubdef.InformationStatus `json:"status" bson:"status"` // 帖子上架状态
+ Video videoinformationmod.VideoInfo `json:"video" bson:"video"` // 视频
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类Id
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ Real pubdef.Flow `json:"real" bson:"real"` // 真实流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
diff --git a/web/service/srv_app/appser.go b/web/service/srv_app/appser.go
new file mode 100644
index 0000000..8770042
--- /dev/null
+++ b/web/service/srv_app/appser.go
@@ -0,0 +1,124 @@
+package srv_app
+
+import (
+ "context"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videocollectionmediamod"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videoinformationmod"
+)
+
+func AppVideoInfoSync(ctx context.Context, req *AppVideoInfoSyncReq) ([]*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.UpdatedAt: bson.M{
+ "$gt": req.LastSyncTime},
+ }
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: 1})
+ opts.SetLimit(req.PageSize)
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if err != nil {
+ log.Error("FindManyVideoInformations Failed", log.Any("err", err))
+ return nil, err
+ }
+ return list, err
+}
+
+func AppVideoCollectionSync(ctx context.Context, req *AppVideoCollectionSyncReq) ([]*AppVideoCollectionSyncInfo, *srvdef.SrvErr) {
+ filter := bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: bson.M{"$in": req.CollectionId},
+ }
+ list, err := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, filter)
+ if err != nil {
+ log.Error("FindManyVideoCollectionMediaModels Failed", log.Any("err", err))
+ return nil, err
+ }
+ rsl := make([]*AppVideoCollectionSyncInfo, 0)
+ for _, v := range list {
+ rsl = append(rsl, &AppVideoCollectionSyncInfo{
+ CollectionID: v.CollectionID,
+ VideoId: v.VideoId,
+ Rank: v.Rank,
+ })
+ }
+ return rsl, nil
+}
+
+func AppCategoryList(ctx context.Context) ([]AppCategoryInfo, *srvdef.SrvErr) {
+ filter := bson.M{
+ videocategorymod.FN.VideoCategory.Status: true,
+ }
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1})
+ list, err := videocategorymod.FindManyVideoCategorys(ctx, filter, opts)
+ if err != nil {
+ log.Error("FindManyVideoCategorys Failed", log.Any("err", err))
+ return nil, err
+ }
+ resp := make([]AppCategoryInfo, 0)
+ for _, v := range list {
+ resp = append(resp, AppCategoryInfo{
+ Id: v.Id,
+ Name: v.Name,
+ Title: v.Title,
+ Desc: v.Desc,
+ Rank: v.Rank,
+ })
+ }
+ return resp, err
+}
+
+func AppCreatorList(ctx context.Context) ([]AppCreatorInfo, *srvdef.SrvErr) {
+ filter := bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Status: true,
+ }
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Sort: -1,
+ })
+ list, err := videocreatermod.FindManyVideoCreaterInfos(ctx, filter, opts)
+ if err != nil {
+ log.Error("FindManyVideoCreaterInfos Failed", log.Any("err", err))
+ return nil, err
+ }
+ resp := make([]AppCreatorInfo, 0)
+ for _, v := range list {
+ resp = append(resp, AppCreatorInfo{
+ Id: v.Id,
+ Name: v.Name,
+ Sort: v.Sort,
+ })
+ }
+ return resp, err
+}
+
+func AppTagList(ctx context.Context) ([]AppTagInfo, *srvdef.SrvErr) {
+ filter := bson.M{
+ contenttagmod.FN.ContentTag.Status: true,
+ contenttagmod.FN.ContentTag.Types: contenttagmod.ContentTagTypeVideo,
+ }
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.D{
+ {Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
+ {Key: contenttagmod.FN.ContentTag.Id, Value: -1},
+ })
+ list, err := contenttagmod.FindManyContentTag(ctx, filter, opts)
+ if err != nil {
+ log.Error("FindManyContentTag Failed", log.Any("err", err))
+ return nil, err
+ }
+ resp := make([]AppTagInfo, 0)
+ for _, v := range list {
+ resp = append(resp, AppTagInfo{
+ Id: v.Id,
+ Title: v.Name,
+ Sort: v.Rank,
+ })
+ }
+ return resp, err
+}
diff --git a/web/service/srv_cfg/cfg.go b/web/service/srv_cfg/cfg.go
new file mode 100644
index 0000000..cb9d16e
--- /dev/null
+++ b/web/service/srv_cfg/cfg.go
@@ -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
+}
diff --git a/web/service/srv_cfg/define.go b/web/service/srv_cfg/define.go
new file mode 100644
index 0000000..923d7fe
--- /dev/null
+++ b/web/service/srv_cfg/define.go
@@ -0,0 +1,114 @@
+package srv_cfg
+
+import (
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type CDNNode struct {
+ Url *string `json:"url" bson:"url,omitempty"`
+ Ratio *int `json:"ratio" bson:"ratio,omitempty"`
+}
+
+type SysCfgUpdate struct {
+ UnencryptedCdn *string `json:"unencryptedCdn" bson:"unencryptedCdn,omitempty"` // 不加密资源使用的cdn
+ SitemapDefaultImages *[]string `json:"sitemapDefaultImages" bson:"sitemapDefaultImages,omitempty"` // Sitemap里面默认封面图
+ PdfAddr *string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
+ ImgAppCdn *string `json:"imgAppCdn" bson:"imgAppCdn"` // 图片CDN(app使用)
+ ImgAppUsCdn *string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片CDN(海外app使用)
+ ImgWebCdn *string `json:"imgWebCdn" bson:"imgWebCdn"` // 图片CDN(web使用)
+ MediaWebCdn *string `json:"mediaWebCdn" bson:"mediaWebCdn"` // 视频CDN(web使用)
+ MediaAppCdn *string `json:"mediaAppCdn" bson:"mediaAppCdn"` // 视频CDN(app使用)
+ MediaAppUsCdn *string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn"` // 视频CDN(海外app使用)
+ WatermarkImage *string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
+ WatermarkMedia *string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件(底部右边)
+ WatermarkMediaTop *string `json:"watermarkMediaTop" bson:"watermarkMediaTop"` // 视频水印文件(顶部左边)
+ WatermarkMediaTopRight *string `json:"watermarkMediaTopRight" bson:"watermarkMediaTopRight"` // 视频水印文件(顶部右边)
+ WatermarkMediaBottomLeft *string `json:"watermarkMediaBottomLeft" bson:"watermarkMediaBottomLeft"` // 视频水印文件(底部左边)
+ Mp4Addr *string `json:"mp4Addr" bson:"mp4Addr"` // mp4地址(web使用)
+ NewUrlAddr *string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
+ NewUrlAddr1 *string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
+ NewUrlAddr2 *string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
+ ForeverUrlAddr *string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
+ GmailAddr *string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
+ AppAddr *string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
+ DownloadVideoUrl *string `json:"downloadVideoUrl" bson:"downloadVideoUrl"` // 下载视频URL
+ WeiChart *string `json:"weiChart" bson:"weiChart"` //微信群
+ QQ *string `json:"qq" bson:"qq"` //qq群
+ TgGroup *string `json:"tgGroup" bson:"tgGroup"` //tg群
+ PotaoGroup *string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
+ GitAddr *string `json:"gitAddr" bson:"gitAddr"` //git地址
+ TwitterAddr *string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
+ Telegram *string `json:"telegram" bson:"telegram"` //商务tg
+ TelegramAdd *string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
+ TranslationAddr *string `json:"translationAddr" bson:"translationAddr"` //应用导航
+ //SEO
+ WebTitle *string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
+ WebKeyworld *[]string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
+ WebDescription *string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
+ WebSiteName *string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
+ WebAddr *string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
+ DHNewUrlAddr1 *string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路一
+ DHNewUrlAddr2 *string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
+ DHNewUrlAddr3 *string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
+ DHTitle *string `json:"dhTitle" bson:"dhTitle"` // 导航SEO标题
+ DHKeyworld *[]string `json:"dhKeyworld" bson:"dhKeyworld"` // 导航SEO关键字
+ DHDescription *string `json:"dhDescription" bson:"dhDescription"` // 导航SEO描述
+ DHSiteName *string `json:"dhSiteName" bson:"dhSiteName"` // 导航SEO网站名称
+ ChannelCode *string `json:"channelCode" bson:"channelCode"` // 渠道码
+ EnableH265 *bool `json:"enableH265" bson:"enableH265"` // 是否允许客户端使用 H265 地址播放
+ M3U8DayRateLimitMax *int64 `json:"m3u8DayRateLimitMax" bson:"m3u8DayRateLimitMax"` // 每 IP 每天最多不同正片数,<=0 使用默认 80
+
+ // Acc2UserCDN *string `json:"acc2UserCDN" bson:"acc2UserCDN"` // up主视频播放CDN
+ // InviteAddr *string `json:"inviteAddr" bson:"inviteAddr"` // 用户邀请地址
+ // TelegramContact *string `json:"telegramContact" bson:"telegramContact"` // Telegram联系方式
+ // PotatoContact *string `json:"potatoContact" bson:"potatoContact"` // Potato联系方式
+ // DriverGroup *string `json:"driverGroup" bson:"driverGroup"` // 开车群
+ // CoinPrice *int64 `json:"coinPrice" bson:"coinPrice"` // 金币单价,单位分
+ // OfficialEmail *string `json:"officialEmail" bson:"officialEmail"` // 官方邮箱
+ // HeaderM3U8 *string `json:"headerM3U8" bson:"headerM3U8"` // 片头m3u8
+ // AppCenter *string `json:"appCenter" bson:"appCenter"` // 应用中心
+ // PayGuidPageUrl *string `json:"payGuidPageUrl" bson:"payGuidPageUrl"` // 支付引导页面地址
+ // ApiLimit *cfg.ApiCallLimit `json:"apiLimit" bson:"apiLimit"` // 访问接口和频率参数限制(每10秒)
+ // H5ShareUrl *string `json:"h5ShareUrl" bson:"h5ShareUrl"` // H5分享域名
+ // OnlineNum *int64 `json:"onlineNum" bson:"onlineNum"` // 在线人数
+ // Mp4Url *string `json:"mp4Url" bson:"mp4Url"` // MP4域名
+ // HotScoreConf *cfg.HotScoreConf `json:"hotScoreConf" bson:"hotScoreConf"` // 视频热度值配置
+ // MinimumDiscount *int64 `json:"minimumDiscount" bson:"minimumDiscount"` // 用户最低折扣
+ // AndroidGuestKey *string `json:"androdiGuestkey" bson:"androdiGuestkey"` // android注册签名
+ // H5GuestKey *string `json:"h5GuestKey" bson:"h5GuestKey"` // h5注册签名
+ // CustomerServiceHost *string `json:"customerServiceHost" bson:"customerServiceHost"` // 客服Host
+ // AdsType *cfg.AdsTypeConfig `json:"adsType" bson:"adsType"` // 广告模式
+ // FloatingWindow *FloatingWindow `json:"floatingWindow" bson:"floatingWindow"` // 悬浮窗
+ // IsLoliBlur *bool `json:"isLoliBlur" bson:"isLoliBlur,omitempty"` // 是否开启站群蒙版
+ // ChargeGiftChannel *string `json:"chargeGiftChannel" bson:"chargeGiftChannel"` // 充值赠送金币的渠道
+ // ChargeGiftGoldNumber *int64 `json:"chargeGiftGoldNumber" bson:"chargeGiftGoldNumber"` // 充值赠送金币数量
+ // AdminTG *string `json:"adminTG" bson:"adminTG"` // 管理员tg
+ // Theme *cfg.Theme `json:"theme" bson:"theme"`
+}
+type SysCfgMapParam struct {
+ Key string `json:"key" bson:"key"` //Key
+ StringVal string `json:"stringVal" bson:"stringVal"` //字符串的值
+ IntVal int64 `json:"intVal" bson:"intVal"` //int64的值
+ Desc string `json:"desc" bson:"desc"` //配置描述
+}
+
+type UpdateSysCfgMapReq struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"` //ID
+ Param *SysCfgMapParam `json:"param" bson:"param"`
+}
+
+type DeleteSysCfgMapReq struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"` //ID
+}
+
+type GetSysCfgMapReq struct {
+ Key *string `json:"key" bson:"key"`
+ Desc *string `json:"desc" bson:"desc"`
+}
+
+type FloatingWindow struct {
+ Enable bool `json:"enable" bson:"enable"`
+ Image string `json:"image" bson:"image"` //浮窗图片
+ Url string `json:"url" bson:"url"` //浮窗URL
+ ImageClose string `json:"imageClose" bson:"imageClose"` //浮窗图片
+}
diff --git a/web/service/srv_commend/commend.go b/web/service/srv_commend/commend.go
new file mode 100644
index 0000000..c23b266
--- /dev/null
+++ b/web/service/srv_commend/commend.go
@@ -0,0 +1,417 @@
+package srv_commend
+
+import (
+ "context"
+ "strconv"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func SearchComandList(ctx context.Context, param *CommendSearch) (*CommendSearchResp, *srvdef.SrvErr) {
+
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+
+ if param.ObjectId != nil {
+ filter[commentmod.FN.Comment.ObjectId] = *param.ObjectId
+ }
+ if param.ObjectType != nil {
+ filter[commentmod.FN.Comment.ObjectType] = *param.ObjectType
+ }
+ if param.Level != nil {
+ filter[commentmod.FN.Comment.Level] = *param.Level
+ }
+
+ if param.UserName != nil {
+ filter[commentmod.FN.Comment.UserName] = bson.M{"$regex": *param.UserName}
+ }
+ if param.Text != nil {
+ filter[commentmod.FN.Comment.Text] = bson.M{"$regex": *param.Text}
+ }
+
+ if param.ReviewStatus != nil {
+ filter[commentmod.FN.Comment.ReviewStatus] = *param.ReviewStatus
+ }
+
+ sort := bson.D{
+ {commentmod.FN.Comment.TopRank, -1},
+ {commentmod.FN.Comment.CreatedAt, -1},
+ }
+
+ opt.SetSort(sort)
+ skip := (param.PageNum - 1) * param.PageSize
+ opt.SetSkip(skip)
+ opt.SetLimit(param.PageSize)
+
+ total, serr := commentmod.CountComments(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ if total == 0 {
+ return &CommendSearchResp{}, nil
+ }
+
+ commends, serr := commentmod.FindManyComment(ctx, filter, opt)
+ if serr != nil {
+ return nil, serr
+ }
+
+ resp := &CommendSearchResp{
+ Total: total,
+ List: make([]*PostCommentInfo, 0),
+ }
+
+ for _, commendInfo := range commends {
+ tmp := &PostCommentInfo{
+ Id: commendInfo.Id,
+ ObjectId: commendInfo.ObjectId,
+ ObjectName: commendInfo.ObjectName,
+ UserName: commendInfo.UserName,
+ Level: commendInfo.Level,
+ ParentsId: commendInfo.ParentsId,
+ ReplyUserName: commendInfo.ReplyUserName,
+ Text: commendInfo.Text,
+ ReviewStatus: commendInfo.ReviewStatus,
+ UpdatedAt: commendInfo.UpdatedAt,
+ Ip: commendInfo.Ip,
+ CreatedAt: commendInfo.CreatedAt,
+ ObjectType: commendInfo.ObjectType,
+ Likes: commendInfo.CountLike,
+ }
+ resp.List = append(resp.List, tmp)
+ }
+
+ return resp, nil
+}
+
+// DelCommend 删除一条评论
+func DelCommend(ctx context.Context, commendId int64) *srvdef.SrvErr {
+ commendInfo, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId})
+ if serr != nil {
+ return serr
+ }
+ _, serr = commentmod.DelOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId})
+ if serr != nil {
+ return serr
+ }
+ if serr = commentlikemod.DeleteByCommentIds(ctx, []int64{commendId}); serr != nil {
+ return serr
+ }
+ clearCommentListCacheByComments(commentmod.CommentModelList{commendInfo})
+
+ if err := refreshParentReplyCounts(ctx, commentmod.CommentModelList{commendInfo}); err != nil {
+ return err
+ }
+ return updateCommentCountByComments(ctx, commentmod.CommentModelList{commendInfo})
+}
+
+// DelCommend 删除用户所有评论
+func DelUserCommend(ctx context.Context, ip string) *srvdef.SrvErr {
+ commendList, serr := commentmod.FindManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip})
+ if serr != nil {
+ return serr
+ }
+ _, serr = commentmod.DelManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip})
+ if serr != nil {
+ return serr
+ }
+ if serr = commentlikemod.DeleteByCommentIds(ctx, commentIdsFromModels(commendList)); serr != nil {
+ return serr
+ }
+ clearCommentListCacheByComments(commendList)
+
+ if err := refreshParentReplyCounts(ctx, commendList); err != nil {
+ return err
+ }
+ return updateCommentCountByComments(ctx, commendList)
+}
+
+func commentIdsFromModels(comments commentmod.CommentModelList) []int64 {
+ ids := make([]int64, 0, len(comments))
+ for _, comment := range comments {
+ if comment != nil {
+ ids = append(ids, comment.Id)
+ }
+ }
+ return ids
+}
+
+// AddPostComment 发布评论
+func AddPostComment(ctx context.Context, req *CommendReplay) *srvdef.SrvErr {
+ now := time.Now()
+ if req.Score > 10 {
+ log.Error("AddPostComment ErrParamError", log.Any("Score", req.Score))
+ return srvdef.New(srvdef.ErrParamError, "", "")
+ }
+
+ // 2. 生成评论相关信息
+ commentInfo := &commentmod.CommentModel{
+ Id: mongos.Lid.Comment.Get(),
+ ObjectId: req.ObjectId,
+ ObjectType: pubdef.CommentObjectTypeVideo,
+ ObjectName: pubdef.CommentObjectTypeName(pubdef.CommentObjectTypeVideo),
+ UserName: req.UserName,
+ ParentsId: req.ParentsId,
+ Text: req.Text,
+ Level: 1,
+ UpdatedAt: now,
+ CreatedAt: now,
+ }
+
+ if commentInfo.ParentsId > 0 {
+ commentInfo.Level = 2
+ } else {
+ commentInfo.Level = 1
+ }
+ commentInfo.ReviewStatus = 1 // 管理端的默认审核通过
+
+ // 3. 插入数据库
+ serr := commentmod.InsertOneComment(ctx, commentInfo)
+ if serr != nil {
+ log.Error("AddPostComment InsertOnePostComment", log.Any("req", req), log.Any("err", serr))
+ return serr
+ }
+ clearCommentListCacheByComments(commentmod.CommentModelList{commentInfo})
+
+ go func() {
+ _ = updateCommentCountByComments(ctx, commentmod.CommentModelList{commentInfo})
+
+ // 4. 修改回复条数
+ if req.ParentsId > 0 {
+ filter := bson.M{
+ commentmod.FN.Comment.Id: req.ParentsId,
+ }
+ updateDoc := bson.M{
+ "$inc": bson.M{
+ commentmod.FN.Comment.CommentCount: 1,
+ },
+ }
+ serr = commentmod.UpdateOneComment(ctx, filter, updateDoc)
+ if serr != nil {
+ log.Error("AddPostComment UpdateOnePostComment", log.Any("req", req), log.Any("err", serr))
+ return
+ }
+ }
+
+ }()
+ return nil
+}
+
+// 置顶评论
+func TopPostComment(ctx context.Context, id int64) *srvdef.SrvErr {
+ filter := bson.M{
+ commentmod.FN.Comment.Id: id,
+ }
+ comment, serr := commentmod.FindOneComment(ctx, filter)
+ if serr != nil {
+ return serr
+ }
+ //获取置顶的一条评论
+ topRankComment, serr := getTopRankComment(ctx, comment.ObjectId)
+ if serr != nil {
+ return serr
+ }
+ if topRankComment.Id == id && topRankComment.TopRank != 0 {
+ return nil
+ }
+ serr = commentmod.UpdateOneComment(ctx, filter, bson.M{
+ "$set": bson.M{
+ commentmod.FN.Comment.TopRank: topRankComment.TopRank + 1,
+ },
+ })
+ if serr != nil {
+ return serr
+ }
+ clearCommentListCacheByComments(commentmod.CommentModelList{comment})
+ return nil
+}
+
+func getTopRankComment(ctx context.Context, objectId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
+ filter := bson.M{
+ commentmod.FN.Comment.ObjectId: objectId,
+ }
+ opts := options.FindOne().SetSort(bson.M{
+ commentmod.FN.Comment.TopRank: -1,
+ })
+ comment, serr := commentmod.FindOneComment(ctx, filter, opts)
+ if serr != nil {
+ return nil, serr
+ }
+ return comment, nil
+}
+
+func ReviewComment(ctx context.Context, id int64, reviewStatus int) *srvdef.SrvErr {
+ comment, err := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: id})
+ if err != nil {
+ return err
+ }
+ updateDoc := bson.M{
+ commentmod.FN.Comment.ReviewStatus: reviewStatus,
+ }
+ filter := bson.M{
+ commentmod.FN.Comment.Id: id,
+ }
+ serr := commentmod.UpdateOneComment(ctx, filter, bson.M{"$set": updateDoc})
+ if serr != nil {
+ log.Error("审核评论失败", log.Any("id", id), log.Any("reviewStatus", reviewStatus), log.Any("err", serr))
+ return serr
+ }
+
+ clearCommentListCacheByComments(commentmod.CommentModelList{comment})
+ if comment.ReviewStatus != reviewStatus {
+ if err := refreshParentReplyCounts(ctx, commentmod.CommentModelList{comment}); err != nil {
+ return err
+ }
+ }
+ return updateCommentCountByComments(ctx, commentmod.CommentModelList{comment})
+}
+
+// ReviewCommendBatch 批量更新
+func ReviewCommendBatch(ctx context.Context, commendIds *[]int64, reviewStatus int) *srvdef.SrvErr {
+ if commendIds == nil || len(*commendIds) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ commentList, err := commentmod.FindManyComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: bson.M{"$in": *commendIds},
+ })
+ if err != nil {
+ return err
+ }
+ updateDoc := bson.M{
+ commentmod.FN.Comment.ReviewStatus: reviewStatus,
+ }
+ filter := bson.M{commentmod.FN.Comment.Id: bson.M{"$in": commendIds}}
+ serr := commentmod.UpdateManyComment(ctx, filter, bson.M{"$set": updateDoc})
+ if serr != nil {
+ log.Error("批量审核评论失败", log.Any("commendId", *commendIds), log.Any("reviewStatus", reviewStatus), log.Any("err", serr))
+ return serr
+ }
+
+ clearCommentListCacheByComments(commentList)
+ if err := refreshParentReplyCounts(ctx, commentList); err != nil {
+ return err
+ }
+ return updateCommentCountByComments(ctx, commentList)
+}
+
+// refreshParentReplyCounts 按已审核的直接回复重算父评论回复数
+func refreshParentReplyCounts(ctx context.Context, comments commentmod.CommentModelList) *srvdef.SrvErr {
+ seen := make(map[int64]struct{}, len(comments))
+ for _, comment := range comments {
+ if comment == nil || comment.ParentsId <= 0 {
+ continue
+ }
+ if _, ok := seen[comment.ParentsId]; ok {
+ continue
+ }
+ seen[comment.ParentsId] = struct{}{}
+ count, err := commentmod.CountComments(ctx, bson.M{
+ commentmod.FN.Comment.ParentsId: comment.ParentsId,
+ commentmod.FN.Comment.ReviewStatus: 1,
+ })
+ if err != nil {
+ return err
+ }
+ if err := commentmod.UpdateOneComment(ctx,
+ bson.M{commentmod.FN.Comment.Id: comment.ParentsId},
+ bson.M{"$set": bson.M{commentmod.FN.Comment.CommentCount: count}}); err != nil && err.Code != srvdef.ErrNoData {
+ return err
+ }
+ }
+ return nil
+}
+
+// clearCommentListCacheByComments 按对象ID和对象类型精确清理评论列表缓存
+func clearCommentListCacheByComments(commentList commentmod.CommentModelList) {
+ seen := make(map[string]struct{}, len(commentList))
+ for _, comment := range commentList {
+ if comment == nil {
+ continue
+ }
+ key := commentCacheUniqueKey(comment.ObjectId, comment.ObjectType)
+ if _, ok := seen[key]; ok {
+ continue
+ }
+ seen[key] = struct{}{}
+ cacheKey := redisconst.CommentListCacheKey(comment.ObjectId, comment.ObjectType)
+ if err := redis.Del(cacheKey); err != nil {
+ log.Warn("清理评论列表缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
+ }
+ }
+}
+
+// commentCacheUniqueKey 生成对象维度去重key,避免批量审核时重复清理同一对象缓存
+func commentCacheUniqueKey(objectId int64, objectType pubdef.CommentObjectType) string {
+ return strconv.FormatInt(objectId, 10) + "_" + strconv.FormatInt(int64(objectType), 10)
+}
+
+// UpdateCommentCount 评论审核通过/删除后,同步更新对应媒体内容的真实评论数
+// objectType: 0=视频 4=短剧 11=短剧剧集
+func UpdateCommentCount(ctx context.Context, commendIds []int64) *srvdef.SrvErr {
+ commentList, _ := commentmod.FindManyComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: bson.M{"$in": commendIds},
+ })
+ return updateCommentCountByComments(ctx, commentList)
+}
+
+func updateCommentCountByComments(ctx context.Context, commentList commentmod.CommentModelList) *srvdef.SrvErr {
+ seen := make(map[string]struct{}, len(commentList))
+ for _, comment := range commentList {
+ if comment == nil {
+ continue
+ }
+ key := commentCacheUniqueKey(comment.ObjectId, comment.ObjectType)
+ if _, ok := seen[key]; ok {
+ continue
+ }
+ seen[key] = struct{}{}
+ count, countErr := commentmod.CountComments(ctx, bson.M{
+ commentmod.FN.Comment.ObjectId: comment.ObjectId,
+ commentmod.FN.Comment.ObjectType: comment.ObjectType,
+ commentmod.FN.Comment.ReviewStatus: 1,
+ })
+ if countErr != nil {
+ return countErr
+ }
+ var serr *srvdef.SrvErr
+ switch comment.ObjectType {
+ case pubdef.CommentObjectTypeVideo:
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Id: comment.ObjectId},
+ bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.Real.Comments: count}},
+ )
+ case pubdef.CommentObjectTypePlaylet:
+ serr = playletmod.UpdateOnePlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: comment.ObjectId},
+ bson.M{"$set": bson.M{playletmod.FN.Playlet.CountComment: count}},
+ )
+ case pubdef.CommentObjectTypePlayletChapter:
+ serr = playletchaptermod.UpdateOnePlayletChapter(ctx,
+ bson.M{playletchaptermod.FN.PlayletChapter.Id: comment.ObjectId},
+ bson.M{"$set": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: count}},
+ )
+ }
+ if serr != nil {
+ log.Error("UpdateCommentCount 更新评论数失败",
+ log.Any("objectId", comment.ObjectId),
+ log.Any("objectType", comment.ObjectType),
+ log.Any("err", serr),
+ )
+ return serr
+ }
+ }
+ return nil
+}
diff --git a/web/service/srv_commend/define.go b/web/service/srv_commend/define.go
new file mode 100644
index 0000000..161d274
--- /dev/null
+++ b/web/service/srv_commend/define.go
@@ -0,0 +1,73 @@
+package srv_commend
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// CommendSearch 查询评论
+type CommendSearch struct {
+ ObjectId *int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectType *pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
+ UserName *string `json:"userName" bson:"userName"` // 用户名称
+ Level *int64 `json:"level" bson:"level"` // 评论级别
+ Text *string `json:"text" bson:"text"` // 评论内容
+ ReviewStatus *int `json:"reviewStatus"`
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+}
+
+type PostCommentInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+ Ip string `json:"ip" bson:"ip"` // 评论IP
+ ObjectType pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectName string `json:"objectName" bson:"objectName"` // 对象类型名称 视频/短剧
+ UserName string `json:"userName" bson:"userName"` // 用户名称
+ Level int64 `json:"level" bson:"level"` // 评论级别
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
+ Text string `json:"text" bson:"text"` // 评论内容
+ ReviewStatus int `json:"reviewStatus" bson:"reviewStatus"` // 审核状态 0 未审核 1审核成功 2 审核失败
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
+ TopRank int64 `json:"topRank" bson:"topRank"` // 置顶排序
+ Likes int64 `json:"likes" bson:"likes"` // 点赞数
+ Status pubdef.UserStatus `json:"status" bson:"status"` // 评论用户的状态
+}
+
+type CommendSearchResp struct {
+ Total int64 `json:"total"`
+ List []*PostCommentInfo `json:"list"`
+}
+
+type CommendDelete struct {
+ Id *int64 `json:"id" bson:"_id" binding:"required"` // Id
+}
+
+type CommendReplay struct {
+ ObjectId int64 `json:"objectId"` // 对象ID
+ UserName string `json:"userName"` // 用户名
+ ParentsId int64 `json:"parentsId"` // 评论ID
+ Text string `json:"text"` // 评论内容
+ Score int64 `json:"score"` // 评分
+}
+
+type CommendUpdate struct {
+ Score *int64 `json:"score" bson:"score,omitempty"` // 评分
+ Likes *int64 `json:"likes" bson:"likes,omitempty"` // 点赞数量
+}
+
+type CommendUpdateReq struct {
+ Param CommendUpdate `json:"param" bson:"param"` // 参数
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+}
+
+type CommendTopReq struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+}
+
+type CommendReviewReq struct {
+ Id int64 `json:"id"`
+ ReviewStatus int `json:"reviewStatus"`
+}
diff --git a/web/service/srv_contactus/contactus.go b/web/service/srv_contactus/contactus.go
new file mode 100644
index 0000000..7f65cfe
--- /dev/null
+++ b/web/service/srv_contactus/contactus.go
@@ -0,0 +1,68 @@
+package srv_contactus
+
+import (
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/contactusmod"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func GetContactusList(ctx *gin.Context, req *GetContactusListReq) (resp *GetContactusListResp, err *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.CategoryName != nil && len(*req.CategoryName) > 0 {
+ filter[contactusmod.FN.ContactUsModel.CategoryName] = *req.CategoryName
+ }
+ if req.Ip != nil && len(*req.Ip) > 0 {
+ filter[contactusmod.FN.ContactUsModel.Ip] = *req.Ip
+ }
+ if req.Name != nil && len(*req.Name) > 0 {
+ filter[contactusmod.FN.ContactUsModel.Name] = *req.Name
+ }
+ if req.Email != nil && len(*req.Email) > 0 {
+ filter[contactusmod.FN.ContactUsModel.Email] = *req.Email
+ }
+ var total int64
+ total, err = contactusmod.CountContactUsModels(ctx, filter)
+ if err != nil {
+ log.Warn("GetContactusList.CountContactUsModels", log.Any("filter", filter), log.E(err))
+ return
+ }
+ resp = &GetContactusListResp{
+ Total: 0,
+ List: make(contactusmod.ContactUsModelList, 0),
+ }
+ if total == 0 {
+ return resp, nil
+ }
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{contactusmod.FN.ContactUsModel.CreatedAt: -1})
+ opts.SetLimit(req.PageSize).SetSkip((req.PageNum - 1) * req.PageSize)
+ list, err := contactusmod.FindManyContactUsModel(ctx, filter, opts)
+ if err != nil {
+ log.Warn("GetContactusList.FindManyContactUsModels", log.Any("filter", filter), log.E(err))
+ return
+ }
+ resp.List = list
+ return resp, nil
+}
+func DelContactus(ctx *gin.Context, id primitive.ObjectID) *srvdef.SrvErr {
+ if id.IsZero() {
+ return srvdef.New(srvdef.ErrParamError)
+ }
+ _, err := contactusmod.DelOneContactUsModel(ctx, bson.M{contactusmod.FN.ContactUsModel.Id: id})
+ return err
+}
+
+func UpdateContactus(ctx *gin.Context, id primitive.ObjectID, status int64) *srvdef.SrvErr {
+ if id.IsZero() {
+ return srvdef.New(srvdef.ErrParamError)
+ }
+ err := contactusmod.UpdateOneContactUsModel(ctx, bson.M{contactusmod.FN.ContactUsModel.Id: id}, bson.M{
+ "$set": bson.M{contactusmod.FN.ContactUsModel.Status: status},
+ })
+ return err
+}
diff --git a/web/service/srv_contactus/define.go b/web/service/srv_contactus/define.go
new file mode 100644
index 0000000..bc92b15
--- /dev/null
+++ b/web/service/srv_contactus/define.go
@@ -0,0 +1,30 @@
+package srv_contactus
+
+import (
+ "videoweb/pub/models/contactusmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type GetContactusListReq struct {
+ pubdef.Page
+ Ip *string `form:"ip" json:"ip"` // ip
+ Name *string `form:"name" json:"name"` // 姓名
+ Email *string `form:"email" json:"email"` // 邮箱
+ CategoryName *string `form:"categoryName" json:"categoryName"` // 分类
+}
+
+type GetContactusListResp struct {
+ Total int64 `json:"total"`
+ List contactusmod.ContactUsModelList `json:"list"`
+}
+
+type DelOneContactusReq struct {
+ Id primitive.ObjectID `form:"id" json:"id"`
+}
+
+type UpdateContactusReq struct {
+ Id primitive.ObjectID `form:"id" json:"id"`
+ Status int64 `form:"status" json:"status"`
+}
diff --git a/web/service/srv_contentexcel/contentexcel.go b/web/service/srv_contentexcel/contentexcel.go
new file mode 100644
index 0000000..aa25010
--- /dev/null
+++ b/web/service/srv_contentexcel/contentexcel.go
@@ -0,0 +1,325 @@
+package srv_contentexcel
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math/rand"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const queryBatchSize = 500
+
+var importAddedTimeStart = time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local)
+
+func randomImportAddedTime(r *rand.Rand) time.Time {
+ now := time.Now()
+ if !now.After(importAddedTimeStart) {
+ return now
+ }
+ if r == nil {
+ r = rand.New(rand.NewSource(time.Now().UnixNano()))
+ }
+ delta := now.UnixNano() - importAddedTimeStart.UnixNano()
+ if delta <= 0 {
+ return now
+ }
+ return importAddedTimeStart.Add(time.Duration(r.Int63n(delta + 1)))
+}
+
+// Export 按条件导出视频 SEO 字段为 Excel。
+func Export(ctx context.Context, req *ExportReq) ([]byte, string, *srvdef.SrvErr) {
+ contentType, serr := normalizeContentType(req.Type)
+ if serr != nil {
+ return nil, "", serr
+ }
+
+ dateFilter, serr := buildTimeRangeFilter(req.StartTime, req.EndTime)
+ if serr != nil {
+ return nil, "", serr
+ }
+
+ ids, serr := normalizeExportIDs(req.Ids)
+ if serr != nil {
+ return nil, "", serr
+ }
+
+ var rows []ContentRow
+ var err error
+ switch contentType {
+ case ContentTypeVideo:
+ rows, err = queryVideoRows(ctx, dateFilter, req.Status, ids)
+ }
+ if err != nil {
+ return nil, "", srvdef.New(srvdef.ExcelExportErr, err.Error())
+ }
+
+ data, err := buildWorkbook(rows)
+ if err != nil {
+ return nil, "", srvdef.New(srvdef.ExcelExportErr, err.Error())
+ }
+
+ filename := fmt.Sprintf("%s_content_%s.xlsx", contentType, time.Now().Format("20060102150405"))
+ return data, filename, nil
+}
+
+// Import 读取 Excel 并按 ID 批量更新标题、描述、关键词。
+func Import(ctx context.Context, contentType string, reader io.Reader) (*ImportResult, *srvdef.SrvErr) {
+ normalizedType, serr := normalizeContentType(contentType)
+ if serr != nil {
+ return nil, serr
+ }
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "读取上传文件失败")
+ }
+
+ rows, err := parseWorkbook(data)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, err.Error())
+ }
+
+ result := &ImportResult{Total: len(rows)}
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for _, row := range rows {
+ var updateErr *srvdef.SrvErr
+ switch normalizedType {
+ case ContentTypeVideo:
+ updateErr = updateVideoRow(ctx, row, r)
+ }
+ if updateErr != nil {
+ result.Failed++
+ result.Errors = append(result.Errors, fmt.Sprintf("ID=%d: %s", row.ID, updateErr.Tips))
+ continue
+ }
+ result.Success++
+ }
+ return result, nil
+}
+
+func normalizeContentType(raw string) (string, *srvdef.SrvErr) {
+ switch stringsTrimLower(raw) {
+ case ContentTypeVideo:
+ return stringsTrimLower(raw), nil
+ default:
+ return "", srvdef.New(srvdef.ErrParamError, "type 仅支持 video")
+ }
+}
+
+func stringsTrimLower(s string) string {
+ return strings.ToLower(strings.TrimSpace(s))
+}
+
+func buildTimeRangeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
+ filter := bson.M{}
+ start, err := parseOptionalTime(startTime, false)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "startTime 格式错误,支持 2006-01-02 或 2006-01-02 15:04:05")
+ }
+ end, err := parseOptionalTime(endTime, true)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "endTime 格式错误,支持 2006-01-02 或 2006-01-02 15:04:05")
+ }
+ if start != nil && end != nil && start.After(*end) {
+ return nil, srvdef.New(srvdef.ErrParamError, "startTime 不能晚于 endTime")
+ }
+ if start != nil {
+ filter["$gte"] = *start
+ }
+ if end != nil {
+ filter["$lte"] = *end
+ }
+ if len(filter) == 0 {
+ return nil, nil
+ }
+ return filter, nil
+}
+
+func parseOptionalTime(raw string, endOfDay bool) (*time.Time, error) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil, nil
+ }
+ layouts := []string{
+ "2006-01-02 15:04:05",
+ "2006-01-02T15:04:05",
+ time.RFC3339,
+ }
+ for _, layout := range layouts {
+ if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
+ return &t, nil
+ }
+ }
+ if t, err := time.ParseInLocation("2006-01-02", raw, time.Local); err == nil {
+ if endOfDay {
+ end := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
+ return &end, nil
+ }
+ return &t, nil
+ }
+ return nil, fmt.Errorf("invalid time: %s", raw)
+}
+
+func applyTimeFilter(base bson.M, field string, rangeFilter bson.M) bson.M {
+ if len(rangeFilter) == 0 {
+ return base
+ }
+ base[field] = rangeFilter
+ return base
+}
+
+func applyStatusFilter(base bson.M, field string, status *pubdef.InformationStatus) bson.M {
+ if status == nil {
+ return base
+ }
+ base[field] = *status
+ return base
+}
+
+func applyIDsFilter(base bson.M, field string, ids []int64) bson.M {
+ if len(ids) == 0 {
+ return base
+ }
+ base[field] = bson.M{"$in": ids}
+ return base
+}
+
+// ParseExportIDs 解析导出接口的 ids 查询参数,支持 ids=1&ids=2 或 ids=1,2,3。
+func ParseExportIDs(raw []string) ([]int64, *srvdef.SrvErr) {
+ if len(raw) == 0 {
+ return nil, nil
+ }
+
+ ids := make([]int64, 0, len(raw))
+ seen := make(map[int64]struct{}, len(raw))
+ for _, item := range raw {
+ item = strings.TrimSpace(item)
+ if item == "" {
+ continue
+ }
+ for _, part := range strings.Split(item, ",") {
+ part = strings.TrimSpace(part)
+ if part == "" {
+ continue
+ }
+ id, err := strconv.ParseInt(part, 10, 64)
+ if err != nil || id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids 格式错误,需为正整数")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ }
+ return ids, nil
+}
+
+func normalizeExportIDs(raw []int64) ([]int64, *srvdef.SrvErr) {
+ if len(raw) == 0 {
+ return nil, nil
+ }
+
+ ids := make([]int64, 0, len(raw))
+ seen := make(map[int64]struct{}, len(raw))
+ for _, id := range raw {
+ if id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids 格式错误,需为正整数")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ return ids, nil
+}
+
+func queryVideoRows(ctx context.Context, dateFilter bson.M, status *pubdef.InformationStatus, ids []int64) ([]ContentRow, error) {
+ filter := applyIDsFilter(applyStatusFilter(applyTimeFilter(bson.M{
+ videoinformationmod.FN.VideoInformation.MediaType: bson.M{
+ "$in": []videoinformationmod.MediaType{
+ videoinformationmod.MediaType_Video,
+ videoinformationmod.MediaType_AV,
+ },
+ },
+ }, videoinformationmod.FN.VideoInformation.CreatedAt, dateFilter),
+ videoinformationmod.FN.VideoInformation.Status, status),
+ videoinformationmod.FN.VideoInformation.Id, ids)
+
+ return queryRowsInBatches(ctx, func(skip int64) ([]ContentRow, bool, error) {
+ opt := options.Find().
+ SetSkip(skip).
+ SetLimit(queryBatchSize).
+ SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}).
+ SetProjection(bson.M{
+ videoinformationmod.FN.VideoInformation.Id: 1,
+ videoinformationmod.FN.VideoInformation.Title: 1,
+ videoinformationmod.FN.VideoInformation.Desc: 1,
+ videoinformationmod.FN.VideoInformation.Keyworld: 1,
+ })
+
+ list, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
+ if serr != nil {
+ return nil, false, fmt.Errorf("%s", serr.Tips)
+ }
+ rows := make([]ContentRow, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ rows = append(rows, ContentRow{
+ ID: item.Id,
+ Title: item.Title,
+ Desc: item.Desc,
+ Keyworld: item.Keyworld,
+ })
+ }
+ return rows, int64(len(list)) == queryBatchSize, nil
+ })
+}
+
+func queryRowsInBatches(ctx context.Context, fetch func(skip int64) ([]ContentRow, bool, error)) ([]ContentRow, error) {
+ all := make([]ContentRow, 0)
+ var skip int64
+ for {
+ rows, hasMore, err := fetch(skip)
+ if err != nil {
+ return nil, err
+ }
+ all = append(all, rows...)
+ if !hasMore {
+ break
+ }
+ skip += queryBatchSize
+ }
+ return all, nil
+}
+
+func updateVideoRow(ctx context.Context, row ContentRow, r *rand.Rand) *srvdef.SrvErr {
+ now := time.Now()
+ addedTime := randomImportAddedTime(r)
+ update := bson.M{
+ "$set": bson.M{
+ videoinformationmod.FN.VideoInformation.Title: row.Title,
+ videoinformationmod.FN.VideoInformation.Desc: row.Desc,
+ videoinformationmod.FN.VideoInformation.Keyworld: row.Keyworld,
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.AddedTime: addedTime,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: now,
+ },
+ }
+ return videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: row.ID,
+ }, update)
+}
diff --git a/web/service/srv_contentexcel/define.go b/web/service/srv_contentexcel/define.go
new file mode 100644
index 0000000..15ec415
--- /dev/null
+++ b/web/service/srv_contentexcel/define.go
@@ -0,0 +1,32 @@
+package srv_contentexcel
+
+import "videoweb/pub/pubdef"
+
+const (
+ ContentTypeVideo = "video"
+)
+
+// ExportReq 导出查询参数。
+type ExportReq struct {
+ Type string `form:"type" binding:"required"` // video
+ StartTime string `form:"startTime"` // 可选,按 createdAt 筛选,格式 2006-01-02 或 2006-01-02 15:04:05
+ EndTime string `form:"endTime"` // 可选,按 createdAt 筛选
+ Status *pubdef.InformationStatus `form:"status"` // 可选,0初始化 1上架 2下架,不传查全部
+ Ids []int64 // 可选,按 ID 筛选,支持 ids=1&ids=2 或 ids=1,2,3
+}
+
+// ImportResult 导入结果。
+type ImportResult struct {
+ Total int `json:"total"`
+ Success int `json:"success"`
+ Failed int `json:"failed"`
+ Errors []string `json:"errors,omitempty"`
+}
+
+// ContentRow Excel 单行数据。
+type ContentRow struct {
+ ID int64
+ Title string
+ Desc string
+ Keyworld []string
+}
diff --git a/web/service/srv_contentexcel/excel.go b/web/service/srv_contentexcel/excel.go
new file mode 100644
index 0000000..e6026d1
--- /dev/null
+++ b/web/service/srv_contentexcel/excel.go
@@ -0,0 +1,160 @@
+package srv_contentexcel
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/xuri/excelize/v2"
+)
+
+const (
+ colID = "ID"
+ colTitle = "标题"
+ colDesc = "描述"
+ colKeyworld = "关键词"
+ colRedLineRemark = "备注(红线-建议下架)"
+ sheetName = "Sheet1"
+ excelHeaders = 4
+ colRedLineRemarkIdx = 4
+)
+
+var headerAliases = map[string]int{
+ "id": 0, colID: 0,
+ "title": 1, colTitle: 1,
+ "desc": 2, "description": 2, colDesc: 2,
+ "keyworld": 3, "keywords": 3, "keyword": 3, colKeyworld: 3,
+ colRedLineRemark: colRedLineRemarkIdx,
+}
+
+func buildWorkbook(rows []ContentRow) ([]byte, error) {
+ f := excelize.NewFile()
+ defer f.Close()
+
+ if err := f.SetSheetRow(sheetName, "A1", &[]string{colID, colTitle, colDesc, colKeyworld}); err != nil {
+ return nil, err
+ }
+
+ for i, row := range rows {
+ cell, err := excelize.CoordinatesToCellName(1, i+2)
+ if err != nil {
+ return nil, err
+ }
+ if err := f.SetSheetRow(sheetName, cell, &[]any{
+ row.ID,
+ row.Title,
+ row.Desc,
+ strings.Join(row.Keyworld, ","),
+ }); err != nil {
+ return nil, err
+ }
+ }
+
+ var buf bytes.Buffer
+ if err := f.Write(&buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func parseWorkbook(data []byte) ([]ContentRow, error) {
+ f, err := excelize.OpenReader(bytes.NewReader(data))
+ if err != nil {
+ return nil, fmt.Errorf("打开 Excel 失败: %w", err)
+ }
+ defer f.Close()
+
+ sheets := f.GetSheetList()
+ if len(sheets) == 0 {
+ return nil, fmt.Errorf("Excel 无工作表")
+ }
+
+ rows, err := f.GetRows(sheets[0])
+ if err != nil {
+ return nil, fmt.Errorf("读取 Excel 失败: %w", err)
+ }
+ if len(rows) <= 1 {
+ return nil, fmt.Errorf("Excel 无数据行")
+ }
+
+ colIndex := mapHeaderColumns(rows[0])
+ if len(colIndex) < excelHeaders {
+ return nil, fmt.Errorf("Excel 表头需包含: ID、标题、描述、关键词")
+ }
+
+ result := make([]ContentRow, 0, len(rows)-1)
+ for lineNo, row := range rows[1:] {
+ contentRow, ok, err := parseDataRow(row, colIndex, lineNo+2)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ continue
+ }
+ result = append(result, contentRow)
+ }
+ if len(result) == 0 {
+ return nil, fmt.Errorf("Excel 无有效数据行")
+ }
+ return result, nil
+}
+
+func mapHeaderColumns(header []string) map[int]int {
+ index := make(map[int]int, excelHeaders)
+ for i, name := range header {
+ key := strings.ToLower(strings.TrimSpace(name))
+ if col, ok := headerAliases[key]; ok {
+ index[col] = i
+ }
+ }
+ return index
+}
+
+func parseDataRow(row []string, colIndex map[int]int, lineNo int) (ContentRow, bool, error) {
+ if remarkIdx, ok := colIndex[colRedLineRemarkIdx]; ok {
+ if cellValue(row, remarkIdx) != "" {
+ return ContentRow{}, false, nil
+ }
+ }
+
+ idStr := cellValue(row, colIndex[0])
+ if idStr == "" {
+ return ContentRow{}, false, nil
+ }
+ id, err := strconv.ParseInt(idStr, 10, 64)
+ if err != nil || id <= 0 {
+ return ContentRow{}, false, fmt.Errorf("第 %d 行 ID 无效: %q", lineNo, idStr)
+ }
+
+ return ContentRow{
+ ID: id,
+ Title: cellValue(row, colIndex[1]),
+ Desc: cellValue(row, colIndex[2]),
+ Keyworld: parseKeywords(cellValue(row, colIndex[3])),
+ }, true, nil
+}
+
+func cellValue(row []string, index int) string {
+ if index < 0 || index >= len(row) {
+ return ""
+ }
+ return strings.TrimSpace(row[index])
+}
+
+func parseKeywords(raw string) []string {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil
+ }
+ raw = strings.NewReplacer(",", ",", ";", ",", ";", ",").Replace(raw)
+ parts := strings.Split(raw, ",")
+ keywords := make([]string, 0, len(parts))
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if part != "" {
+ keywords = append(keywords, part)
+ }
+ }
+ return keywords
+}
diff --git a/web/service/srv_contentexcel/excel_test.go b/web/service/srv_contentexcel/excel_test.go
new file mode 100644
index 0000000..fe85e4f
--- /dev/null
+++ b/web/service/srv_contentexcel/excel_test.go
@@ -0,0 +1,201 @@
+package srv_contentexcel
+
+import (
+ "bytes"
+ "math/rand"
+ "testing"
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+
+ "github.com/xuri/excelize/v2"
+)
+
+func TestBuildTimeRangeFilter(t *testing.T) {
+ t.Parallel()
+
+ filter, err := buildTimeRangeFilter("2026-01-01", "2026-01-31")
+ if err != nil {
+ t.Fatalf("buildTimeRangeFilter() err = %v", err)
+ }
+ if filter["$gte"] == nil || filter["$lte"] == nil {
+ t.Fatalf("buildTimeRangeFilter() = %#v, want range filter", filter)
+ }
+
+ empty, err := buildTimeRangeFilter("", "")
+ if err != nil {
+ t.Fatalf("buildTimeRangeFilter() empty err = %v", err)
+ }
+ if len(empty) != 0 {
+ t.Fatalf("buildTimeRangeFilter() empty = %#v, want nil/empty", empty)
+ }
+}
+
+func TestApplyStatusFilter(t *testing.T) {
+ t.Parallel()
+
+ base := bson.M{"a": 1}
+ if got := applyStatusFilter(base, "status", nil); len(got) != 1 {
+ t.Fatalf("nil status should not add filter: %#v", got)
+ }
+
+ status := pubdef.InformationStatusVerifyOK
+ got := applyStatusFilter(bson.M{}, videoinformationmod.FN.VideoInformation.Status, &status)
+ if got[videoinformationmod.FN.VideoInformation.Status] != pubdef.InformationStatusVerifyOK {
+ t.Fatalf("applyStatusFilter() status = %#v", got[videoinformationmod.FN.VideoInformation.Status])
+ }
+
+ start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.Local)
+ end := time.Date(2026, 1, 31, 23, 59, 59, 0, time.Local)
+ dateFilter := bson.M{"$gte": start, "$lte": end}
+ videoFilter := applyIDsFilter(applyStatusFilter(
+ applyTimeFilter(bson.M{}, videoinformationmod.FN.VideoInformation.CreatedAt, dateFilter),
+ videoinformationmod.FN.VideoInformation.Status, &status,
+ ), videoinformationmod.FN.VideoInformation.Id, []int64{1001, 1002})
+ if videoFilter[videoinformationmod.FN.VideoInformation.CreatedAt] == nil {
+ t.Fatal("expected createdAt filter")
+ }
+ if videoFilter[videoinformationmod.FN.VideoInformation.Status] != pubdef.InformationStatusVerifyOK {
+ t.Fatal("expected status filter")
+ }
+ if videoFilter[videoinformationmod.FN.VideoInformation.Id] == nil {
+ t.Fatal("expected id filter")
+ }
+}
+
+func TestParseExportIDs(t *testing.T) {
+ t.Parallel()
+
+ ids, err := ParseExportIDs([]string{"1001,1002", "1003"})
+ if err != nil {
+ t.Fatalf("ParseExportIDs() err = %v", err)
+ }
+ if len(ids) != 3 || ids[0] != 1001 || ids[1] != 1002 || ids[2] != 1003 {
+ t.Fatalf("ParseExportIDs() = %v", ids)
+ }
+
+ dup, err := ParseExportIDs([]string{"1001", "1001,1002"})
+ if err != nil {
+ t.Fatalf("ParseExportIDs() dup err = %v", err)
+ }
+ if len(dup) != 2 {
+ t.Fatalf("ParseExportIDs() dup = %v, want 2 unique ids", dup)
+ }
+
+ if _, err := ParseExportIDs([]string{"abc"}); err == nil {
+ t.Fatal("ParseExportIDs() invalid id should fail")
+ }
+}
+
+func TestParseKeywords(t *testing.T) {
+ t.Parallel()
+
+ got := parseKeywords("a,b, c;d")
+ want := []string{"a", "b", "c", "d"}
+ if len(got) != len(want) {
+ t.Fatalf("parseKeywords() = %v, want %v", got, want)
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("parseKeywords()[%d] = %q, want %q", i, got[i], want[i])
+ }
+ }
+}
+
+func TestMapHeaderColumns(t *testing.T) {
+ t.Parallel()
+
+ index := mapHeaderColumns([]string{"ID", "标题", "描述", "关键词"})
+ for i := 0; i < excelHeaders; i++ {
+ if _, ok := index[i]; !ok {
+ t.Fatalf("missing column index %d", i)
+ }
+ }
+}
+
+func TestParseWorkbook(t *testing.T) {
+ t.Parallel()
+
+ rows := []ContentRow{{
+ ID: 1001,
+ Title: "标题A",
+ Desc: "描述A",
+ Keyworld: []string{"k1", "k2"},
+ }}
+ data, err := buildWorkbook(rows)
+ if err != nil {
+ t.Fatalf("buildWorkbook() err = %v", err)
+ }
+
+ parsed, err := parseWorkbook(data)
+ if err != nil {
+ t.Fatalf("parseWorkbook() err = %v", err)
+ }
+ if len(parsed) != 1 {
+ t.Fatalf("parseWorkbook() len = %d, want 1", len(parsed))
+ }
+ if parsed[0].ID != 1001 || parsed[0].Title != "标题A" {
+ t.Fatalf("parseWorkbook() = %+v", parsed[0])
+ }
+}
+
+func TestParseWorkbookSkipRedLineRemark(t *testing.T) {
+ t.Parallel()
+
+ f := excelize.NewFile()
+ defer f.Close()
+
+ if err := f.SetSheetRow(sheetName, "A1", &[]string{colID, colTitle, colDesc, colKeyworld, colRedLineRemark}); err != nil {
+ t.Fatalf("SetSheetRow header err = %v", err)
+ }
+ if err := f.SetSheetRow(sheetName, "A2", &[]any{1001, "标题A", "描述A", "k1", "建议下架"}); err != nil {
+ t.Fatalf("SetSheetRow row1 err = %v", err)
+ }
+ if err := f.SetSheetRow(sheetName, "A3", &[]any{1002, "标题B", "描述B", "k2", ""}); err != nil {
+ t.Fatalf("SetSheetRow row2 err = %v", err)
+ }
+
+ var buf bytes.Buffer
+ if err := f.Write(&buf); err != nil {
+ t.Fatalf("Write() err = %v", err)
+ }
+
+ parsed, err := parseWorkbook(buf.Bytes())
+ if err != nil {
+ t.Fatalf("parseWorkbook() err = %v", err)
+ }
+ if len(parsed) != 1 {
+ t.Fatalf("parseWorkbook() len = %d, want 1", len(parsed))
+ }
+ if parsed[0].ID != 1002 {
+ t.Fatalf("parseWorkbook() ID = %d, want 1002", parsed[0].ID)
+ }
+}
+
+func TestRandomImportAddedTime(t *testing.T) {
+ t.Parallel()
+
+ r := rand.New(rand.NewSource(1))
+ for i := 0; i < 20; i++ {
+ got := randomImportAddedTime(r)
+ if got.Before(importAddedTimeStart) {
+ t.Fatalf("randomImportAddedTime() = %v, before %v", got, importAddedTimeStart)
+ }
+ if got.After(time.Now()) {
+ t.Fatalf("randomImportAddedTime() = %v, after now", got)
+ }
+ }
+}
+
+func TestNormalizeContentType(t *testing.T) {
+ t.Parallel()
+
+ if _, err := normalizeContentType("video"); err != nil {
+ t.Fatalf("video should be valid: %v", err)
+ }
+ if _, err := normalizeContentType("post"); err == nil {
+ t.Fatal("post should be invalid")
+ }
+}
diff --git a/web/service/srv_contenttag/tag.go b/web/service/srv_contenttag/tag.go
new file mode 100644
index 0000000..ba2713f
--- /dev/null
+++ b/web/service/srv_contenttag/tag.go
@@ -0,0 +1,592 @@
+package srv_contenttag
+
+import (
+ "context"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/pubdef"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultContentTagPageSize int64 = 10
+ maxContentTagPageSize int64 = 100
+ maxContentTagOptionSize int64 = 200
+)
+
+var contentTagWebProjection = bson.D{
+ {Key: contenttagmod.FN.ContentTag.Id, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.Name, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.Slug, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.Types, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.Status, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.Rank, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.VideoCount, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.PlayletCount, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.SeoInfo.Title, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.SeoInfo.Description, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.SeoInfo.Keyworld, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.CreatedById, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.CreatedByName, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.UpdatedByName, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.CreatedAt, Value: 1},
+ {Key: contenttagmod.FN.ContentTag.UpdatedAt, Value: 1},
+}
+
+// ContentTagBindChange 保存一次内容标签变更前后的标签ID集合
+type ContentTagBindChange struct {
+ OldTagIds []int64
+ NewTagIds []int64
+}
+
+// ListContentTag 分页查询统一标签列表
+func ListContentTag(ctx context.Context, req *proto.ContentTagListReq) (*proto.ContentTagListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[contenttagmod.FN.ContentTag.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[contenttagmod.FN.ContentTag.Status] = *req.Status
+ }
+ if req.Type != nil {
+ if !isValidContentTagType(*req.Type) {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ filter[contenttagmod.FN.ContentTag.Types] = *req.Type
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[contenttagmod.FN.ContentTag.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ pageNum, pageSize := normalizeContentTagPage(req.PageNum, req.PageSize)
+ totalCount, err := contenttagmod.CountContentTag(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.ContentTagListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.ContentTagListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
+ SetProjection(contentTagWebProjection).
+ SetSort(bson.D{
+ {Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
+ {Key: contenttagmod.FN.ContentTag.UpdatedAt, Value: -1},
+ {Key: contenttagmod.FN.ContentTag.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.ContentTagListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, contentTagListItem(item))
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddContentTag 新增统一标签
+func AddContentTag(ctx context.Context, req *proto.ContentTagAddReq, operatorId int64) (*proto.ContentTagAddResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ name := strings.TrimSpace(req.Name)
+ if name == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
+ }
+ if req.Status == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "状态不能为空", "")
+ }
+ types, err := normalizeContentTagTypes(req.Types)
+ if err != nil {
+ return nil, err
+ }
+ operatorId, operatorName := resolveContentTagOperator(ctx, operatorId)
+ doc := &contenttagmod.ContentTag{
+ Name: name,
+ Slug: req.Slug,
+ Types: types,
+ Status: *req.Status,
+ Rank: req.Rank,
+ VideoCount: 0,
+ PlayletCount: 0,
+ SeoInfo: normalizeContentTagSeoValue(req.SeoInfo),
+ CreatedById: operatorId,
+ CreatedByName: operatorName,
+ UpdatedByName: operatorName,
+ }
+ exists := func(s string) bool {
+ item, _ := contenttagmod.FindOneContentTag(ctx, slugx.Filter(
+ contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.Id, s, 0))
+ return item != nil
+ }
+ if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, name), exists); serr != nil {
+ return nil, serr
+ }
+ if err := contenttagmod.InsertOneContentTag(ctx, doc); err != nil {
+ return nil, err
+ }
+ return &proto.ContentTagAddResp{Id: doc.Id}, nil
+}
+
+// UpdateContentTag 更新统一标签
+func UpdateContentTag(ctx context.Context, req *proto.ContentTagUpdateReq, operatorId int64) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
+ }
+ current, err := contenttagmod.FindOneContentTag(ctx, bson.M{
+ contenttagmod.FN.ContentTag.Id: req.Id,
+ }, options.FindOne().SetProjection(contentTagWebProjection))
+ if err != nil {
+ return err
+ }
+ nextTypes := append([]contenttagmod.ContentTagType{}, current.Types...)
+ nextVideoCount := current.VideoCount
+ nextPlayletCount := current.PlayletCount
+ setDoc := bson.M{contenttagmod.FN.ContentTag.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
+ }
+ setDoc[contenttagmod.FN.ContentTag.Name] = name
+ }
+ if req.Types != nil {
+ types, err := normalizeContentTagTypes(*req.Types)
+ if err != nil {
+ return err
+ }
+ nextTypes = types
+ setDoc[contenttagmod.FN.ContentTag.Types] = types
+ }
+ if req.Status != nil {
+ setDoc[contenttagmod.FN.ContentTag.Status] = *req.Status
+ }
+ if req.Rank != nil {
+ setDoc[contenttagmod.FN.ContentTag.Rank] = *req.Rank
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeContentTagSeoValue(*req.SeoInfo)
+ }
+ name := current.Name
+ if req.Name != nil {
+ name = strings.TrimSpace(*req.Name)
+ }
+ keywords := current.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeContentTagSeoValue(*req.SeoInfo).Keyworld
+ }
+ exists := func(s string) bool {
+ item, _ := contenttagmod.FindOneContentTag(ctx, slugx.Filter(
+ contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.Id, s, req.Id))
+ return item != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ setDoc[contenttagmod.FN.ContentTag.Slug] = slug
+ }
+ if err := validateContentTagCounts(nextTypes, nextVideoCount, nextPlayletCount); err != nil {
+ return err
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ _, operatorName := resolveContentTagOperator(ctx, operatorId)
+ setDoc[contenttagmod.FN.ContentTag.UpdatedByName] = operatorName
+ return contenttagmod.UpdateOneContentTag(ctx,
+ bson.M{contenttagmod.FN.ContentTag.Id: req.Id},
+ bson.M{"$set": setDoc},
+ )
+}
+
+// DelContentTag 删除统一标签,存在任一类型绑定数量时禁止删除
+func DelContentTag(ctx context.Context, req *proto.ContentTagDelReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
+ }
+ tag, err := contenttagmod.FindOneContentTag(ctx, bson.M{
+ contenttagmod.FN.ContentTag.Id: req.Id,
+ }, options.FindOne().SetProjection(contentTagWebProjection))
+ if err != nil {
+ return err
+ }
+ if tag.VideoCount > 0 || tag.PlayletCount > 0 {
+ return srvdef.New(srvdef.ErrParamError, "标签已被内容绑定,无法删除", "")
+ }
+ count, err := contenttagmod.DelOneContentTag(ctx, bson.M{contenttagmod.FN.ContentTag.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// ListContentTagOption 查询启用的统一标签下拉列表,传type时按支持类型过滤
+func ListContentTagOption(ctx context.Context, req *proto.ContentTagOptionReq) (*proto.ContentTagOptionResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{
+ contenttagmod.FN.ContentTag.Status: true,
+ }
+ if req.Type != nil {
+ if !isValidContentTagType(*req.Type) {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ filter[contenttagmod.FN.ContentTag.Types] = *req.Type
+ }
+ list, err := contenttagmod.FindManyContentTag(ctx, filter, options.Find().
+ SetProjection(contentTagWebProjection).
+ SetSort(bson.D{
+ {Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
+ {Key: contenttagmod.FN.ContentTag.Id, Value: -1},
+ }).
+ SetLimit(maxContentTagOptionSize))
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.ContentTagOptionResp{List: make([]*proto.ContentTagOptionItem, 0, len(list))}
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ resp.List = append(resp.List, &proto.ContentTagOptionItem{
+ Id: item.Id,
+ Name: item.Name,
+ Types: append([]contenttagmod.ContentTagType{}, item.Types...),
+ TypeNames: contentTagTypeNames(item.Types),
+ Rank: item.Rank,
+ BindCount: contentTagOptionBindCount(item, req.Type),
+ })
+ }
+ return resp, nil
+}
+
+// NormalizeContentTagIds 校验标签ID并去重,保留原有顺序便于内容详情按后台提交顺序展示
+func NormalizeContentTagIds(tagIds []int64) ([]int64, *srvdef.SrvErr) {
+ result := normalizeContentTagIdsForDiff(tagIds)
+ if len(result) != len(tagIds) {
+ for _, tagId := range tagIds {
+ if tagId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
+ }
+ }
+ }
+ return result, nil
+}
+
+// ValidateContentTags 校验统一标签存在、启用,并且支持当前内容类型
+func ValidateContentTags(ctx context.Context, tagIds []int64, tagType contenttagmod.ContentTagType) *srvdef.SrvErr {
+ ids, err := NormalizeContentTagIds(tagIds)
+ if err != nil {
+ return err
+ }
+ if len(ids) == 0 {
+ return nil
+ }
+ if !isValidContentTagType(tagType) {
+ return srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ count, err := contenttagmod.CountContentTag(ctx, bson.M{
+ contenttagmod.FN.ContentTag.Id: bson.M{"$in": ids},
+ contenttagmod.FN.ContentTag.Status: true,
+ contenttagmod.FN.ContentTag.Types: tagType,
+ })
+ if err != nil {
+ return err
+ }
+ if count != int64(len(ids)) {
+ return srvdef.New(srvdef.ErrParamError, "标签不存在、未启用或不支持当前内容类型", "")
+ }
+ return nil
+}
+
+// SyncContentTagBindCount 按绑定差异增减统一标签对应类型的绑定数量
+func SyncContentTagBindCount(ctx context.Context, oldTagIds, newTagIds []int64, tagType contenttagmod.ContentTagType) *srvdef.SrvErr {
+ return SyncContentTagBindCountBatch(ctx, []ContentTagBindChange{
+ {OldTagIds: oldTagIds, NewTagIds: newTagIds},
+ }, tagType)
+}
+
+// SyncContentTagBindCountBatch 聚合多条内容标签变更后批量增减统一标签对应类型的绑定数量
+func SyncContentTagBindCountBatch(ctx context.Context, changes []ContentTagBindChange, tagType contenttagmod.ContentTagType) *srvdef.SrvErr {
+ if !isValidContentTagType(tagType) {
+ return srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ field, err := contentTagCountField(tagType)
+ if err != nil {
+ return err
+ }
+ deltaMap := make(map[int64]int64)
+ for _, change := range changes {
+ addIds, removeIds := diffContentTagIds(
+ normalizeContentTagIdsForDiff(change.OldTagIds),
+ normalizeContentTagIdsForDiff(change.NewTagIds),
+ )
+ for _, tagId := range addIds {
+ deltaMap[tagId]++
+ }
+ for _, tagId := range removeIds {
+ deltaMap[tagId]--
+ }
+ }
+ if len(deltaMap) == 0 {
+ return nil
+ }
+ now := time.Now()
+ docs := make([]mongo.WriteModel, 0, len(deltaMap))
+ for tagId, delta := range deltaMap {
+ if delta == 0 {
+ continue
+ }
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{contenttagmod.FN.ContentTag.Id: tagId}).
+ SetUpdate(bson.A{
+ bson.M{"$set": bson.M{
+ field: bson.M{"$max": bson.A{
+ 0,
+ bson.M{"$add": bson.A{
+ bson.M{"$ifNull": bson.A{"$" + field, 0}},
+ delta,
+ }},
+ }},
+ contenttagmod.FN.ContentTag.UpdatedAt: now,
+ }},
+ }))
+ }
+ if len(docs) == 0 {
+ return nil
+ }
+ _, err = contenttagmod.BulkWriteContentTag(ctx, docs, options.BulkWrite().SetOrdered(false))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// normalizeContentTagPage 规范分页参数,限制单页数量避免大分页拖慢查询
+func normalizeContentTagPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultContentTagPageSize
+ }
+ if pageSize > maxContentTagPageSize {
+ pageSize = maxContentTagPageSize
+ }
+ return pageNum, pageSize
+}
+
+func normalizeContentTagIdsForDiff(tagIds []int64) []int64 {
+ result := make([]int64, 0, len(tagIds))
+ seen := make(map[int64]struct{}, len(tagIds))
+ for _, tagId := range tagIds {
+ if tagId <= 0 {
+ continue
+ }
+ if _, ok := seen[tagId]; ok {
+ continue
+ }
+ seen[tagId] = struct{}{}
+ result = append(result, tagId)
+ }
+ return result
+}
+
+func diffContentTagIds(oldTagIds, newTagIds []int64) ([]int64, []int64) {
+ oldMap := make(map[int64]struct{}, len(oldTagIds))
+ newMap := make(map[int64]struct{}, len(newTagIds))
+ for _, tagId := range oldTagIds {
+ oldMap[tagId] = struct{}{}
+ }
+ for _, tagId := range newTagIds {
+ newMap[tagId] = struct{}{}
+ }
+ addIds := make([]int64, 0)
+ removeIds := make([]int64, 0)
+ for _, tagId := range newTagIds {
+ if _, ok := oldMap[tagId]; !ok {
+ addIds = append(addIds, tagId)
+ }
+ }
+ for _, tagId := range oldTagIds {
+ if _, ok := newMap[tagId]; !ok {
+ removeIds = append(removeIds, tagId)
+ }
+ }
+ return addIds, removeIds
+}
+
+func contentTagCountField(tagType contenttagmod.ContentTagType) (string, *srvdef.SrvErr) {
+ switch tagType {
+ case contenttagmod.ContentTagTypeVideo:
+ return contenttagmod.FN.ContentTag.VideoCount, nil
+ case contenttagmod.ContentTagTypePlaylet:
+ return contenttagmod.FN.ContentTag.PlayletCount, nil
+ default:
+ return "", srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+}
+
+func contentTagBindCount(item *contenttagmod.ContentTag, tagType contenttagmod.ContentTagType) int64 {
+ switch tagType {
+ case contenttagmod.ContentTagTypeVideo:
+ return item.VideoCount
+ case contenttagmod.ContentTagTypePlaylet:
+ return item.PlayletCount
+ default:
+ return 0
+ }
+}
+
+func contentTagOptionBindCount(item *contenttagmod.ContentTag, tagType *contenttagmod.ContentTagType) int64 {
+ if tagType == nil {
+ return 0
+ }
+ return contentTagBindCount(item, *tagType)
+}
+
+// normalizeContentTagTypes 去重并校验统一标签支持的内容类型
+func normalizeContentTagTypes(types []contenttagmod.ContentTagType) ([]contenttagmod.ContentTagType, *srvdef.SrvErr) {
+ if len(types) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签类型不能为空", "")
+ }
+ seen := make(map[contenttagmod.ContentTagType]struct{}, len(types))
+ result := make([]contenttagmod.ContentTagType, 0, len(types))
+ for _, item := range types {
+ if !isValidContentTagType(item) {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
+ }
+ if _, ok := seen[item]; ok {
+ continue
+ }
+ seen[item] = struct{}{}
+ result = append(result, item)
+ }
+ sort.Slice(result, func(i, j int) bool {
+ return result[i] < result[j]
+ })
+ return result, nil
+}
+
+// isValidContentTagType 判断统一标签类型是否为视频、短剧之一
+func isValidContentTagType(tagType contenttagmod.ContentTagType) bool {
+ return tagType == contenttagmod.ContentTagTypeVideo ||
+ tagType == contenttagmod.ContentTagTypePlaylet
+}
+
+// validateContentTagCounts 校验各类型绑定数量非负且数量字段和支持类型保持一致
+func validateContentTagCounts(types []contenttagmod.ContentTagType, videoCount, playletCount int64) *srvdef.SrvErr {
+ if videoCount < 0 || playletCount < 0 {
+ return srvdef.New(srvdef.ErrParamError, "绑定数量不能小于0", "")
+ }
+ typeMap := make(map[contenttagmod.ContentTagType]struct{}, len(types))
+ for _, item := range types {
+ typeMap[item] = struct{}{}
+ }
+ if videoCount > 0 && !hasContentTagType(typeMap, contenttagmod.ContentTagTypeVideo) {
+ return srvdef.New(srvdef.ErrParamError, "存在视频绑定数量时必须支持视频类型", "")
+ }
+ if playletCount > 0 && !hasContentTagType(typeMap, contenttagmod.ContentTagTypePlaylet) {
+ return srvdef.New(srvdef.ErrParamError, "存在短剧绑定数量时必须支持短剧类型", "")
+ }
+ return nil
+}
+
+func hasContentTagType(typeMap map[contenttagmod.ContentTagType]struct{}, tagType contenttagmod.ContentTagType) bool {
+ _, ok := typeMap[tagType]
+ return ok
+}
+
+// contentTagListItem 将统一标签模型转换为后台列表响应
+func contentTagListItem(item *contenttagmod.ContentTag) *proto.ContentTagListResp {
+ return &proto.ContentTagListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Types: append([]contenttagmod.ContentTagType{}, item.Types...),
+ TypeNames: contentTagTypeNames(item.Types),
+ Status: item.Status,
+ Rank: item.Rank,
+ VideoCount: item.VideoCount,
+ PlayletCount: item.PlayletCount,
+ SeoInfo: normalizeContentTagSeoValue(item.SeoInfo),
+ CreatedById: item.CreatedById,
+ CreatedByName: item.CreatedByName,
+ UpdatedByName: item.UpdatedByName,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+// contentTagTypeNames 将统一标签类型转换为后台展示名称
+func contentTagTypeNames(types []contenttagmod.ContentTagType) []string {
+ names := make([]string, 0, len(types))
+ for _, item := range types {
+ switch item {
+ case contenttagmod.ContentTagTypeVideo:
+ names = append(names, "视频")
+ case contenttagmod.ContentTagTypePlaylet:
+ names = append(names, "短剧")
+ }
+ }
+ return names
+}
+
+// normalizeContentTagSeoValue 规整 SEO 字段,避免空切片返回 null
+func normalizeContentTagSeoValue(seo pubdef.SeoValue) pubdef.SeoValue {
+ if seo.Keyworld == nil {
+ seo.Keyworld = make([]string, 0)
+ }
+ return seo
+}
+
+// resolveContentTagOperator 从后台账号表解析当前操作人名称
+func resolveContentTagOperator(ctx context.Context, operatorId int64) (int64, string) {
+ if operatorId <= 0 {
+ return 0, ""
+ }
+ acc, err := mwacc.FindOneAccByFilter(ctx, bson.M{
+ mwacc.FN.MwAcc.Id: operatorId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: mwacc.FN.MwAcc.Id, Value: 1},
+ {Key: mwacc.FN.MwAcc.UserName, Value: 1},
+ }))
+ if err != nil || acc == nil {
+ return operatorId, ""
+ }
+ return acc.Id, acc.UserName
+}
diff --git a/web/service/srv_detailnav/define.go b/web/service/srv_detailnav/define.go
new file mode 100644
index 0000000..3928dca
--- /dev/null
+++ b/web/service/srv_detailnav/define.go
@@ -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"`
+}
diff --git a/web/service/srv_detailnav/detailnav.go b/web/service/srv_detailnav/detailnav.go
new file mode 100644
index 0000000..5d5c111
--- /dev/null
+++ b/web/service/srv_detailnav/detailnav.go
@@ -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
+}
diff --git a/web/service/srv_homebanner/banner.go b/web/service/srv_homebanner/banner.go
new file mode 100644
index 0000000..a80fb0e
--- /dev/null
+++ b/web/service/srv_homebanner/banner.go
@@ -0,0 +1,226 @@
+package srv_homebanner
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/homebannermod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultHomeBannerPageSize int64 = 10
+ maxHomeBannerPageSize int64 = 100
+)
+
+var homeBannerWebProjection = bson.D{
+ {Key: homebannermod.FN.HomeBanner.Id, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Name, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Image, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.VerticalImage, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Url, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Rank, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Status, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.CreatedById, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.CreatedByName, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.UpdatedByName, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.CreatedAt, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.UpdatedAt, Value: 1},
+}
+
+// ListHomeBanner 分页查询首页 banner 列表
+func ListHomeBanner(ctx context.Context, req *proto.HomeBannerListReq) (*proto.HomeBannerListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[homebannermod.FN.HomeBanner.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[homebannermod.FN.HomeBanner.Status] = *req.Status
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[homebannermod.FN.HomeBanner.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ pageNum, pageSize := normalizeHomeBannerPage(req.PageNum, req.PageSize)
+ totalCount, err := homebannermod.CountHomeBanner(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.HomeBannerListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.HomeBannerListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := homebannermod.FindManyHomeBanner(ctx, filter, options.Find().
+ SetProjection(homeBannerWebProjection).
+ SetSort(bson.D{
+ {Key: homebannermod.FN.HomeBanner.Rank, Value: -1},
+ {Key: homebannermod.FN.HomeBanner.UpdatedAt, Value: -1},
+ {Key: homebannermod.FN.HomeBanner.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.HomeBannerListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.HomeBannerListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Image: item.Image,
+ VerticalImage: item.VerticalImage,
+ Url: item.Url,
+ Rank: item.Rank,
+ Status: item.Status,
+ CreatedById: item.CreatedById,
+ CreatedByName: item.CreatedByName,
+ UpdatedByName: item.UpdatedByName,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddHomeBanner 新增首页 banner
+func AddHomeBanner(ctx context.Context, req *proto.HomeBannerAddReq, operatorId int64) (*proto.HomeBannerAddResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ name := strings.TrimSpace(req.Name)
+ image := strings.TrimSpace(req.Image)
+ if name == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
+ }
+ if image == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "图片不能为空", "")
+ }
+ if req.Status == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "状态不能为空", "")
+ }
+ operatorId, operatorName := resolveHomeBannerOperator(ctx, operatorId)
+ doc := &homebannermod.HomeBanner{
+ Name: name,
+ Image: image,
+ VerticalImage: strings.TrimSpace(req.VerticalImage),
+ Url: strings.TrimSpace(req.Url),
+ Rank: req.Rank,
+ Status: *req.Status,
+ CreatedById: operatorId,
+ CreatedByName: operatorName,
+ UpdatedByName: operatorName,
+ }
+ if err := homebannermod.InsertOneHomeBanner(ctx, doc); err != nil {
+ return nil, err
+ }
+ return &proto.HomeBannerAddResp{Id: doc.Id}, nil
+}
+
+// UpdateHomeBanner 更新首页 banner
+func UpdateHomeBanner(ctx context.Context, req *proto.HomeBannerUpdateReq, operatorId int64) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "首页banner ID无效", "")
+ }
+ setDoc := bson.M{homebannermod.FN.HomeBanner.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
+ }
+ setDoc[homebannermod.FN.HomeBanner.Name] = name
+ }
+ if req.Image != nil {
+ image := strings.TrimSpace(*req.Image)
+ if image == "" {
+ return srvdef.New(srvdef.ErrParamError, "图片不能为空", "")
+ }
+ setDoc[homebannermod.FN.HomeBanner.Image] = image
+ }
+ if req.VerticalImage != nil {
+ setDoc[homebannermod.FN.HomeBanner.VerticalImage] = strings.TrimSpace(*req.VerticalImage)
+ }
+ if req.Url != nil {
+ setDoc[homebannermod.FN.HomeBanner.Url] = strings.TrimSpace(*req.Url)
+ }
+ if req.Rank != nil {
+ setDoc[homebannermod.FN.HomeBanner.Rank] = *req.Rank
+ }
+ if req.Status != nil {
+ setDoc[homebannermod.FN.HomeBanner.Status] = *req.Status
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ _, operatorName := resolveHomeBannerOperator(ctx, operatorId)
+ setDoc[homebannermod.FN.HomeBanner.UpdatedByName] = operatorName
+ return homebannermod.UpdateOneHomeBanner(ctx,
+ bson.M{homebannermod.FN.HomeBanner.Id: req.Id},
+ bson.M{"$set": setDoc},
+ )
+}
+
+// DelHomeBanner 删除首页 banner
+func DelHomeBanner(ctx context.Context, req *proto.HomeBannerDelReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "首页banner ID无效", "")
+ }
+ count, err := homebannermod.DelOneHomeBanner(ctx, bson.M{homebannermod.FN.HomeBanner.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// normalizeHomeBannerPage 规范分页参数,限制单页数量避免大分页拖慢查询
+func normalizeHomeBannerPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultHomeBannerPageSize
+ }
+ if pageSize > maxHomeBannerPageSize {
+ pageSize = maxHomeBannerPageSize
+ }
+ return pageNum, pageSize
+}
+
+// resolveHomeBannerOperator 从后台账号表解析当前操作人名称
+func resolveHomeBannerOperator(ctx context.Context, operatorId int64) (int64, string) {
+ if operatorId <= 0 {
+ return 0, ""
+ }
+ acc, err := mwacc.FindOneAccByFilter(ctx, bson.M{
+ mwacc.FN.MwAcc.Id: operatorId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: mwacc.FN.MwAcc.Id, Value: 1},
+ {Key: mwacc.FN.MwAcc.UserName, Value: 1},
+ }))
+ if err != nil || acc == nil {
+ return operatorId, ""
+ }
+ return acc.Id, acc.UserName
+}
diff --git a/web/service/srv_hotnav/define.go b/web/service/srv_hotnav/define.go
new file mode 100644
index 0000000..d765000
--- /dev/null
+++ b/web/service/srv_hotnav/define.go
@@ -0,0 +1,42 @@
+package srv_hotnav
+
+import (
+ "videoweb/pub/models/hotnavmod"
+ "videoweb/pub/pubdef"
+)
+
+type GetHotNavListReq struct {
+ pubdef.Page
+}
+
+type GetHotNavListResp struct {
+ Total int64 `json:"total"`
+ List hotnavmod.HotNavList `json:"list"`
+}
+
+type AddHotNavReq struct {
+ Title string `json:"title" binding:"required"` // 标题
+ Intro string `json:"intro"` // 介绍
+ Url string `json:"url" binding:"required"` // 链接
+ Icon string `json:"icon"` // 图标
+ Sort int64 `json:"sort"` // 排序
+ Status *bool `json:"status"` // 是否启用
+}
+
+type UpdateHotNavReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param UpdateHotNavParam `json:"param"`
+}
+
+type UpdateHotNavParam struct {
+ Title *string `json:"title"`
+ Intro *string `json:"intro"`
+ Url *string `json:"url"`
+ Icon *string `json:"icon"`
+ Sort *int64 `json:"sort"`
+ Status *bool `json:"status"`
+}
+
+type DelHotNavReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_hotnav/hotnav.go b/web/service/srv_hotnav/hotnav.go
new file mode 100644
index 0000000..4a48135
--- /dev/null
+++ b/web/service/srv_hotnav/hotnav.go
@@ -0,0 +1,100 @@
+package srv_hotnav
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/hotnavmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func GetHotNavList(ctx context.Context, req *GetHotNavListReq) (*GetHotNavListResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ count, serr := hotnavmod.CountHotNav(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ if count == 0 {
+ return &GetHotNavListResp{
+ Total: 0,
+ List: make(hotnavmod.HotNavList, 0),
+ }, nil
+ }
+
+ opts := options.FindOptions{}
+ opts.SetSort(bson.D{
+ {Key: hotnavmod.FN.HotNav.Sort, Value: -1},
+ {Key: hotnavmod.FN.HotNav.CreatedAt, Value: -1},
+ })
+ opts.SetLimit(req.PageSize)
+ opts.SetSkip((req.PageNum - 1) * req.PageSize)
+ list, serr := hotnavmod.FindManyHotNav(ctx, filter, &opts)
+ if serr != nil {
+ return nil, serr
+ }
+ return &GetHotNavListResp{
+ Total: count,
+ List: list,
+ }, nil
+}
+
+func AddHotNav(ctx context.Context, req *AddHotNavReq) *srvdef.SrvErr {
+ now := time.Now()
+ status := true
+ if req.Status != nil {
+ status = *req.Status
+ }
+ doc := &hotnavmod.HotNav{
+ Id: mongos.Lid.HotNav.Get(),
+ Title: req.Title,
+ Intro: req.Intro,
+ Url: req.Url,
+ Icon: req.Icon,
+ Sort: req.Sort,
+ Status: status,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ return hotnavmod.InsertOneHotNav(ctx, doc)
+}
+
+func UpdateHotNav(ctx context.Context, req *UpdateHotNavReq) *srvdef.SrvErr {
+ filter := bson.M{hotnavmod.FN.HotNav.Id: req.Id}
+ updateFields := bson.M{
+ hotnavmod.FN.HotNav.UpdatedAt: time.Now(),
+ }
+ if req.Param.Title != nil {
+ updateFields[hotnavmod.FN.HotNav.Title] = *req.Param.Title
+ }
+ if req.Param.Intro != nil {
+ updateFields[hotnavmod.FN.HotNav.Intro] = *req.Param.Intro
+ }
+ if req.Param.Url != nil {
+ updateFields[hotnavmod.FN.HotNav.Url] = *req.Param.Url
+ }
+ if req.Param.Icon != nil {
+ updateFields[hotnavmod.FN.HotNav.Icon] = *req.Param.Icon
+ }
+ if req.Param.Sort != nil {
+ updateFields[hotnavmod.FN.HotNav.Sort] = *req.Param.Sort
+ }
+ if req.Param.Status != nil {
+ updateFields[hotnavmod.FN.HotNav.Status] = *req.Param.Status
+ }
+ return hotnavmod.UpdateOneHotNav(ctx, filter, bson.M{"$set": updateFields})
+}
+
+func DelHotNav(ctx context.Context, req *DelHotNavReq) *srvdef.SrvErr {
+ filter := bson.M{hotnavmod.FN.HotNav.Id: req.Id}
+ count, serr := hotnavmod.DelOneHotNav(ctx, filter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/web/service/srv_ipblack/define.go b/web/service/srv_ipblack/define.go
new file mode 100644
index 0000000..9da1d67
--- /dev/null
+++ b/web/service/srv_ipblack/define.go
@@ -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"`
+}
\ No newline at end of file
diff --git a/web/service/srv_ipblack/ipblack.go b/web/service/srv_ipblack/ipblack.go
new file mode 100644
index 0000000..253b338
--- /dev/null
+++ b/web/service/srv_ipblack/ipblack.go
@@ -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
+}
\ No newline at end of file
diff --git a/web/service/srv_mediaresource/ai_content_backflow.go b/web/service/srv_mediaresource/ai_content_backflow.go
new file mode 100644
index 0000000..3d3fcc5
--- /dev/null
+++ b/web/service/srv_mediaresource/ai_content_backflow.go
@@ -0,0 +1,1068 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/pubdef"
+
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ aidatacenter "videoweb/pub/data_center"
+ "videoweb/pub/models/playletbackflowstatmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/web/webg"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ // defaultAIContentBackflowPageSize 每页扫描原创短剧数量,避免一次性加载过多 Mongo 数据。
+ defaultAIContentBackflowPageSize = int64(200)
+ // defaultAIContentBackflowBatchSize 每批提交给内容中台的记录数。
+ defaultAIContentBackflowBatchSize = int64(500)
+ // maxAIContentBackflowBatchSize 内容中台单批最多允许 1000 条。
+ maxAIContentBackflowBatchSize = int64(1000)
+ // defaultAIContentBackflowReportTimeout 单批同步上报的最长等待时间。
+ defaultAIContentBackflowReportTimeout = 60 * time.Second
+)
+
+var (
+ // aiContentBackflowPlatformBaseURL 覆盖短剧播放页域名;App-only 产品为空时 platform_url 回退到 video_url。
+ aiContentBackflowPlatformBaseURL = ""
+ // aiContentBackflowVideoBaseURL 覆盖视频回源域名;空则使用 dataCenterBackflow.selfSourceBaseUrl。
+ aiContentBackflowVideoBaseURL = ""
+ // aiContentBackflowPageSize 运行时扫描分页大小,非法值回退默认值。
+ aiContentBackflowPageSize = int64(200)
+ // aiContentBackflowBatchSize 运行时上报批量大小,非法值回退默认值。
+ aiContentBackflowBatchSize = int64(500)
+ // aiContentBackflowCronSpec 运行时 cron 表达式,空值回退每天 02:30。
+ aiContentBackflowCronSpec = ""
+)
+
+// aiContentBackflowStats 记录一次任务的扫描、构造和跳过数量。
+type aiContentBackflowStats struct {
+ DataDate string // 数据日期,格式 DD/MM/YYYY
+ Scanned int64 // 扫描到的原创短剧数
+ Built int64 // 成功构造的回流记录数
+ Stored int64 // 已写入本地统计表的记录数
+ Sent int64 // 已发送给内容中台的记录数
+ Batches int64 // 发送批次数
+ Reported int64 // 已标记上报成功的记录数
+ Failed int64 // 已标记上报失败的记录数
+ Skipped int64 // 因数据缺口跳过的短剧数
+ MissingChapter int64 // 缺少可用章节的数量
+ MissingVideo int64 // 缺少视频地址的数量
+ MissingMediaContent int64 // 第一集缺少媒资章节 ID 的数量
+ FuturePublish int64 // 上线日期晚于统计日期的数量
+}
+
+// aiContentBackflowChapters 一部短剧回流需要的章节:第一集,以及按集数最早的付费集。
+type aiContentBackflowChapters struct {
+ First *playletchaptermod.PlayletChapter
+ FirstPaid *playletchaptermod.PlayletChapter
+}
+
+// aiContentBackflowStore 抽象数据读写和发送函数,方便单测注入假实现。
+type aiContentBackflowStore struct {
+ findPlaylets func(context.Context, bson.M, *options.FindOptions) (playletmod.PlayletList, *srvdef.SrvErr)
+ findChapters func(context.Context, []int64) (map[int64]*aiContentBackflowChapters, *srvdef.SrvErr)
+}
+
+// aiContentBackflowClient 复用 SDK client,避免每次定时任务重复创建后台队列。
+var aiContentBackflowClient = struct {
+ sync.Mutex
+ client *aidatacenter.Client
+ apiKey string
+ endpoint string
+}{}
+
+// AIContentBackflowCronSpec 返回原创短剧数据回流的每日调度时间。
+func AIContentBackflowCronSpec() string {
+ if spec := strings.TrimSpace(aiContentBackflowCronSpec); spec != "" {
+ return spec
+ }
+ return "0 30 2 * * ?"
+}
+
+// AIContentBackflowTodayStatCronSpec 返回当天统计表小时级刷新时间。
+func AIContentBackflowTodayStatCronSpec() string {
+ return "0 0 * * * ?"
+}
+
+// ReportAIContentBackflowYesterday 上报昨日原创短剧数据。
+//
+// 内容中台日记录按自然日覆盖更新,重复执行同一天不会累加。
+func ReportAIContentBackflowYesterday() {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ targetDate := time.Now().In(loc).AddDate(0, 0, -1)
+ stats, serr := reportAIContentBackflow(context.Background(), targetDate)
+ if serr != nil {
+ log.Error("AI 内容中台原创短剧数据回流失败", log.Any("err", serr), log.Any("stats", stats))
+ return
+ }
+ if stats != nil {
+ log.Info("AI 内容中台原创短剧数据回流完成", log.Any("stats", stats))
+ }
+}
+
+// RefreshAIContentBackflowTodayStats 刷新当天原创短剧回流统计,便于上线后观察数据。
+func RefreshAIContentBackflowTodayStats() {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ targetDate := time.Now().In(loc)
+ stats, serr := refreshAIContentBackflowStats(context.Background(), targetDate)
+ if serr != nil {
+ log.Error("AI 内容中台原创短剧当天统计刷新失败", log.Any("err", serr), log.Any("stats", stats))
+ return
+ }
+ log.Info("AI 内容中台原创短剧当天统计刷新完成", log.Any("stats", stats))
+}
+
+// CloseAIContentBackflowClient 关闭复用的内容中台 SDK client。
+func CloseAIContentBackflowClient() {
+ aiContentBackflowClient.Lock()
+ client := aiContentBackflowClient.client
+ aiContentBackflowClient.client = nil
+ aiContentBackflowClient.apiKey = ""
+ aiContentBackflowClient.endpoint = ""
+ aiContentBackflowClient.Unlock()
+ if client != nil {
+ client.Close()
+ }
+}
+
+// reportAIContentBackflow 从数据回流配置读取 API Key;空 key 表示未启用真实回流。
+func reportAIContentBackflow(ctx context.Context, targetDate time.Time) (*aiContentBackflowStats, *srvdef.SrvErr) {
+ apiKey := strings.TrimSpace(webg.Conf.DataCenterBackflow.Key)
+ if apiKey == "" {
+ log.Info("AI 内容中台原创短剧数据回流未启用",
+ log.Any("dataDate", formatAIContentBackflowDate(targetDate)),
+ log.Any("reason", "dataCenterBackflow.key empty"),
+ )
+ return nil, nil
+ }
+ stats, serr := ensureAIContentBackflowStats(ctx, targetDate)
+ if serr != nil {
+ return stats, serr
+ }
+ client, serr := getAIContentBackflowClient(apiKey)
+ if serr != nil {
+ return stats, serr
+ }
+ reportStats, serr := reportAIContentBackflowStats(ctx, targetDate, client)
+ if reportStats != nil {
+ mergeAIContentBackflowStats(stats, *reportStats)
+ }
+ return stats, serr
+}
+
+// refreshAIContentBackflowStats 计算指定日期数据并写入本地统计表,不依赖内容中台 API Key。
+func refreshAIContentBackflowStats(ctx context.Context, targetDate time.Time) (*aiContentBackflowStats, *srvdef.SrvErr) {
+ return refreshAIContentBackflowStatsWithStore(ctx, targetDate, aiContentBackflowStore{
+ findPlaylets: findOriginalPlayletsForBackflow,
+ findChapters: findFirstPlayletChaptersForBackflow,
+ })
+}
+
+// getAIContentBackflowClient 创建或复用 data_center SDK client。
+func getAIContentBackflowClient(apiKey string) (*aidatacenter.Client, *srvdef.SrvErr) {
+ if !strings.HasPrefix(apiKey, "ld_") {
+ return nil, srvdef.New(srvdef.Failure, "AI内容中台API Key格式错误", "AI内容中台API Key格式错误")
+ }
+ aiContentBackflowClient.Lock()
+ defer aiContentBackflowClient.Unlock()
+ if aiContentBackflowClient.client != nil && aiContentBackflowClient.apiKey == apiKey {
+ return aiContentBackflowClient.client, nil
+ }
+ if aiContentBackflowClient.client != nil {
+ aiContentBackflowClient.client.Close()
+ }
+ client := aidatacenter.New(apiKey)
+ endpoint := client.Endpoint
+ client.OnError = func(err error) {
+ log.Error("AI 内容中台原创短剧异步上报失败",
+ log.Any("endpoint", endpoint),
+ log.Any("companyPlatform", pubdef.CompanyPlatform),
+ log.E(err),
+ )
+ }
+ log.Info("AI 内容中台原创短剧数据回流客户端初始化",
+ log.Any("endpoint", endpoint),
+ log.Any("companyPlatform", pubdef.CompanyPlatform),
+ )
+ aiContentBackflowClient.client = client
+ aiContentBackflowClient.apiKey = apiKey
+ aiContentBackflowClient.endpoint = endpoint
+ return client, nil
+}
+
+// refreshAIContentBackflowStatsWithStore 分页扫描原创短剧并写入本地日统计表。
+func refreshAIContentBackflowStatsWithStore(ctx context.Context, targetDate time.Time, store aiContentBackflowStore) (*aiContentBackflowStats, *srvdef.SrvErr) {
+ if serr := validateAIContentBackflowRuntime(); serr != nil {
+ return nil, serr
+ }
+ pageSize := normalizeAIContentBackflowPageSize(aiContentBackflowPageSize)
+ stats := &aiContentBackflowStats{DataDate: formatAIContentBackflowDate(targetDate)}
+ log.Info("AI 内容中台原创短剧日统计刷新开始",
+ log.Any("dataDate", stats.DataDate),
+ log.Any("companyPlatform", pubdef.CompanyPlatform),
+ log.Any("pageSize", pageSize),
+ log.Any("videoBaseURL", resolveAIContentBackflowVideoBaseURL()),
+ )
+ for lastID := int64(0); ; {
+ list, serr := store.findPlaylets(ctx, originalPlayletBackflowFilter(lastID), originalPlayletBackflowFindOptions(pageSize))
+ if serr != nil {
+ return stats, serr
+ }
+ if len(list) == 0 {
+ break
+ }
+ stats.Scanned += int64(len(list))
+ docs, pageStats, nextID, serr := buildAIContentBackflowDailyStats(ctx, targetDate, list, store.findChapters)
+ mergeAIContentBackflowStats(stats, pageStats)
+ if serr != nil {
+ return stats, serr
+ }
+ if serr := playletbackflowstatmod.UpsertManyPlayletBackflowDailyStats(ctx, docs); serr != nil {
+ return stats, serr
+ }
+ stats.Stored += int64(len(docs))
+ log.Info("AI 内容中台原创短剧日统计分页刷新完成",
+ log.Any("dataDate", stats.DataDate),
+ log.Any("lastID", lastID),
+ log.Any("nextID", nextID),
+ log.Any("scanned", len(list)),
+ log.Any("built", pageStats.Built),
+ log.Any("stored", len(docs)),
+ log.Any("skipped", pageStats.Skipped),
+ log.Any("missingChapter", pageStats.MissingChapter),
+ log.Any("missingVideo", pageStats.MissingVideo),
+ log.Any("missingMediaContent", pageStats.MissingMediaContent),
+ log.Any("futurePublish", pageStats.FuturePublish),
+ )
+ if int64(len(list)) < pageSize || nextID <= lastID {
+ break
+ }
+ lastID = nextID
+ }
+ return stats, nil
+}
+
+// ensureAIContentBackflowStats 确保指定日期已有本地统计,避免每日上报重复扫描。
+func ensureAIContentBackflowStats(ctx context.Context, targetDate time.Time) (*aiContentBackflowStats, *srvdef.SrvErr) {
+ dataDate := formatAIContentBackflowDate(targetDate)
+ total, serr := playletbackflowstatmod.CountPlayletBackflowDailyStats(ctx, bson.M{
+ playletbackflowstatmod.FN.PlayletBackflowDailyStat.DataDate: dataDate,
+ })
+ if serr != nil {
+ return nil, serr
+ }
+ success, serr := playletbackflowstatmod.CountPlayletBackflowDailyStats(ctx, bson.M{
+ playletbackflowstatmod.FN.PlayletBackflowDailyStat.DataDate: dataDate,
+ playletbackflowstatmod.FN.PlayletBackflowDailyStat.ReportStatus: playletbackflowstatmod.ReportStatusSuccess,
+ })
+ if serr != nil {
+ return nil, serr
+ }
+ if total > 0 && total == success {
+ stats := &aiContentBackflowStats{DataDate: dataDate, Stored: total}
+ log.Info("AI 内容中台原创短剧日统计已全部上报成功",
+ log.Any("dataDate", stats.DataDate),
+ log.Any("count", total),
+ )
+ return stats, nil
+ }
+ log.Info("AI 内容中台原创短剧上报前刷新日统计",
+ log.Any("dataDate", dataDate),
+ log.Any("total", total),
+ log.Any("success", success),
+ )
+ return refreshAIContentBackflowStats(ctx, targetDate)
+}
+
+// reportAIContentBackflowStats 从本地统计表读取待上报数据,同步发送后标记状态。
+func reportAIContentBackflowStats(ctx context.Context, targetDate time.Time, client *aidatacenter.Client) (*aiContentBackflowStats, *srvdef.SrvErr) {
+ batchSize := normalizeAIContentBackflowBatchSize(aiContentBackflowBatchSize)
+ stats := &aiContentBackflowStats{DataDate: formatAIContentBackflowDate(targetDate)}
+ log.Info("AI 内容中台原创短剧统计上报开始",
+ log.Any("dataDate", stats.DataDate),
+ log.Any("batchSize", batchSize),
+ )
+ for lastID, hasLastID := primitive.NilObjectID, false; ; {
+ filter := bson.M{
+ playletbackflowstatmod.FN.PlayletBackflowDailyStat.DataDate: stats.DataDate,
+ playletbackflowstatmod.FN.PlayletBackflowDailyStat.ReportStatus: bson.M{
+ "$ne": playletbackflowstatmod.ReportStatusSuccess,
+ },
+ }
+ if hasLastID {
+ filter[playletbackflowstatmod.FN.PlayletBackflowDailyStat.Id] = bson.M{"$gt": lastID}
+ }
+ list, serr := playletbackflowstatmod.FindManyPlayletBackflowDailyStats(ctx, filter, options.Find().
+ SetSort(bson.D{{Key: playletbackflowstatmod.FN.PlayletBackflowDailyStat.Id, Value: 1}}).
+ SetLimit(batchSize))
+ if serr != nil {
+ return stats, serr
+ }
+ if len(list) == 0 {
+ break
+ }
+ records := make([]aidatacenter.Record, 0, len(list))
+ ids := make([]primitive.ObjectID, 0, len(list))
+ for _, item := range list {
+ if item == nil || item.Id.IsZero() {
+ continue
+ }
+ lastID = item.Id
+ hasLastID = true
+ if !shouldReportAIContentBackflowStat(item) {
+ stats.Skipped++
+ stats.FuturePublish++
+ log.Warn("AI 内容中台原创短剧统计跳过未来上线快照",
+ log.Any("dataDate", item.DataDate),
+ log.Any("playletId", item.PlayletId),
+ log.Any("publishedData", item.PublishedData),
+ )
+ continue
+ }
+ if item.MediaContentId <= 0 || item.ChapterId <= 0 {
+ stats.Skipped++
+ stats.MissingMediaContent++
+ log.Warn("AI 内容中台原创短剧统计跳过缺少剧集标识的快照",
+ log.Any("dataDate", item.DataDate),
+ log.Any("playletId", item.PlayletId),
+ log.Any("chapterId", item.ChapterId),
+ log.Any("mediaContentId", item.MediaContentId),
+ )
+ continue
+ }
+ records = append(records, makeAIContentBackflowRecordFromStat(item))
+ ids = append(ids, item.Id)
+ }
+ if len(records) == 0 {
+ if int64(len(list)) < batchSize {
+ break
+ }
+ continue
+ }
+ if len(ids) == 0 {
+ break
+ }
+ sendCtx, cancel := context.WithTimeout(ctx, defaultAIContentBackflowReportTimeout)
+ _, err := client.Send(sendCtx, records...)
+ cancel()
+ if err != nil {
+ reportErr := err.Error()
+ if serr := playletbackflowstatmod.MarkPlayletBackflowDailyStatsFailed(ctx, ids, reportErr); serr != nil {
+ return stats, serr
+ }
+ stats.Failed += int64(len(ids))
+ log.Error("AI 内容中台原创短剧统计批次上报失败",
+ log.Any("dataDate", stats.DataDate),
+ log.Any("batchSize", len(records)),
+ log.Any("ids", ids),
+ log.E(err),
+ )
+ return stats, srvdef.New(srvdef.Failure, "AI内容中台原创短剧上报失败", "AI内容中台原创短剧上报失败")
+ }
+ reportAt := time.Now()
+ if serr := playletbackflowstatmod.MarkPlayletBackflowDailyStatsReported(ctx, ids, reportAt); serr != nil {
+ return stats, serr
+ }
+ stats.Sent += int64(len(records))
+ stats.Reported += int64(len(ids))
+ stats.Batches++
+ log.Info("AI 内容中台原创短剧统计批次上报成功",
+ log.Any("dataDate", stats.DataDate),
+ log.Any("batchNo", stats.Batches),
+ log.Any("batchSize", len(records)),
+ log.Any("sent", stats.Sent),
+ )
+ if int64(len(list)) < batchSize {
+ break
+ }
+ }
+ return stats, nil
+}
+
+// validateAIContentBackflowRuntime 校验代码内置口径是否能生成中台必填字段。
+func validateAIContentBackflowRuntime() *srvdef.SrvErr {
+ if strings.TrimSpace(pubdef.CompanyPlatform) == "" {
+ return srvdef.New(srvdef.Failure, "AI内容中台产品名称未配置", "AI内容中台产品名称未配置")
+ }
+ if resolveAIContentBackflowVideoBaseURL() == "" {
+ return srvdef.New(srvdef.Failure, "AI内容中台视频回源地址未配置", "AI内容中台视频回源地址未配置")
+ }
+ return nil
+}
+
+// normalizeAIContentBackflowPageSize 修正扫描分页大小。
+func normalizeAIContentBackflowPageSize(v int64) int64 {
+ if v <= 0 {
+ return defaultAIContentBackflowPageSize
+ }
+ return v
+}
+
+// normalizeAIContentBackflowBatchSize 修正上报批量大小,并限制在中台上限内。
+func normalizeAIContentBackflowBatchSize(v int64) int64 {
+ if v <= 0 {
+ return defaultAIContentBackflowBatchSize
+ }
+ if v > maxAIContentBackflowBatchSize {
+ return maxAIContentBackflowBatchSize
+ }
+ return v
+}
+
+// originalPlayletBackflowFilter 只选择已标记媒资来源且已上架的短剧。
+func originalPlayletBackflowFilter(lastID int64) bson.M {
+ filter := bson.M{
+ playletmod.FN.Playlet.MediaBusiness: bson.M{"$gt": ""},
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ if lastID > 0 {
+ filter[playletmod.FN.Playlet.Id] = bson.M{"$gt": lastID}
+ }
+ return filter
+}
+
+// originalPlayletBackflowFindOptions 按 ID 游标分页,只读取组装回流记录需要的字段。
+func originalPlayletBackflowFindOptions(pageSize int64) *options.FindOptions {
+ return options.Find().
+ SetSort(bson.D{{Key: playletmod.FN.Playlet.Id, Value: 1}}).
+ SetLimit(pageSize).
+ SetProjection(bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.MediaCenterId: 1,
+ playletmod.FN.Playlet.Title: 1,
+ playletmod.FN.Playlet.Status: 1,
+ playletmod.FN.Playlet.OnlineAt: 1,
+ playletmod.FN.Playlet.MediaCreatedAt: 1,
+ playletmod.FN.Playlet.CreatedAt: 1,
+ playletmod.FN.Playlet.CountBrowse: 1,
+ playletmod.FN.Playlet.CountLike: 1,
+ playletmod.FN.Playlet.CountComment: 1,
+ playletmod.FN.Playlet.TotalEpisode: 1,
+ })
+}
+
+// findOriginalPlayletsForBackflow 查询待回流原创短剧。
+func findOriginalPlayletsForBackflow(ctx context.Context, filter bson.M, opts *options.FindOptions) (playletmod.PlayletList, *srvdef.SrvErr) {
+ return playletmod.FindManyPlaylet(ctx, filter, opts)
+}
+
+// findFirstPlayletChaptersForBackflow 取每部短剧的第一集,以及按集数最早的付费集。
+func findFirstPlayletChaptersForBackflow(ctx context.Context, playletIDs []int64) (map[int64]*aiContentBackflowChapters, *srvdef.SrvErr) {
+ if len(playletIDs) == 0 {
+ return map[int64]*aiContentBackflowChapters{}, nil
+ }
+ list, serr := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": playletIDs},
+ }, options.Find().
+ SetSort(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ }).
+ SetProjection(bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: 1,
+ playletchaptermod.FN.PlayletChapter.MediaContentId: 1,
+ playletchaptermod.FN.PlayletChapter.PlayletId: 1,
+ playletchaptermod.FN.PlayletChapter.CurrentEpisode: 1,
+ playletchaptermod.FN.PlayletChapter.VideoUrl: 1,
+ playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
+ playletchaptermod.FN.PlayletChapter.CountLike: 1,
+ playletchaptermod.FN.PlayletChapter.CountComment: 1,
+ playletchaptermod.FN.PlayletChapter.CountBuy: 1,
+ playletchaptermod.FN.PlayletChapter.PayType: 1,
+ }))
+ if serr != nil {
+ return nil, serr
+ }
+ result := make(map[int64]*aiContentBackflowChapters, len(playletIDs))
+ for _, chapter := range list {
+ if chapter == nil {
+ continue
+ }
+ item := result[chapter.PlayletId]
+ if item == nil {
+ item = &aiContentBackflowChapters{First: chapter}
+ result[chapter.PlayletId] = item
+ }
+ if item.FirstPaid == nil && isPaidPlayletChapter(chapter) {
+ item.FirstPaid = chapter
+ }
+ }
+ return result, nil
+}
+
+// isPaidPlayletChapter 金币或 VIP 剧集需要解锁后才能完整观看。
+func isPaidPlayletChapter(chapter *playletchaptermod.PlayletChapter) bool {
+ if chapter == nil {
+ return false
+ }
+ return chapter.PayType == playletchaptermod.PayTypeCoin || chapter.PayType == playletchaptermod.PayTypeVip
+}
+
+// buildAIContentBackflowRecords 把短剧模型转换成内容中台 records。
+func buildAIContentBackflowRecords(
+ ctx context.Context,
+ targetDate time.Time,
+ playlets playletmod.PlayletList,
+ findChapters func(context.Context, []int64) (map[int64]*aiContentBackflowChapters, *srvdef.SrvErr),
+) ([]aidatacenter.Record, aiContentBackflowStats, int64, *srvdef.SrvErr) {
+ stats := aiContentBackflowStats{DataDate: formatAIContentBackflowDate(targetDate)}
+ ids := make([]int64, 0, len(playlets))
+ nextID := int64(0)
+ for _, playlet := range playlets {
+ if playlet == nil {
+ continue
+ }
+ ids = append(ids, playlet.Id)
+ if playlet.Id > nextID {
+ nextID = playlet.Id
+ }
+ }
+ chapterByPlayletID, serr := findChapters(ctx, ids)
+ if serr != nil {
+ return nil, stats, nextID, serr
+ }
+ records := make([]aidatacenter.Record, 0, len(playlets))
+ for _, playlet := range playlets {
+ if playlet == nil {
+ continue
+ }
+ if !shouldReportAIContentBackflowPlaylet(targetDate, playlet) {
+ stats.Skipped++
+ stats.FuturePublish++
+ continue
+ }
+ chapters := chapterByPlayletID[playlet.Id]
+ if chapters == nil || chapters.First == nil {
+ stats.Skipped++
+ stats.MissingChapter++
+ continue
+ }
+ videoURL := buildAIContentBackflowVideoURL(chapters.First.VideoUrl)
+ if videoURL == "" {
+ stats.Skipped++
+ stats.MissingVideo++
+ continue
+ }
+ if chapters.First.MediaContentId == 0 {
+ stats.Skipped++
+ stats.MissingMediaContent++
+ continue
+ }
+ records = append(records, makeAIContentBackflowRecord(targetDate, playlet, chapters, videoURL))
+ stats.Built++
+ }
+ return records, stats, nextID, nil
+}
+
+// buildAIContentBackflowDailyStats 把短剧模型转换成本地日统计快照。
+func buildAIContentBackflowDailyStats(
+ ctx context.Context,
+ targetDate time.Time,
+ playlets playletmod.PlayletList,
+ findChapters func(context.Context, []int64) (map[int64]*aiContentBackflowChapters, *srvdef.SrvErr),
+) (playletbackflowstatmod.PlayletBackflowDailyStatList, aiContentBackflowStats, int64, *srvdef.SrvErr) {
+ stats := aiContentBackflowStats{DataDate: formatAIContentBackflowDate(targetDate)}
+ ids := make([]int64, 0, len(playlets))
+ nextID := int64(0)
+ for _, playlet := range playlets {
+ if playlet == nil {
+ continue
+ }
+ ids = append(ids, playlet.Id)
+ if playlet.Id > nextID {
+ nextID = playlet.Id
+ }
+ }
+ chapterByPlayletID, serr := findChapters(ctx, ids)
+ if serr != nil {
+ return nil, stats, nextID, serr
+ }
+ docs := make(playletbackflowstatmod.PlayletBackflowDailyStatList, 0, len(playlets))
+ for _, playlet := range playlets {
+ if playlet == nil {
+ continue
+ }
+ if !shouldReportAIContentBackflowPlaylet(targetDate, playlet) {
+ stats.Skipped++
+ stats.FuturePublish++
+ continue
+ }
+ chapters := chapterByPlayletID[playlet.Id]
+ if chapters == nil || chapters.First == nil {
+ stats.Skipped++
+ stats.MissingChapter++
+ continue
+ }
+ videoURL := buildAIContentBackflowVideoURL(chapters.First.VideoUrl)
+ if videoURL == "" {
+ stats.Skipped++
+ stats.MissingVideo++
+ continue
+ }
+ if chapters.First.MediaContentId == 0 {
+ stats.Skipped++
+ stats.MissingMediaContent++
+ continue
+ }
+ record := makeAIContentBackflowRecord(targetDate, playlet, chapters, videoURL)
+ docs = append(docs, makeAIContentBackflowDailyStat(targetDate, playlet, chapters.First, record))
+ stats.Built++
+ }
+ return docs, stats, nextID, nil
+}
+
+// makeAIContentBackflowRecord 组装一条内容中台数据,缺失指标保持空或 unknown。
+func makeAIContentBackflowRecord(targetDate time.Time, playlet *playletmod.Playlet, chapters *aiContentBackflowChapters, videoURL string) aidatacenter.Record {
+ var first, firstPaid *playletchaptermod.PlayletChapter
+ if chapters != nil {
+ first = chapters.First
+ firstPaid = chapters.FirstPaid
+ }
+ views := aiContentBackflowViews(playlet)
+ clicks := aiContentBackflowClicks(first)
+ likes := aiContentBackflowLikes(playlet, first)
+ comments := aiContentBackflowComments(playlet, first)
+ platformURL := ""
+ if first != nil {
+ platformURL = buildAIContentBackflowPlatformURL(playlet.Id, first.Id)
+ }
+ if platformURL == "" {
+ // 内容中台把 platform_url 设为必填;哔哩2是 App-only 产品时没有真实 Web 播放页。
+ // 使用已验证可访问的视频回源 URL 兜底,避免伪造不存在的播放站点链接。
+ platformURL = videoURL
+ }
+ record := aidatacenter.Record{
+ DataDate: formatAIContentBackflowDate(targetDate),
+ CompanyPlatform: strings.TrimSpace(pubdef.CompanyPlatform),
+ PublishedData: formatAIContentBackflowDate(resolveAIContentBackflowPublishedAt(playlet)),
+ ScriptName: strings.TrimSpace(playlet.Title),
+ DramaName: strings.TrimSpace(playlet.Title),
+ DramaNo: aiContentBackflowDramaNo(playlet, first),
+ VideoURL: videoURL,
+ PlatformURL: platformURL,
+ VideoID: aiContentBackflowVideoID(first),
+ PlatformVideoID: aiContentBackflowPlatformVideoID(first),
+ Views: views,
+ Clicks: clicks,
+ Likes: likes,
+ Comments: aidatacenter.Int(comments),
+ Shares: aidatacenter.Int(0),
+ CompletionRateScope: "unknown",
+ }
+ if record.ScriptName == "" {
+ record.ScriptName = record.VideoID
+ }
+ if record.DramaName == "" {
+ record.DramaName = record.VideoID
+ }
+ if clicks > 0 {
+ record.LikeRate = percentString(likes, clicks)
+ }
+ record.PaidRate = aiContentBackflowPaidRate(playlet, firstPaid)
+ return record
+}
+
+// aiContentBackflowVideoID 使用第一集的媒资章节 ID 作为中台视频编号。
+func aiContentBackflowVideoID(chapter *playletchaptermod.PlayletChapter) string {
+ if chapter == nil || chapter.MediaContentId == 0 {
+ return ""
+ }
+ return strconv.FormatUint(uint64(chapter.MediaContentId), 10)
+}
+
+// aiContentBackflowPlatformVideoID 使用本项目剧集 ID 作为产品内视频编号。
+func aiContentBackflowPlatformVideoID(chapter *playletchaptermod.PlayletChapter) string {
+ if chapter == nil || chapter.Id <= 0 {
+ return ""
+ }
+ return strconv.FormatInt(chapter.Id, 10)
+}
+
+// aiContentBackflowStatVideoID 从日统计快照还原中台视频编号。
+func aiContentBackflowStatVideoID(stat *playletbackflowstatmod.PlayletBackflowDailyStat) string {
+ if stat == nil || stat.MediaContentId <= 0 {
+ return ""
+ }
+ return strconv.FormatInt(stat.MediaContentId, 10)
+}
+
+// aiContentBackflowStatPlatformVideoID 从日统计快照还原产品内视频编号。
+func aiContentBackflowStatPlatformVideoID(stat *playletbackflowstatmod.PlayletBackflowDailyStat) string {
+ if stat == nil || stat.ChapterId <= 0 {
+ return ""
+ }
+ return strconv.FormatInt(stat.ChapterId, 10)
+}
+
+// aiContentBackflowViews 浏览量使用短剧累计浏览数。
+func aiContentBackflowViews(playlet *playletmod.Playlet) int64 {
+ if playlet == nil {
+ return 0
+ }
+ return nonNegativeInt64(playlet.CountBrowse)
+}
+
+// aiContentBackflowClicks 播放量使用第一集累计播放数。剧集浏览计数在播放时增加。
+func aiContentBackflowClicks(chapter *playletchaptermod.PlayletChapter) int64 {
+ if chapter == nil {
+ return 0
+ }
+ return nonNegativeInt64(chapter.CountBrowse)
+}
+
+// aiContentBackflowLikes 点赞优先用第一集,第一集没有点赞时回退到短剧点赞数。
+func aiContentBackflowLikes(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) int64 {
+ if chapter != nil && chapter.CountLike > 0 {
+ return chapter.CountLike
+ }
+ if playlet == nil {
+ return 0
+ }
+ return nonNegativeInt64(playlet.CountLike)
+}
+
+// aiContentBackflowComments 评论优先用第一集,第一集没有评论时回退到短剧评论数。
+func aiContentBackflowComments(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) int64 {
+ if chapter != nil && chapter.CountComment > 0 {
+ return chapter.CountComment
+ }
+ if playlet == nil {
+ return 0
+ }
+ return nonNegativeInt64(playlet.CountComment)
+}
+
+// aiContentBackflowPaidRate 付费率 = 第一个付费剧集解锁数 / 短剧浏览数。没有付费剧集时记 0.00%。
+func aiContentBackflowPaidRate(playlet *playletmod.Playlet, firstPaid *playletchaptermod.PlayletChapter) string {
+ if firstPaid == nil {
+ return "0.00%"
+ }
+ if playlet == nil || playlet.CountBrowse <= 0 {
+ return ""
+ }
+ return percentString(firstPaid.CountBuy, playlet.CountBrowse)
+}
+
+// makeAIContentBackflowDailyStat 保存一条可观察、可重试的本地统计快照。
+func makeAIContentBackflowDailyStat(targetDate time.Time, playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter, record aidatacenter.Record) *playletbackflowstatmod.PlayletBackflowDailyStat {
+ stat := &playletbackflowstatmod.PlayletBackflowDailyStat{
+ DataDate: record.DataDate,
+ PlayletId: playlet.Id,
+ ChapterId: chapter.Id,
+ CurrentEpisode: chapter.CurrentEpisode,
+ MediaCenterId: int64(playlet.MediaCenterId),
+ MediaContentId: int64(chapter.MediaContentId),
+ Title: strings.TrimSpace(playlet.Title),
+ PublishedData: record.PublishedData,
+ ScriptName: record.ScriptName,
+ DramaName: record.DramaName,
+ DramaNo: record.DramaNo,
+ VideoURL: record.VideoURL,
+ PlatformURL: record.PlatformURL,
+ Views: record.Views,
+ Clicks: record.Clicks,
+ Likes: record.Likes,
+ LikeRate: record.LikeRate,
+ CompletionRate: record.CompletionRate,
+ PaidRate: record.PaidRate,
+ CompletionRateScope: record.CompletionRateScope,
+ ReportStatus: playletbackflowstatmod.ReportStatusPending,
+ }
+ if record.Comments != nil {
+ stat.Comments = *record.Comments
+ }
+ if record.Shares != nil {
+ stat.Shares = *record.Shares
+ }
+ return stat
+}
+
+// makeAIContentBackflowRecordFromStat 还原 SDK records,用于统计表和上报逻辑解耦。
+func makeAIContentBackflowRecordFromStat(stat *playletbackflowstatmod.PlayletBackflowDailyStat) aidatacenter.Record {
+ record := aidatacenter.Record{
+ DataDate: stat.DataDate,
+ CompanyPlatform: strings.TrimSpace(pubdef.CompanyPlatform),
+ PublishedData: stat.PublishedData,
+ ScriptName: stat.ScriptName,
+ DramaName: stat.DramaName,
+ DramaNo: stat.DramaNo,
+ VideoURL: stat.VideoURL,
+ PlatformURL: stat.PlatformURL,
+ VideoID: aiContentBackflowStatVideoID(stat),
+ PlatformVideoID: aiContentBackflowStatPlatformVideoID(stat),
+ Views: stat.Views,
+ Clicks: stat.Clicks,
+ Likes: stat.Likes,
+ LikeRate: stat.LikeRate,
+ CompletionRate: stat.CompletionRate,
+ PaidRate: stat.PaidRate,
+ Comments: aidatacenter.Int(stat.Comments),
+ Shares: aidatacenter.Int(stat.Shares),
+ CompletionRateScope: stat.CompletionRateScope,
+ }
+ return record
+}
+
+// mergeAIContentBackflowStats 合并单页构造统计。
+func mergeAIContentBackflowStats(dst *aiContentBackflowStats, src aiContentBackflowStats) {
+ if dst == nil {
+ return
+ }
+ dst.Built += src.Built
+ dst.Stored += src.Stored
+ dst.Sent += src.Sent
+ dst.Batches += src.Batches
+ dst.Reported += src.Reported
+ dst.Failed += src.Failed
+ dst.Skipped += src.Skipped
+ dst.MissingChapter += src.MissingChapter
+ dst.MissingVideo += src.MissingVideo
+ dst.MissingMediaContent += src.MissingMediaContent
+ dst.FuturePublish += src.FuturePublish
+}
+
+// splitAIContentBackflowRecords 按中台批量上限切分 records。
+func splitAIContentBackflowRecords(records []aidatacenter.Record, batchSize int64) [][]aidatacenter.Record {
+ if len(records) == 0 {
+ return nil
+ }
+ if batchSize <= 0 {
+ batchSize = defaultAIContentBackflowBatchSize
+ }
+ batchLimit := int(batchSize)
+ if batchLimit <= 0 {
+ batchLimit = int(defaultAIContentBackflowBatchSize)
+ }
+ batches := make([][]aidatacenter.Record, 0, (len(records)+batchLimit-1)/batchLimit)
+ for len(records) > 0 {
+ n := batchLimit
+ if len(records) < n {
+ n = len(records)
+ }
+ batches = append(batches, records[:n])
+ records = records[n:]
+ }
+ return batches
+}
+
+// shouldReportAIContentBackflowPlaylet 判断短剧在统计日当天是否已经上线。
+func shouldReportAIContentBackflowPlaylet(targetDate time.Time, playlet *playletmod.Playlet) bool {
+ publishedAt, ok := resolveAIContentBackflowPublishedAtCandidate(playlet)
+ if !ok {
+ // 没有任何可用时间时保留旧兜底逻辑,交给后续必填校验和服务端校验处理。
+ return true
+ }
+ return !aiContentBackflowDateOnly(publishedAt).After(aiContentBackflowDateOnly(targetDate))
+}
+
+// shouldReportAIContentBackflowStat 判断历史快照是否适合在当前数据日上报。
+func shouldReportAIContentBackflowStat(stat *playletbackflowstatmod.PlayletBackflowDailyStat) bool {
+ if stat == nil {
+ return false
+ }
+ dataDate, okDataDate := parseAIContentBackflowDate(stat.DataDate)
+ publishedDate, okPublishedDate := parseAIContentBackflowDate(stat.PublishedData)
+ if !okDataDate || !okPublishedDate {
+ // 日期格式异常时不在这里吞掉,继续让 SDK 或内容中台返回明确错误。
+ return true
+ }
+ return !publishedDate.After(dataDate)
+}
+
+// resolveAIContentBackflowPublishedAt 按上架时间、媒资创建时间、本地创建时间依次兜底。
+func resolveAIContentBackflowPublishedAt(playlet *playletmod.Playlet) time.Time {
+ if t, ok := resolveAIContentBackflowPublishedAtCandidate(playlet); ok {
+ return t
+ }
+ return time.Now()
+}
+
+// resolveAIContentBackflowPublishedAtCandidate 返回业务上可确认的短剧上线候选时间。
+func resolveAIContentBackflowPublishedAtCandidate(playlet *playletmod.Playlet) (time.Time, bool) {
+ if playlet == nil {
+ return time.Time{}, false
+ }
+ for _, t := range []time.Time{playlet.OnlineAt, playlet.MediaCreatedAt, playlet.CreatedAt} {
+ if !t.IsZero() {
+ return t, true
+ }
+ }
+ return time.Time{}, false
+}
+
+// buildAIContentBackflowVideoURL 按配置的回源地址生成完整 m3u8 链接
+func buildAIContentBackflowVideoURL(rawURL string) string {
+ rawURL = strings.TrimSpace(rawURL)
+ if rawURL == "" {
+ return ""
+ }
+ if isAbsoluteHTTPURL(rawURL) {
+ return rawURL
+ }
+ if strings.ContainsAny(rawURL, "\r\n\x00\\") || strings.Contains(strings.ToLower(rawURL), "%2e") || len(rawURL) > 4096 {
+ return ""
+ }
+ parsed, err := url.Parse(rawURL)
+ if err != nil || parsed.IsAbs() || parsed.Host != "" || parsed.User != nil {
+ return ""
+ }
+ name := strings.TrimLeft(parsed.Path, "/")
+ if name == "" || hasAIContentBackflowParentPathSegment(name) {
+ return ""
+ }
+ if ext := path.Ext(name); ext == "" {
+ name += ".m3u8"
+ } else if !strings.EqualFold(ext, ".m3u8") {
+ return ""
+ }
+ result := joinAIContentBackflowURL(resolveAIContentBackflowVideoBaseURL(), name)
+ if result != "" && parsed.RawQuery != "" {
+ result += "?" + parsed.RawQuery
+ }
+ return result
+}
+
+// buildAIContentBackflowPlatformURL 生成产品内播放页 URL。
+func buildAIContentBackflowPlatformURL(playletID, chapterID int64) string {
+ return joinAIContentBackflowURL(resolveAIContentBackflowPlatformBaseURL(), fmt.Sprintf("/play?id=%d&chapterId=%d", playletID, chapterID))
+}
+
+// resolveAIContentBackflowPlatformBaseURL 返回短剧播放页域名。
+func resolveAIContentBackflowPlatformBaseURL() string {
+ if baseURL := strings.TrimSpace(aiContentBackflowPlatformBaseURL); baseURL != "" {
+ return strings.TrimRight(baseURL, "/")
+ }
+ return ""
+}
+
+// resolveAIContentBackflowVideoBaseURL 返回数据回流专用视频回源域名。
+func resolveAIContentBackflowVideoBaseURL() string {
+ if baseURL := strings.TrimSpace(aiContentBackflowVideoBaseURL); baseURL != "" {
+ return strings.TrimRight(baseURL, "/")
+ }
+ if baseURL := strings.TrimSpace(webg.Conf.DataCenterBackflow.SelfSourceBaseUrl); baseURL != "" {
+ return strings.TrimRight(baseURL, "/")
+ }
+ return ""
+}
+
+// normalizeAIContentBackflowBaseURL 兼容配置里的裸域名和完整 URL。
+func normalizeAIContentBackflowBaseURL(host string, listenAddr string) string {
+ host = strings.TrimRight(strings.TrimSpace(host), "/")
+ if host == "" {
+ return ""
+ }
+ if isAbsoluteHTTPURL(host) {
+ return host
+ }
+ if listenAddr = strings.TrimSpace(listenAddr); listenAddr != "" && !strings.Contains(host, ":") {
+ host += ":" + listenAddr
+ }
+ return "https://" + host
+}
+
+// joinAIContentBackflowURL 拼接 baseURL 和相对路径;绝对路径直接返回。
+func joinAIContentBackflowURL(baseURL string, path string) string {
+ baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
+ path = strings.TrimSpace(path)
+ if baseURL == "" || path == "" {
+ return ""
+ }
+ if isAbsoluteHTTPURL(path) {
+ return path
+ }
+ return baseURL + "/" + strings.TrimLeft(path, "/")
+}
+
+// isAbsoluteHTTPURL 判断是否为 http/https 完整 URL。
+func isAbsoluteHTTPURL(value string) bool {
+ value = strings.ToLower(strings.TrimSpace(value))
+ return strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://")
+}
+
+// hasAIContentBackflowParentPathSegment 拒绝路径穿越。
+func hasAIContentBackflowParentPathSegment(name string) bool {
+ for _, segment := range strings.Split(name, "/") {
+ if segment == ".." {
+ return true
+ }
+ }
+ return false
+}
+
+// formatAIContentBackflowDate 使用北京时间输出中台要求的 DD/MM/YYYY。
+func formatAIContentBackflowDate(t time.Time) string {
+ return t.In(aiContentBackflowLocation()).Format(aidatacenter.DateLayout)
+}
+
+// parseAIContentBackflowDate 解析中台 DD/MM/YYYY 日期为北京时间零点。
+func parseAIContentBackflowDate(value string) (time.Time, bool) {
+ t, err := time.ParseInLocation(aidatacenter.DateLayout, strings.TrimSpace(value), aiContentBackflowLocation())
+ return t, err == nil
+}
+
+// aiContentBackflowDateOnly 只保留北京时间日期,用于比较发布日和统计日。
+func aiContentBackflowDateOnly(t time.Time) time.Time {
+ local := t.In(aiContentBackflowLocation())
+ return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, aiContentBackflowLocation())
+}
+
+// aiContentBackflowLocation 返回内容中台日期口径使用的北京时间。
+func aiContentBackflowLocation() *time.Location {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ return loc
+}
+
+// aiContentBackflowDramaNo 生成中台要求的集数文案。
+func aiContentBackflowDramaNo(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) string {
+ if chapter != nil && chapter.CurrentEpisode > 0 {
+ return fmt.Sprintf("第%d集", chapter.CurrentEpisode)
+ }
+ if playlet != nil && playlet.TotalEpisode > 0 {
+ return fmt.Sprintf("共%d集", playlet.TotalEpisode)
+ }
+ return "第1集"
+}
+
+// nonNegativeInt64 把业务累计计数转成中台非负 int64。
+func nonNegativeInt64(v int64) int64 {
+ if v <= 0 {
+ return 0
+ }
+ return v
+}
+
+// percentString 生成保留两位小数的百分比字符串。
+func percentString(numerator, denominator int64) string {
+ if numerator < 0 {
+ numerator = 0
+ }
+ if denominator <= 0 {
+ return ""
+ }
+ return fmt.Sprintf("%.2f%%", float64(numerator)*100/float64(denominator))
+}
diff --git a/web/service/srv_mediaresource/cover_backfill.go b/web/service/srv_mediaresource/cover_backfill.go
new file mode 100644
index 0000000..67c191f
--- /dev/null
+++ b/web/service/srv_mediaresource/cover_backfill.go
@@ -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))
+ }
+ }
+}
diff --git a/web/service/srv_mediaresource/cover_compress.go b/web/service/srv_mediaresource/cover_compress.go
new file mode 100644
index 0000000..ab0ac85
--- /dev/null
+++ b/web/service/srv_mediaresource/cover_compress.go
@@ -0,0 +1,177 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "videoweb/pub/common/imgutil"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/storage/proxy"
+ "videoweb/web/service/srv_search"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// compressPlayletCoverOrEmpty 下载原封面并按 media Resize 规则处理。
+// 原图未超过 60KB,或原图是 GIF 时,直接返回原封面地址;其它格式超过阈值则压缩为 JPEG 后上传。
+// 失败返回空字符串,不阻断入库。
+func compressPlayletCoverOrEmpty(cover string) string {
+ cover = strings.TrimSpace(cover)
+ if cover == "" {
+ return ""
+ }
+ pathStr, err := uploadCompressedPlayletCover(cover)
+ if err != nil {
+ log.Warn("压缩短剧封面失败", log.Any("cover", cover), log.E(err))
+ return ""
+ }
+ return pathStr
+}
+
+// FillPlayletCompressedCover 为指定短剧补写压缩封面,已有值则跳过。
+func FillPlayletCompressedCover(ctx context.Context, playlet *playletmod.Playlet) bool {
+ if playlet == nil || playlet.Id <= 0 {
+ return false
+ }
+ if strings.TrimSpace(playlet.CompressedCover) != "" {
+ return false
+ }
+ cover := strings.TrimSpace(playlet.Cover)
+ if cover == "" {
+ return false
+ }
+ pathStr := compressPlayletCoverOrEmpty(cover)
+ if pathStr == "" {
+ return false
+ }
+ now := time.Now()
+ if err := playletmod.UpdateOnePlaylet(ctx,
+ bson.M{
+ playletmod.FN.Playlet.Id: playlet.Id,
+ "$or": []bson.M{
+ {playletmod.FN.Playlet.CompressedCover: bson.M{"$exists": false}},
+ {playletmod.FN.Playlet.CompressedCover: ""},
+ {playletmod.FN.Playlet.CompressedCover: nil},
+ },
+ },
+ bson.M{"$set": bson.M{
+ playletmod.FN.Playlet.CompressedCover: pathStr,
+ playletmod.FN.Playlet.UpdatedAt: now,
+ }},
+ ); err != nil {
+ log.Warn("回写短剧压缩封面失败", log.Any("playletId", playlet.Id), log.E(err))
+ return false
+ }
+ playlet.CompressedCover = pathStr
+ latest, serr := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playlet.Id})
+ if serr == nil && latest != nil && latest.Status == playletmod.StatusUp {
+ if syncErr := srv_search.SyncPlayletStatusToES(ctx, []int64{playlet.Id}, latest.Status); syncErr != nil {
+ log.Warn("压缩封面后同步ES失败", log.Any("playletId", playlet.Id), log.E(syncErr))
+ }
+ }
+ return true
+}
+
+func uploadCompressedPlayletCover(cover string) (string, error) {
+ downloadURL, err := resolvePlayletCoverURL(cover)
+ if err != nil {
+ return "", err
+ }
+ downloadURL, err = encodeCoverDownloadURL(downloadURL)
+ if err != nil {
+ return "", err
+ }
+ data, err := downloadPlayletCover(downloadURL)
+ if err != nil {
+ return "", err
+ }
+ data = imgutil.DecryptImage(data)
+ if !imgutil.NeedCompress(data) {
+ return cover, nil
+ }
+ fileName := imgutil.SafeCoverFileName(downloadURL)
+ compressed, outName, err := imgutil.CompressCover(data, fileName)
+ if err != nil {
+ return "", err
+ }
+ pathStr, err := proxy.UploadImg(outName, compressed, "")
+ if err != nil {
+ return "", fmt.Errorf("上传压缩封面失败: %w", err)
+ }
+ if strings.TrimSpace(pathStr) == "" {
+ return "", fmt.Errorf("上传压缩封面返回空路径")
+ }
+ return pathStr, nil
+}
+
+func resolvePlayletCoverURL(cover string) (string, error) {
+ if strings.HasPrefix(cover, "http://") || strings.HasPrefix(cover, "https://") {
+ return cover, nil
+ }
+ sysCfg, serr := cfg.FindOneSysCfg(context.Background(), bson.M{cfg.FN.SysCfg.Id: 1})
+ if serr != nil {
+ return "", fmt.Errorf("查询图片CDN失败: %s", serr.Error())
+ }
+ imageDomain := ""
+ if sysCfg != nil {
+ imageDomain = strings.TrimSpace(sysCfg.ImgWebCdn)
+ }
+ if imageDomain == "" {
+ return "", fmt.Errorf("图片CDN未配置,无法下载相对路径封面")
+ }
+ return netutil.MergeURL(imageDomain, cover), nil
+}
+
+// encodeCoverDownloadURL 把路径里的中文等非 ASCII 段转成百分号编码,避免下载失败或二次拼接异常。
+func encodeCoverDownloadURL(raw string) (string, error) {
+ raw = strings.TrimSpace(raw)
+ u, err := url.Parse(raw)
+ if err != nil {
+ return "", fmt.Errorf("解析封面地址失败: %w", err)
+ }
+ if u.Scheme == "" || u.Host == "" {
+ return "", fmt.Errorf("封面地址无效")
+ }
+ escaped := u.EscapedPath()
+ if escaped != "" {
+ if dec, err := url.PathUnescape(escaped); err == nil {
+ u.Path = dec
+ u.RawPath = ""
+ }
+ }
+ return u.String(), nil
+}
+
+func downloadPlayletCover(rawURL string) ([]byte, error) {
+ req, err := http.NewRequest(http.MethodGet, rawURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("下载封面失败: %w", err)
+ }
+ req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; xjsp-playlet-cover/1.0)")
+ req.Header.Set("Accept", "image/*")
+
+ cli := &http.Client{Timeout: 90 * time.Second}
+ resp, err := cli.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("下载封面失败: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("下载封面失败,状态码 %d", resp.StatusCode)
+ }
+ data, err := io.ReadAll(io.LimitReader(resp.Body, 20<<20))
+ if err != nil {
+ return nil, fmt.Errorf("下载封面失败: %w", err)
+ }
+ if len(data) == 0 {
+ return nil, fmt.Errorf("下载封面内容为空")
+ }
+ return data, nil
+}
diff --git a/web/service/srv_mediaresource/import_worker.go b/web/service/srv_mediaresource/import_worker.go
new file mode 100644
index 0000000..d9adf81
--- /dev/null
+++ b/web/service/srv_mediaresource/import_worker.go
@@ -0,0 +1,169 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ goredis "github.com/go-redis/redis"
+)
+
+const (
+ playletResourceImportQueueKeyFmt = "playlet:resource:import:queue:%d"
+ playletResourceImportLockKeyFmt = "playlet:resource:import:lock:%d:%d"
+ playletResourceImportLockTTL = 10 * time.Minute
+ playletResourceImportPopWait = 5 * time.Second
+ playletResourceImportRetryWait = time.Second
+)
+
+type playletResourceImportJob struct {
+ Ids []uint `json:"ids"`
+ OperatorId int64 `json:"operatorId"`
+ OperatorName string `json:"operatorName"`
+ CreateTime int64 `json:"createTime"`
+}
+
+var playletResourceImportWorker = struct {
+ mu sync.Mutex
+ cancel context.CancelFunc
+}{}
+
+func playletResourceImportQueueKey() string {
+ return fmt.Sprintf(playletResourceImportQueueKeyFmt, pubdef.AppId)
+}
+
+func playletResourceImportLockKey(mediaId uint) string {
+ return fmt.Sprintf(playletResourceImportLockKeyFmt, pubdef.AppId, mediaId)
+}
+
+// Start 启动短剧媒资导入消费者
+func Start() {
+ playletResourceImportWorker.mu.Lock()
+ defer playletResourceImportWorker.mu.Unlock()
+ if playletResourceImportWorker.cancel != nil {
+ return
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ playletResourceImportWorker.cancel = cancel
+ go runPlayletResourceImportWorker(ctx)
+ go runPlayletCoverCompressWorker(ctx)
+ go runPlayletSerialSyncWorker(ctx)
+ go func() {
+ time.Sleep(10 * time.Second)
+ EnqueuePlayletCoverCompressJobs()
+ }()
+ log.Info("短剧媒资后台消费者已启动")
+}
+
+// Stop 停止短剧媒资导入消费者
+func Stop() {
+ playletResourceImportWorker.mu.Lock()
+ defer playletResourceImportWorker.mu.Unlock()
+ if playletResourceImportWorker.cancel == nil {
+ return
+ }
+ playletResourceImportWorker.cancel()
+ playletResourceImportWorker.cancel = nil
+ log.Info("短剧媒资后台消费者已停止")
+}
+
+// pushPlayletResourceImportJob 投递短剧媒资导入任务
+func pushPlayletResourceImportJob(ctx context.Context, ids []uint, operatorId int64, operatorName string) *srvdef.SrvErr {
+ select {
+ case <-ctx.Done():
+ log.Error("投递短剧媒资导入任务取消", log.E(ctx.Err()))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ default:
+ }
+ job := playletResourceImportJob{
+ Ids: ids,
+ OperatorId: operatorId,
+ OperatorName: operatorName,
+ CreateTime: time.Now().Unix(),
+ }
+ payload, err := json.Marshal(job)
+ if err != nil {
+ log.Error("序列化短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ }
+ if err = redis.RPush(playletResourceImportQueueKey(), string(payload)); err != nil {
+ log.Error("投递短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ }
+ log.Info("投递短剧媒资导入任务成功", log.Any("ids", ids), log.Any("count", len(ids)))
+ return nil
+}
+
+// runPlayletResourceImportWorker 循环消费短剧媒资导入队列
+func runPlayletResourceImportWorker(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ result, err := redis.Client().BLPop(playletResourceImportPopWait, playletResourceImportQueueKey()).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ continue
+ }
+ log.Error("消费短剧媒资导入任务失败", log.E(err))
+ time.Sleep(playletResourceImportRetryWait)
+ continue
+ }
+ if len(result) < 2 {
+ log.Error("短剧媒资导入任务格式异常", log.Any("result", result))
+ continue
+ }
+ handlePlayletResourceImportPayload(ctx, result[1])
+ }
+}
+
+// handlePlayletResourceImportPayload 处理单个短剧媒资导入任务
+func handlePlayletResourceImportPayload(ctx context.Context, payload string) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("处理短剧媒资导入任务异常", log.Any("err", err), log.Any("payload", payload))
+ }
+ }()
+ var job playletResourceImportJob
+ if err := json.Unmarshal([]byte(payload), &job); err != nil {
+ log.Error("解析短剧媒资导入任务失败", log.Any("payload", payload), log.E(err))
+ return
+ }
+ ids := dedupePlayletResourceIds(job.Ids)
+ if len(ids) == 0 {
+ log.Error("短剧媒资导入任务没有有效媒资ID", log.Any("payload", payload))
+ return
+ }
+ log.Info("开始处理短剧媒资导入任务",
+ log.Any("ids", ids),
+ log.Any("count", len(ids)),
+ log.Any("createTime", job.CreateTime),
+ log.Any("operatorId", job.OperatorId),
+ log.Any("operatorName", job.OperatorName))
+ resp := importPlayletResourceBatch(ctx, ids, job.OperatorId, job.OperatorName)
+ log.Info("短剧媒资导入任务处理完成",
+ log.Any("ids", ids),
+ log.Any("successCount", len(resp.Success)),
+ log.Any("failedCount", len(resp.Failed)),
+ log.Any("failed", resp.Failed))
+}
+
+// lockPlayletResourceImportMedia 锁定单条媒资导入
+func lockPlayletResourceImportMedia(mediaId uint) (bool, error) {
+ return redis.Setnx_NewOK(playletResourceImportLockKey(mediaId), true, playletResourceImportLockTTL)
+}
+
+// unlockPlayletResourceImportMedia 释放单条媒资导入锁
+func unlockPlayletResourceImportMedia(mediaId uint) {
+ if err := redis.Del(playletResourceImportLockKey(mediaId)); err != nil {
+ log.Error("释放短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
+ }
+}
diff --git a/web/service/srv_mediaresource/media_center.go b/web/service/srv_mediaresource/media_center.go
new file mode 100644
index 0000000..a3b0fc4
--- /dev/null
+++ b/web/service/srv_mediaresource/media_center.go
@@ -0,0 +1,214 @@
+package srv_mediaresource
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strconv"
+ "videoweb/pub/common"
+ "videoweb/pub/common/httputil"
+ "videoweb/pub/common/log"
+ "videoweb/web/api/proto"
+)
+
+const (
+ playletMediaListErrMsg = "短剧媒资列表查询失败"
+ playletMediaSyncErrMsg = "短剧媒资同步失败"
+ playletMediaDeletedCode = 7003
+)
+
+// playletMediaCenterService 短剧媒资中心客户端
+type playletMediaCenterService struct {
+ url string // 媒资中心接口域名
+ proId string // 媒资中心商户号
+ secret string // 媒资中心签名密钥,当前接口按源项目逻辑暂不签名
+}
+
+// playletMediaCenterConfig 创建短剧媒资中心客户端所需配置
+type playletMediaCenterConfig struct {
+ Url string // 媒资中心接口域名
+ ProId string // 媒资中心商户号
+ Secret string // 媒资中心签名密钥
+}
+
+// playletMediaCenterMsg 媒资中心统一响应结构
+type playletMediaCenterMsg struct {
+ Code int `json:"code"` // 媒资中心业务码,0 表示成功
+ Msg string `json:"msg"` // 媒资中心业务消息
+ Data json.RawMessage `json:"data"` // 媒资中心业务数据,校验 code 成功后再解析
+}
+
+// playletMediaPagination 媒资中心列表分页信息
+type playletMediaPagination struct {
+ IsLastPage bool `json:"is_last_page"` // 是否最后一页
+ Page uint `json:"page"` // 当前页码
+ Size uint `json:"size"` // 每页数量
+ Count uint `json:"count"` // 总数量
+}
+
+// playletMediaListReq 媒资中心 /services/media/list 查询参数
+type playletMediaListReq struct {
+ MediaType int `json:"media_type"` // 媒体类型:1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧
+ Id *uint `json:"id"` // 媒资中心资源ID
+ VloggerId *uint `json:"vlogger_id"` // UP主ID
+ IsUp *int `json:"is_up"` // 是否UP主资源:0-否 1-是
+ Title *string `json:"title"` // 标题筛选
+ Tag *string `json:"tag"` // 标签筛选
+ SyncType int `json:"sync_type"` // 同步类型:1-全量 2-增量
+ Page uint `json:"page"` // 页码
+ Size uint `json:"size"` // 每页数量
+}
+
+// playletMediaListResp 媒资中心列表响应 data 节点
+type playletMediaListResp struct {
+ Data []*proto.PlayletResourceListItem `json:"data"` // 短剧媒资列表
+ Pagination playletMediaPagination `json:"pagination"` // 分页信息
+}
+
+// playletMediaContent 媒资中心短剧章节内容
+type playletMediaContent struct {
+ ID uint `json:"id"` // 媒资内容ID
+ SaleCoin uint `json:"sale_coin"` // 单集售价
+ VloggerId uint `json:"vlogger_id"` // UP主ID
+ Cover string `json:"cover"` // 封面
+ MediaId uint `json:"video_id"` // 媒资中心资源ID
+ MediaType int `json:"media_type"` // 媒体类型
+ EpisodeNumber int `json:"episode_number"` // 第几集
+ Title string `json:"title"` // 章节名
+ Text string `json:"text"` // 文本内容
+ Urls string `json:"urls"` // 资源地址集
+ Height uint `json:"height"` // 高
+ Width uint `json:"width"` // 宽
+ Ratio float64 `json:"ratio"` // 宽高比
+ MediaSize uint `json:"media_size"` // 资源大小
+ PlayTime uint `json:"play_time"` // 播放时长
+ M3u8Url string `json:"m3u8_url"` // m3u8视频地址
+ AudioUrl string `json:"audio_url"` // 音频地址
+ FsResourceId string `json:"fs_resource_id"` // 文件服务资源ID
+ FsResourceStatus string `json:"fs_resource_status"` // 文件服务资源状态
+ HashId string `json:"hash_id"` // 资源hash
+ DataVersion int8 `json:"data_version"` // 数据版本
+ TerminalCdo string `json:"terminal_cdo"` // 上传者
+ CreateTime int64 `json:"create_time"` // 创建时间
+ UpdateTime int64 `json:"update_time"` // 更新时间
+}
+
+// playletMediaSyncReq 媒资中心 /services/media/sync 查询参数
+type playletMediaSyncReq struct {
+ Id uint `json:"id"` // 媒资中心资源ID
+ SyncTime int64 `json:"sync_time"` // 增量同步时间,0 表示拉全量详情
+}
+
+// playletMediaSyncResp 媒资中心同步响应 data 节点
+type playletMediaSyncResp struct {
+ IsDelete bool `json:"-"`
+ Media *proto.PlayletResourceListItem `json:"media"`
+ MediaContentList []playletMediaContent `json:"media_content_list"`
+}
+
+// newPlayletMediaCenterService 创建短剧媒资中心客户端
+func newPlayletMediaCenterService(c playletMediaCenterConfig) *playletMediaCenterService {
+ return &playletMediaCenterService{
+ url: c.Url,
+ proId: c.ProId,
+ secret: c.Secret,
+ }
+}
+
+// mediaListParams 构造媒资中心列表查询参数
+func (m *playletMediaCenterService) mediaListParams(p *playletMediaListReq) map[string]string {
+ params := map[string]string{
+ "media_type": strconv.Itoa(p.MediaType),
+ "sync_type": strconv.Itoa(p.SyncType),
+ "page": strconv.FormatUint(uint64(p.Page), 10),
+ "size": strconv.FormatUint(uint64(p.Size), 10),
+ "pro_id": m.proId,
+ "sign": m.sign(),
+ }
+ if p.Id != nil {
+ params["id"] = strconv.FormatUint(uint64(*p.Id), 10)
+ }
+ if p.VloggerId != nil {
+ params["vlogger_id"] = strconv.FormatUint(uint64(*p.VloggerId), 10)
+ }
+ if p.IsUp != nil {
+ params["is_up"] = strconv.Itoa(*p.IsUp)
+ }
+ if p.Title != nil {
+ params["title"] = *p.Title
+ }
+ if p.Tag != nil {
+ params["tag"] = *p.Tag
+ }
+ return params
+}
+
+// mediaSyncParams 构造媒资中心同步查询参数
+func (m *playletMediaCenterService) mediaSyncParams(p *playletMediaSyncReq) map[string]string {
+ return map[string]string{
+ "id": strconv.FormatUint(uint64(p.Id), 10),
+ "sync_time": strconv.FormatInt(p.SyncTime, 10),
+ "pro_id": m.proId,
+ "sign": m.sign(),
+ }
+}
+
+// mediaList 查询短剧媒资列表
+func (m *playletMediaCenterService) mediaList(p *playletMediaListReq) (result playletMediaListResp, err error) {
+ var retMsg playletMediaCenterMsg
+ params := m.mediaListParams(p)
+ path := "/services/media/list"
+ log.Info("查询短剧媒资列表", log.Any("params", params))
+ code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
+ if err != nil {
+ log.Error("查询短剧媒资列表请求失败", log.Any("code", code), log.E(err))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if code != http.StatusOK {
+ log.Error("查询短剧媒资列表状态码异常", log.Any("code", code))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if retMsg.Code != 0 {
+ log.Error("查询短剧媒资列表业务码异常", log.Any("retMsg", retMsg))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if err = json.Unmarshal(retMsg.Data, &result); err != nil {
+ log.Error("解析短剧媒资列表失败", log.E(err))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ return result, nil
+}
+
+// mediaSync 查询短剧媒资详情和章节内容
+func (m *playletMediaCenterService) mediaSync(p *playletMediaSyncReq) (result playletMediaSyncResp, err error) {
+ var retMsg playletMediaCenterMsg
+ params := m.mediaSyncParams(p)
+ path := "/services/media/sync"
+ code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
+ if err != nil {
+ log.Error("同步短剧媒资详情请求失败", log.Any("params", params), log.Any("code", code), log.E(err))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ if code != http.StatusOK {
+ log.Error("同步短剧媒资详情状态码异常", log.Any("params", params), log.Any("code", code))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ if retMsg.Code == playletMediaDeletedCode {
+ result.IsDelete = true
+ return result, nil
+ }
+ if retMsg.Code != 0 {
+ log.Error("同步短剧媒资详情业务码异常", log.Any("params", params), log.Any("retMsg", retMsg))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ if err = json.Unmarshal(retMsg.Data, &result); err != nil {
+ log.Error("解析短剧媒资详情失败", log.Any("params", params), log.E(err))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ return result, nil
+}
+
+// sign 生成媒资中心签名
+func (m *playletMediaCenterService) sign() string {
+ return ""
+}
diff --git a/web/service/srv_mediaresource/original_backfill.go b/web/service/srv_mediaresource/original_backfill.go
new file mode 100644
index 0000000..1fdfbca
--- /dev/null
+++ b/web/service/srv_mediaresource/original_backfill.go
@@ -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
+}
diff --git a/web/service/srv_mediaresource/resource.go b/web/service/srv_mediaresource/resource.go
new file mode 100644
index 0000000..902b8ba
--- /dev/null
+++ b/web/service/srv_mediaresource/resource.go
@@ -0,0 +1,678 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "math/rand"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/web/api/proto"
+ "videoweb/web/middleware/auth"
+ "videoweb/web/webg"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultResourceSyncType = 1
+ defaultResourcePageNum = 1
+ defaultResourcePageSize = 20
+ maxResourcePageSize = 100
+ maxResourceImportIds = 100
+ playletResourceImportMsg = "同步任务已提交,后台正在处理"
+ playletHeatMinValue = 1000
+ playletHeatMaxValue = 5000
+
+ playletResourceMediaTypeAnime = 1 // 动漫
+ playletResourceMediaTypeComic = 4 // 漫画短剧
+ playletResourceMediaTypeShort = 5 // 短剧
+ playletResourceMediaTypeNormal = 6 // 普通短剧
+)
+
+// ListPlayletResource 分页查询短剧媒资列表
+func ListPlayletResource(ctx context.Context, req *proto.PlayletResourceListReq) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ syncType := defaultResourceSyncType
+ if req.SyncType != nil {
+ syncType = *req.SyncType
+ }
+ pageNumber := req.PageNumber
+ if pageNumber <= 0 {
+ pageNumber = defaultResourcePageNum
+ }
+ pageSize := req.PageSize
+ if pageSize <= 0 {
+ pageSize = defaultResourcePageSize
+ }
+ if pageSize > maxResourcePageSize {
+ pageSize = maxResourcePageSize
+ }
+ mediaType, err1 := resolvePlayletResourceMediaType(req.MediaType)
+ if err1 != nil {
+ return nil, err1
+ }
+ ms := newPlayletMediaCenterFromConfig()
+ data, err := ms.mediaList(&playletMediaListReq{
+ MediaType: mediaType,
+ Title: req.Title,
+ Tag: req.Tag,
+ SyncType: syncType,
+ Page: uint(pageNumber),
+ Size: uint(pageSize),
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ if err := fillPlayletResourceAddStatus(ctx, data.Data); err != nil {
+ return nil, err
+ }
+ return &proto.PlayletResourceListResp{
+ List: data.Data,
+ Total: data.Pagination.Count,
+ }, nil
+}
+
+// ListPlayletResourceChapters 查询短剧媒资剧集信息
+func ListPlayletResourceChapters(ctx context.Context, req *proto.PlayletResourceChapterListReq) (*proto.PlayletResourceChapterListResp, *srvdef.SrvErr) {
+ if req == nil || req.Id == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
+ }
+ ms := newPlayletMediaCenterFromConfig()
+ result, err := ms.mediaSync(&playletMediaSyncReq{
+ Id: req.Id,
+ SyncTime: 0,
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ if result.IsDelete || result.Media == nil {
+ return nil, srvdef.New(srvdef.ErrNoData, "媒资不存在或已删除", "")
+ }
+ list := make([]*proto.PlayletResourceChapterItem, 0, len(result.MediaContentList))
+ for _, item := range result.MediaContentList {
+ videoUrl := strings.TrimSpace(item.M3u8Url)
+ if videoUrl == "" {
+ videoUrl = strings.TrimSpace(item.Urls)
+ }
+ list = append(list, &proto.PlayletResourceChapterItem{
+ Id: item.ID,
+ EpisodeNumber: item.EpisodeNumber,
+ Title: strings.TrimSpace(item.Title),
+ Cover: item.Cover,
+ VideoUrl: videoUrl,
+ Height: item.Height,
+ Width: item.Width,
+ MediaSize: item.MediaSize,
+ PlayTime: item.PlayTime,
+ SaleCoin: item.SaleCoin,
+ HashId: item.HashId,
+ CreateTime: item.CreateTime,
+ UpdateTime: item.UpdateTime,
+ })
+ }
+ return &proto.PlayletResourceChapterListResp{
+ MediaId: req.Id,
+ Title: result.Media.Title,
+ List: list,
+ Total: len(list),
+ }, nil
+}
+
+// resolvePlayletResourceMediaType 校验后台显式传入的媒资类型
+func resolvePlayletResourceMediaType(mediaType *int) (int, *srvdef.SrvErr) {
+ if mediaType == nil {
+ return 0, srvdef.New(srvdef.ErrParamError, "媒体类型不能为空", "媒体类型不能为空")
+ }
+ if !validPlayletResourceMediaType(*mediaType) {
+ return 0, srvdef.New(srvdef.ErrParamError, "媒体类型无效,仅支持1-动漫、2-漫画、4-漫画短剧、5-短剧、6-普通短剧", "媒体类型无效,仅支持1-动漫、2-漫画、4-漫画短剧、5-短剧、6-普通短剧")
+ }
+ return *mediaType, nil
+}
+
+// validPlayletResourceMediaType 媒资列表当前允许的媒体类型
+func validPlayletResourceMediaType(mediaType int) bool {
+ switch mediaType {
+ case playletResourceMediaTypeAnime, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
+ return true
+ default:
+ return false
+ }
+}
+
+// validPlayletImportMediaType 允许入库的媒资类型
+func validPlayletImportMediaType(mediaType int) bool {
+ switch mediaType {
+ case playletResourceMediaTypeAnime, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
+ return true
+ default:
+ return false
+ }
+}
+
+// contentTypeFromMediaType 媒资中心 media_type 映射为本项目 contentType
+func contentTypeFromMediaType(mediaType int) playletmod.ContentType {
+ switch mediaType {
+ case playletResourceMediaTypeAnime:
+ return playletmod.ContentTypeAnime
+ case playletResourceMediaTypeComic:
+ return playletmod.ContentTypeComic
+ case playletResourceMediaTypeNormal:
+ return playletmod.ContentTypeAlbum
+ default:
+ return playletmod.ContentTypeTheater
+ }
+}
+
+// fillPlayletResourceAddStatus 回填媒资列表是否已加入后台短剧库
+func fillPlayletResourceAddStatus(ctx context.Context, list []*proto.PlayletResourceListItem) *srvdef.SrvErr {
+ mediaIds := make([]uint, 0, len(list))
+ seen := make(map[uint]struct{}, len(list))
+ for _, item := range list {
+ if item == nil || item.ID == 0 {
+ continue
+ }
+ if _, ok := seen[item.ID]; ok {
+ continue
+ }
+ seen[item.ID] = struct{}{}
+ mediaIds = append(mediaIds, item.ID)
+ }
+ if len(mediaIds) == 0 {
+ return nil
+ }
+
+ playletList, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": mediaIds},
+ })
+ if err != nil {
+ log.Error("查询短剧媒资加入状态失败", log.Any("mediaIds", mediaIds), log.Any("err", err))
+ return err
+ }
+ addedMap := make(map[uint]bool, len(playletList))
+ for _, item := range playletList {
+ if item == nil || item.MediaCenterId == 0 {
+ continue
+ }
+ addedMap[item.MediaCenterId] = true
+ }
+ for _, item := range list {
+ if item != nil {
+ item.IsAdd = addedMap[item.ID]
+ }
+ }
+ return nil
+}
+
+// ImportPlayletResource 提交短剧媒资入库任务
+func ImportPlayletResource(ctx context.Context, req *proto.PlayletResourceUseReq) (string, *srvdef.SrvErr) {
+ ids, err := validatePlayletResourceImportReq(req)
+ if err != nil {
+ return "", err
+ }
+ operatorId, operatorName := resolveImportOperator(ctx)
+ if err := pushPlayletResourceImportJob(ctx, ids, operatorId, operatorName); err != nil {
+ return "", err
+ }
+ return playletResourceImportMsg, nil
+}
+
+// importPlayletResourceBatch 批量处理短剧媒资入库任务
+func importPlayletResourceBatch(ctx context.Context, ids []uint, operatorId int64, operatorName string) *proto.PlayletResourceUseResp {
+ resp := &proto.PlayletResourceUseResp{
+ Success: make([]*proto.PlayletResourceUseSuccess, 0, len(ids)),
+ Failed: make([]*proto.PlayletResourceUseFailed, 0),
+ }
+ importedMap, err := findImportedPlayletResourceMap(ctx, ids)
+ if err != nil {
+ for _, id := range ids {
+ resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
+ MediaId: id,
+ Reason: "查询导入状态失败",
+ })
+ }
+ return resp
+ }
+ ms := newPlayletMediaCenterFromConfig()
+ for _, id := range ids {
+ if importedMap[id] {
+ resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
+ MediaId: id,
+ Reason: "该媒资已导入",
+ })
+ continue
+ }
+ success, reason := importSinglePlayletResource(ctx, ms, id, operatorId, operatorName)
+ if reason != "" {
+ resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
+ MediaId: id,
+ Reason: reason,
+ })
+ continue
+ }
+ resp.Success = append(resp.Success, success)
+ }
+ return resp
+}
+
+// findImportedPlayletResourceMap 批量查询已经导入的媒资ID
+func findImportedPlayletResourceMap(ctx context.Context, ids []uint) (map[uint]bool, *srvdef.SrvErr) {
+ result := make(map[uint]bool, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ playletList, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": ids},
+ })
+ if err != nil {
+ log.Error("批量查询短剧媒资导入状态失败", log.Any("mediaIds", ids), log.Any("err", err))
+ return nil, err
+ }
+ for _, item := range playletList {
+ if item == nil || item.MediaCenterId == 0 {
+ continue
+ }
+ result[item.MediaCenterId] = true
+ }
+ return result, nil
+}
+
+func newPlayletMediaCenterFromConfig() *playletMediaCenterService {
+ return newPlayletMediaCenterService(playletMediaCenterConfig{
+ Url: webg.Conf.MediaResourceDBCfg.BaseUrl,
+ ProId: webg.Conf.MediaResourceDBCfg.Merchat,
+ Secret: webg.Conf.MediaResourceDBCfg.Secret,
+ })
+}
+
+// validatePlayletResourceImportReq 校验短剧媒资入库请求
+func validatePlayletResourceImportReq(req *proto.PlayletResourceUseReq) ([]uint, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePlayletResourceIds(req.Ids)
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
+ }
+ if len(ids) > maxResourceImportIds {
+ return nil, srvdef.New(srvdef.ErrParamError, "单次最多导入100条媒资", "")
+ }
+ return ids, nil
+}
+
+// dedupePlayletResourceIds 去重并过滤无效媒资ID
+func dedupePlayletResourceIds(ids []uint) []uint {
+ result := make([]uint, 0, len(ids))
+ seen := make(map[uint]struct{}, len(ids))
+ for _, id := range ids {
+ if id == 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
+
+// importSinglePlayletResource 导入单条短剧媒资
+func importSinglePlayletResource(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string) (*proto.PlayletResourceUseSuccess, string) {
+ locked, err := lockPlayletResourceImportMedia(mediaId)
+ if err != nil {
+ log.Error("获取短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
+ return nil, "获取导入锁失败"
+ }
+ if !locked {
+ return nil, "该媒资正在导入"
+ }
+ defer unlockPlayletResourceImportMedia(mediaId)
+
+ if reason := checkPlayletResourceImported(ctx, mediaId); reason != "" {
+ return nil, reason
+ }
+ result, err := ms.mediaSync(&playletMediaSyncReq{
+ Id: mediaId,
+ SyncTime: 0,
+ })
+ if err != nil {
+ log.Error("同步短剧媒资失败", log.Any("mediaId", mediaId), log.E(err))
+ return nil, "同步媒资失败"
+ }
+ if result.IsDelete || result.Media == nil {
+ return nil, "媒资不存在或已删除"
+ }
+ if !validPlayletImportMediaType(result.Media.MediaType) {
+ return nil, "该媒资类型不支持导入短剧"
+ }
+ if len(result.MediaContentList) == 0 {
+ return nil, "媒资章节为空"
+ }
+
+ playlet := buildPlayletFromMedia(result.Media, operatorId, operatorName)
+ playlet.MediaSyncTime = maxPlayletMediaContentUpdateTime(0, result.MediaContentList)
+ if playlet.Title == "" {
+ return nil, "媒资标题为空"
+ }
+ exists := func(s string) bool {
+ item, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, 0))
+ return item != nil
+ }
+ if serr := slugx.Ensure(&playlet.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, playlet.Title), exists); serr != nil {
+ if serr.Tips != "" {
+ return nil, serr.Tips
+ }
+ return nil, "slug生成失败"
+ }
+ // 原图继续写入 Cover;压缩成功才写 CompressedCover,失败保持空。
+ playlet.CompressedCover = compressPlayletCoverOrEmpty(playlet.Cover)
+ chapters := buildPlayletChaptersFromMedia(result.MediaContentList)
+ if len(chapters) == 0 {
+ return nil, "媒资章节为空"
+ }
+ if err1 := playletmod.InsertOnePlaylet(ctx, playlet); err1 != nil {
+ log.Error("短剧媒资主表入库失败", log.Any("mediaId", mediaId), log.Any("err", err1))
+ return nil, "主资源入库失败"
+ }
+ for _, chapter := range chapters {
+ chapter.PlayletId = playlet.Id
+ }
+ if err1 := playletchaptermod.InsertManyPlayletChapter(ctx, chapters); err1 != nil {
+ log.Error("短剧媒资章节批量入库失败", log.Any("mediaId", mediaId), log.Any("playletId", playlet.Id), log.Any("chapterCount", len(chapters)), log.Any("err", err1))
+ rollbackImportedPlayletResource(ctx, playlet.Id, collectPlayletChapterIds(chapters))
+ return nil, "章节入库失败"
+ }
+ return &proto.PlayletResourceUseSuccess{
+ MediaId: mediaId,
+ PlayletId: playlet.Id,
+ }, ""
+}
+
+// collectPlayletChapterIds 收集章节ID
+func collectPlayletChapterIds(chapters playletchaptermod.PlayletChapterList) []int64 {
+ ids := make([]int64, 0, len(chapters))
+ for _, chapter := range chapters {
+ if chapter == nil || chapter.Id == 0 {
+ continue
+ }
+ ids = append(ids, chapter.Id)
+ }
+ return ids
+}
+
+// checkPlayletResourceImported 检查媒资是否已经导入到本项目短剧表
+func checkPlayletResourceImported(ctx context.Context, mediaId uint) string {
+ _, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.MediaCenterId: mediaId})
+ if err == nil {
+ return "该媒资已导入"
+ }
+ if err.Code != srvdef.ErrNoData {
+ log.Error("查询短剧媒资导入状态失败", log.Any("mediaId", mediaId), log.Any("err", err))
+ return "查询导入状态失败"
+ }
+ return ""
+}
+
+// buildPlayletFromMedia 将媒资中心主资源转换为本项目 playlet 主表模型
+func buildPlayletFromMedia(media *proto.PlayletResourceListItem, operatorId int64, operatorName string) *playletmod.Playlet {
+ cover := strings.TrimSpace(media.VerticalCover)
+ if cover == "" {
+ cover = strings.TrimSpace(media.HorizontalCover)
+ }
+ return &playletmod.Playlet{
+ MediaCenterId: uint(media.ID),
+ MediaCreatedAt: mediaResourceTime(media.CreateTime),
+ MediaUpdatedAt: mediaResourceTime(media.UpdateTime),
+ Title: strings.TrimSpace(media.Title),
+ Desc: strings.TrimSpace(media.Summary),
+ Anchor: strings.TrimSpace(media.Anchor),
+ Cover: cover,
+ OldTags: oldTagNamesFromPlayletTagsText(media.TagsText),
+ MediaBusiness: playletMediaBusiness(media),
+ ContentType: contentTypeFromMediaType(media.MediaType),
+ TotalEpisode: int64(media.TotalEpisode),
+ UpdateStatus: normalizePlayletUpdateStatus(media.UpdateStatus),
+ HeatValue: randomPlayletHeatValue(),
+ Status: playletmod.StatusDown,
+ OperatorId: operatorId,
+ OperatorName: operatorName,
+ }
+}
+
+// playletMediaBusinessCGDJ 赤果短剧、CGDJ 标题前缀对应的媒资业务来源。
+const playletMediaBusinessCGDJ = "CGDJ"
+
+// playletMediaBusinessRule 把媒资标题前缀映射成要写入的业务来源。先匹配先生效。
+type playletMediaBusinessRule struct {
+ Prefix string
+ Business string
+}
+
+// playletMediaBusinessRules 多个前缀可以对应同一个业务来源。
+// 新增来源时在这里追加;同一来源的不同叫法共用同一个 Business。
+var playletMediaBusinessRules = []playletMediaBusinessRule{
+ //{Prefix: "赤果短剧", Business: playletMediaBusinessCGDJ},
+ {Prefix: "CGDJ", Business: playletMediaBusinessCGDJ},
+}
+
+func playletMediaBusiness(media *proto.PlayletResourceListItem) string {
+ if media == nil || media.MediaType != playletResourceMediaTypeShort {
+ return ""
+ }
+ title := strings.ToUpper(strings.TrimSpace(media.Title))
+ for _, rule := range playletMediaBusinessRules {
+ prefix := strings.ToUpper(strings.TrimSpace(rule.Prefix))
+ business := strings.TrimSpace(rule.Business)
+ if prefix == "" || business == "" {
+ continue
+ }
+ if strings.HasPrefix(title, prefix) {
+ return business
+ }
+ }
+ return ""
+}
+
+// playletMediaBusinessSearchTitles 返回回填时用来搜索媒资库的标题前缀。
+func playletMediaBusinessSearchTitles() []string {
+ seen := make(map[string]struct{}, len(playletMediaBusinessRules))
+ titles := make([]string, 0, len(playletMediaBusinessRules))
+ for _, rule := range playletMediaBusinessRules {
+ prefix := strings.TrimSpace(rule.Prefix)
+ if prefix == "" {
+ continue
+ }
+ key := strings.ToUpper(prefix)
+ if _, ok := seen[key]; ok {
+ continue
+ }
+ seen[key] = struct{}{}
+ titles = append(titles, prefix)
+ }
+ return titles
+}
+
+func oldTagNamesFromPlayletTagsText(tagsText string) []string {
+ fields := strings.FieldsFunc(strings.TrimSpace(tagsText), func(r rune) bool {
+ switch r {
+ case ',', ',', '、', ';', ';', '/', '|':
+ return true
+ default:
+ return false
+ }
+ })
+ names := make([]string, 0, len(fields))
+ seen := make(map[string]struct{}, len(fields))
+ for _, field := range fields {
+ name := strings.TrimSpace(field)
+ if name == "" {
+ continue
+ }
+ if _, ok := seen[name]; ok {
+ continue
+ }
+ seen[name] = struct{}{}
+ names = append(names, name)
+ }
+ return names
+}
+
+func randomPlayletHeatValue() int64 {
+ return int64(rand.Intn(playletHeatMaxValue-playletHeatMinValue+1) + playletHeatMinValue)
+}
+
+// mediaResourceTime 转换媒资中心时间戳
+func mediaResourceTime(ts int64) time.Time {
+ if ts <= 0 {
+ return time.Time{}
+ }
+ if ts > 1e12 {
+ return time.UnixMilli(ts)
+ }
+ return time.Unix(ts, 0)
+}
+
+// buildPlayletChaptersFromMedia 将媒资中心章节资源转换为本项目 playlet_chapter 模型
+func buildPlayletChaptersFromMedia(mediaContentList []playletMediaContent) playletchaptermod.PlayletChapterList {
+ chapters := make(playletchaptermod.PlayletChapterList, 0, len(mediaContentList))
+ seenMediaContentIds := make(map[uint]struct{}, len(mediaContentList))
+ seenEpisodes := make(map[int]struct{}, len(mediaContentList))
+ seenHashIds := make(map[string]struct{}, len(mediaContentList))
+ seenVideoUrls := make(map[string]struct{}, len(mediaContentList))
+ for _, item := range mediaContentList {
+ videoUrl := strings.TrimSpace(item.M3u8Url)
+ if videoUrl == "" {
+ videoUrl = strings.TrimSpace(item.Urls)
+ }
+ if videoUrl == "" {
+ continue
+ }
+ if isDuplicatePlayletMediaChapter(item, videoUrl, seenMediaContentIds, seenEpisodes, seenHashIds, seenVideoUrls) {
+ continue
+ }
+ episodeNumber := int64(item.EpisodeNumber)
+ if episodeNumber <= 0 {
+ episodeNumber = int64(len(chapters) + 1)
+ }
+ height := int64(item.Height)
+ width := int64(item.Width)
+ ratio := item.Ratio
+ if ratio <= 0 && height > 0 {
+ ratio = float64(width) / float64(height)
+ }
+ chapters = append(chapters, &playletchaptermod.PlayletChapter{
+ MediaContentId: uint(item.ID),
+ CurrentEpisode: episodeNumber,
+ Title: strings.TrimSpace(item.Title),
+ Hash: item.HashId,
+ Height: height,
+ Width: width,
+ Ratio: ratio,
+ VideoUrl: videoUrl,
+ MediaSize: int64(item.MediaSize),
+ MediaTime: int64(item.PlayTime),
+ PayType: playletchaptermod.PayTypeFree,
+ SingleCoins: int64(item.SaleCoin),
+ WatermarkStatus: playletchaptermod.WatermarkStatusPending,
+ })
+ }
+ return chapters
+}
+
+// isDuplicatePlayletMediaChapter 判断媒资中心章节是否重复
+func isDuplicatePlayletMediaChapter(item playletMediaContent, videoUrl string, seenMediaContentIds map[uint]struct{}, seenEpisodes map[int]struct{}, seenHashIds map[string]struct{}, seenVideoUrls map[string]struct{}) bool {
+ if item.ID > 0 {
+ if _, ok := seenMediaContentIds[item.ID]; ok {
+ log.Info("跳过重复短剧媒资章节", log.Any("mediaContentId", item.ID), log.Any("episodeNumber", item.EpisodeNumber))
+ return true
+ }
+ seenMediaContentIds[item.ID] = struct{}{}
+ return false
+ }
+ if item.EpisodeNumber > 0 {
+ if _, ok := seenEpisodes[item.EpisodeNumber]; ok {
+ log.Info("跳过重复短剧媒资章节", log.Any("episodeNumber", item.EpisodeNumber))
+ return true
+ }
+ seenEpisodes[item.EpisodeNumber] = struct{}{}
+ return false
+ }
+ hashId := strings.TrimSpace(item.HashId)
+ if hashId != "" {
+ if _, ok := seenHashIds[hashId]; ok {
+ log.Info("跳过重复短剧媒资章节", log.Any("hashId", hashId))
+ return true
+ }
+ seenHashIds[hashId] = struct{}{}
+ return false
+ }
+ if _, ok := seenVideoUrls[videoUrl]; ok {
+ log.Info("跳过重复短剧媒资章节", log.Any("videoUrl", videoUrl))
+ return true
+ }
+ seenVideoUrls[videoUrl] = struct{}{}
+ return false
+}
+
+// rollbackImportedPlayletResource 回滚单条媒资入库产生的数据
+func rollbackImportedPlayletResource(ctx context.Context, playletId int64, chapterIds []int64) {
+ for _, chapterId := range chapterIds {
+ if _, err := playletchaptermod.DelOnePlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.Id: chapterId}); err != nil {
+ log.Error("回滚短剧媒资章节失败", log.Any("playletId", playletId), log.Any("chapterId", chapterId), log.Any("err", err))
+ }
+ }
+ if _, err := playletmod.DelOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playletId}); err != nil {
+ log.Error("回滚短剧媒资主表失败", log.Any("playletId", playletId), log.Any("err", err))
+ }
+}
+
+// resolveImportOperator 从后台登录态解析当前操作人
+func resolveImportOperator(ctx context.Context) (int64, string) {
+ c, ok := ctx.(*gin.Context)
+ if !ok || c == nil {
+ return 0, ""
+ }
+ accId := auth.GetAccount(c)
+ if accId <= 0 {
+ return 0, ""
+ }
+ acc, err := mwacc.FindOneAccByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Id: accId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: mwacc.FN.MwAcc.Id, Value: 1},
+ {Key: mwacc.FN.MwAcc.UserName, Value: 1},
+ }))
+ if err != nil || acc == nil {
+ return accId, ""
+ }
+ return acc.Id, acc.UserName
+}
+
+// maxPlayletMediaContentUpdateTime 记录本次同步返回章节中的最大更新时间
+func maxPlayletMediaContentUpdateTime(syncTime int64, mediaContentList []playletMediaContent) int64 {
+ result := syncTime
+ for _, item := range mediaContentList {
+ if item.UpdateTime > result {
+ result = item.UpdateTime
+ }
+ }
+ return result
+}
+
+// normalizePlayletUpdateStatus 三方 update_status=0 在本项目按 2-已完结保存
+func normalizePlayletUpdateStatus(updateStatus int) int {
+ if updateStatus == 0 {
+ return 2
+ }
+ return updateStatus
+}
diff --git a/web/service/srv_mediaresource/serial_sync.go b/web/service/srv_mediaresource/serial_sync.go
new file mode 100644
index 0000000..96d6a1c
--- /dev/null
+++ b/web/service/srv_mediaresource/serial_sync.go
@@ -0,0 +1,468 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+ "videoweb/web/api/proto"
+
+ goredis "github.com/go-redis/redis"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ // PlayletSerialSyncCronSpec 连载中短剧同步扫描频率:每1小时扫描一次,扫描阶段只投递 Redis 任务
+ PlayletSerialSyncCronSpec = "0 0 * * * ?"
+
+ // playletSerialSyncQueueKeyFmt 解耦定时扫描和三方同步调用,避免 cron 扫描过程直接阻塞在外部接口上
+ playletSerialSyncQueueKeyFmt = "playlet:serial:sync:queue:%d"
+ playletSerialSyncLockKeyFmt = "playlet:serial:sync:lock:%d:%d"
+ playletSerialSyncLockTTL = 55 * time.Minute
+ playletSerialSyncPopWait = 5 * time.Second
+ playletSerialSyncRetryWait = time.Second
+ playletSerialSyncBatchSize = int64(100)
+ playletManualSyncMaxCount = 100
+)
+
+func playletSerialSyncQueueKey() string {
+ return fmt.Sprintf(playletSerialSyncQueueKeyFmt, pubdef.AppId)
+}
+
+func playletSerialSyncLockKey(mediaId uint) string {
+ return fmt.Sprintf(playletSerialSyncLockKeyFmt, pubdef.AppId, mediaId)
+}
+
+type playletSerialSyncJob struct {
+ PlayletId int64 `json:"playletId"`
+ MediaCenterId uint `json:"mediaCenterId"`
+ MediaSyncTime int64 `json:"mediaSyncTime"`
+ CreateTime int64 `json:"createTime"`
+}
+
+// EnqueueSerialPlayletSyncJobs 扫描连载中短剧并投递 Redis 同步任务
+//
+// 这里不直接请求三方媒资中心:扫描只负责发现任务,实际同步由后台消费者逐条处理
+// 可以降低定时任务执行时间,也方便 Redis 队列在三方接口短暂异常时继续缓冲任务
+func EnqueueSerialPlayletSyncJobs() {
+ ctx := context.Background()
+ filter := bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
+ playletmod.FN.Playlet.UpdateStatus: 1,
+ // 原创短剧由后台维护剧集,自动同步只处理媒资导入剧
+ playletmod.FN.Playlet.IsOriginal: bson.M{"$ne": true},
+ }
+ opts := options.Find().
+ SetSort(bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1}, {Key: playletmod.FN.Playlet.Id, Value: 1}}).
+ SetLimit(playletSerialSyncBatchSize)
+ list, err := playletmod.FindManyPlaylet(ctx, filter, opts)
+ if err != nil {
+ log.Error("扫描连载中短剧失败", log.Any("err", err))
+ return
+ }
+ enqueued := 0
+ for _, item := range list {
+ if item == nil || item.Id == 0 || item.MediaCenterId == 0 || item.IsOriginal {
+ continue
+ }
+ if err := pushPlayletSerialSyncJob(ctx, item); err != nil {
+ log.Error("投递连载短剧同步任务失败", log.Any("playletId", item.Id), log.Any("mediaCenterId", item.MediaCenterId), log.E(err))
+ continue
+ }
+ enqueued++
+ }
+ log.Info("连载中短剧同步任务扫描完成", log.Any("scanCount", len(list)), log.Any("enqueued", enqueued))
+}
+
+// pushPlayletSerialSyncJob 将单部连载短剧投递到 Redis 队列
+func pushPlayletSerialSyncJob(ctx context.Context, playlet *playletmod.Playlet) error {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+ job := playletSerialSyncJob{
+ PlayletId: playlet.Id,
+ MediaCenterId: playlet.MediaCenterId,
+ MediaSyncTime: playlet.MediaSyncTime,
+ CreateTime: time.Now().Unix(),
+ }
+ payload, err := json.Marshal(job)
+ if err != nil {
+ return err
+ }
+ return redis.RPush(playletSerialSyncQueueKey(), string(payload))
+}
+
+// runPlayletSerialSyncWorker 常驻消费连载短剧同步队列
+func runPlayletSerialSyncWorker(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ result, err := redis.Client().BLPop(playletSerialSyncPopWait, playletSerialSyncQueueKey()).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ continue
+ }
+ log.Error("消费连载短剧同步任务失败", log.E(err))
+ time.Sleep(playletSerialSyncRetryWait)
+ continue
+ }
+ if len(result) < 2 {
+ log.Error("连载短剧同步任务格式异常", log.Any("result", result))
+ continue
+ }
+ handlePlayletSerialSyncPayload(ctx, result[1])
+ }
+}
+
+// handlePlayletSerialSyncPayload 解析单个 Redis 任务并执行同步,panic 会被捕获避免 worker 退出
+func handlePlayletSerialSyncPayload(ctx context.Context, payload string) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("处理连载短剧同步任务异常", log.Any("err", err), log.Any("payload", payload))
+ }
+ }()
+ var job playletSerialSyncJob
+ if err := json.Unmarshal([]byte(payload), &job); err != nil {
+ log.Error("解析连载短剧同步任务失败", log.Any("payload", payload), log.E(err))
+ return
+ }
+ if job.PlayletId <= 0 || job.MediaCenterId == 0 {
+ log.Error("连载短剧同步任务参数异常", log.Any("job", job))
+ return
+ }
+ if _, reason := syncSerialPlaylet(ctx, job, false, false); reason != "" {
+ log.Error("连载短剧同步任务失败", log.Any("job", job), log.Any("reason", reason))
+ return
+ }
+ log.Info("连载短剧同步任务完成", log.Any("job", job))
+}
+
+// SyncPlayletChapters 手动同步指定短剧的媒资剧集,支持单/批量,允许已完结短剧补集或修正状态。
+func SyncPlayletChapters(ctx context.Context, req *proto.PlayletChapterSyncReq) (*proto.PlayletChapterSyncResp, *srvdef.SrvErr) {
+ ids := normalizePlayletChapterSyncIds(req)
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ if len(ids) > playletManualSyncMaxCount {
+ return nil, srvdef.New(srvdef.ErrParamError, "单次最多同步100部短剧", "")
+ }
+ resp := &proto.PlayletChapterSyncResp{
+ Total: len(ids),
+ List: make([]*proto.PlayletChapterSyncResult, 0, len(ids)),
+ }
+ for _, playletId := range ids {
+ item := syncOnePlayletChapters(ctx, playletId)
+ if item.Success {
+ resp.SuccessCount++
+ } else {
+ resp.FailedCount++
+ }
+ resp.List = append(resp.List, item)
+ }
+ return resp, nil
+}
+
+// normalizePlayletChapterSyncIds 过滤重复或无效短剧ID。
+func normalizePlayletChapterSyncIds(req *proto.PlayletChapterSyncReq) []int64 {
+ if req == nil {
+ return nil
+ }
+ result := make([]int64, 0, len(req.PlayletIds))
+ seen := make(map[int64]struct{}, len(req.PlayletIds))
+ for _, id := range req.PlayletIds {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
+
+// syncOnePlayletChapters 同步单部短剧的媒资剧集,单部失败不影响批量里其他短剧。
+func syncOnePlayletChapters(ctx context.Context, playletId int64) *proto.PlayletChapterSyncResult {
+ result := &proto.PlayletChapterSyncResult{
+ PlayletId: playletId,
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: playletId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaSyncTime, Value: 1},
+ }))
+ if err != nil {
+ result.Reason = playletChapterSyncErrReason(err)
+ return result
+ }
+ if playlet.MediaCenterId == 0 {
+ result.Reason = "非媒资导入短剧不能同步剧集"
+ return result
+ }
+ newChapterCount, reason := syncSerialPlaylet(ctx, playletSerialSyncJob{
+ PlayletId: playlet.Id,
+ MediaCenterId: playlet.MediaCenterId,
+ MediaSyncTime: playlet.MediaSyncTime,
+ CreateTime: time.Now().Unix(),
+ }, true, true)
+ if reason != "" {
+ result.Reason = reason
+ return result
+ }
+ result.Success = true
+ result.NewChapterCount = newChapterCount
+ return result
+}
+
+// playletChapterSyncErrReason 转换单部短剧同步失败原因,避免批量结果里 reason 为空。
+func playletChapterSyncErrReason(err *srvdef.SrvErr) string {
+ if err == nil {
+ return ""
+ }
+ if err.Code == srvdef.ErrNoData {
+ return "短剧不存在"
+ }
+ if reason := err.GetTip(); reason != "" {
+ return reason
+ }
+ if reason := err.GetMsg(); reason != "" {
+ return reason
+ }
+ return err.Error()
+}
+
+// syncSerialPlaylet 更新主表集数/状态,只追加新增章节,不覆盖已有章节。
+// 手动同步使用 forceFullSync=true 全量拉取,便于恢复本地误删的旧章节;定时任务继续使用增量同步。
+func syncSerialPlaylet(ctx context.Context, job playletSerialSyncJob, allowCompleted bool, forceFullSync bool) (int, string) {
+ locked, err := lockPlayletSerialSync(job.MediaCenterId)
+ if err != nil {
+ log.Error("获取连载短剧同步锁失败", log.Any("job", job), log.E(err))
+ return 0, "获取同步锁失败"
+ }
+ if !locked {
+ return 0, "该媒资正在同步"
+ }
+ defer unlockPlayletSerialSync(job.MediaCenterId)
+
+ playlet, serr := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: job.PlayletId,
+ playletmod.FN.Playlet.MediaCenterId: job.MediaCenterId,
+ })
+ if serr != nil {
+ if serr.Code == srvdef.ErrNoData {
+ return 0, "短剧不存在"
+ }
+ log.Error("查询连载短剧失败", log.Any("job", job), log.Any("err", serr))
+ return 0, "查询短剧失败"
+ }
+ if !allowCompleted && playlet.IsOriginal {
+ return 0, ""
+ }
+ if !allowCompleted && playlet.UpdateStatus != 1 {
+ return 0, ""
+ }
+
+ ms := newPlayletMediaCenterFromConfig()
+ syncTime := playlet.MediaSyncTime
+ if forceFullSync {
+ syncTime = 0
+ }
+ result, err := ms.mediaSync(&playletMediaSyncReq{
+ Id: playlet.MediaCenterId,
+ SyncTime: syncTime,
+ })
+ if err != nil {
+ log.Error("同步连载短剧媒资失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.E(err))
+ return 0, "同步媒资失败"
+ }
+ if result.IsDelete {
+ return 0, "媒资不存在或已删除"
+ }
+ if result.Media == nil {
+ return 0, "媒资主资源为空"
+ }
+
+ newSyncTime := maxPlayletMediaContentUpdateTime(playlet.MediaSyncTime, result.MediaContentList)
+ if reason := updateSerialPlayletInfo(ctx, playlet, result.Media, newSyncTime); reason != "" {
+ return 0, reason
+ }
+ if len(result.MediaContentList) == 0 {
+ return 0, ""
+ }
+ chapters, reason := buildNewSerialPlayletChapters(ctx, playlet.Id, result.MediaContentList)
+ if reason != "" {
+ return 0, reason
+ }
+ if len(chapters) == 0 {
+ return 0, ""
+ }
+ // 上架且连载中时,新增剧集默认支付方式取剧级设置。
+ applyPlayletDefaultPayToChapters(playlet, chapters)
+ if serr = playletchaptermod.InsertManyPlayletChapter(ctx, chapters); serr != nil {
+ log.Error("连载短剧新增章节入库失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("count", len(chapters)), log.Any("err", serr))
+ return 0, "新增章节入库失败"
+ }
+ log.Info("连载短剧新增章节成功", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("count", len(chapters)))
+ return len(chapters), ""
+}
+
+// updateSerialPlayletInfo 更新三方主资源里会随连载变化的字段
+func updateSerialPlayletInfo(ctx context.Context, playlet *playletmod.Playlet, media *proto.PlayletResourceListItem, mediaSyncTime int64) string {
+ updateStatus := normalizePlayletUpdateStatus(media.UpdateStatus)
+ setDoc := bson.M{
+ playletmod.FN.Playlet.TotalEpisode: int64(media.TotalEpisode),
+ playletmod.FN.Playlet.UpdateStatus: updateStatus,
+ playletmod.FN.Playlet.MediaUpdatedAt: mediaResourceTime(media.UpdateTime),
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if mediaSyncTime > playlet.MediaSyncTime {
+ setDoc[playletmod.FN.Playlet.MediaSyncTime] = mediaSyncTime
+ }
+ if business := playletMediaBusiness(media); business != "" && playlet.MediaBusiness != business {
+ setDoc[playletmod.FN.Playlet.MediaBusiness] = business
+ }
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: playlet.Id,
+ }, bson.M{"$set": setDoc}); err != nil {
+ log.Error("更新连载短剧主表失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("err", err))
+ return "更新主表失败"
+ }
+ return ""
+}
+
+// buildNewSerialPlayletChapters 根据本地已存在章节做幂等过滤,只返回需要追加的新章节
+func buildNewSerialPlayletChapters(ctx context.Context, playletId int64, mediaContentList []playletMediaContent) (playletchaptermod.PlayletChapterList, string) {
+ exists, err := loadExistingPlayletChapterKeys(ctx, playletId)
+ if err != nil {
+ return nil, "查询已有章节失败"
+ }
+ chapters := buildPlayletChaptersFromMedia(mediaContentList)
+ result := make(playletchaptermod.PlayletChapterList, 0, len(chapters))
+ for _, chapter := range chapters {
+ if chapter == nil || chapterDuplicateByKeys(chapter, exists) {
+ continue
+ }
+ chapter.PlayletId = playletId
+ markExistingPlayletChapterKeys(chapter, exists)
+ result = append(result, chapter)
+ }
+ return result, ""
+}
+
+type existingPlayletChapterKeys struct {
+ mediaContentIds map[uint]struct{}
+ episodes map[int64]struct{}
+ hashes map[string]struct{}
+ videoUrls map[string]struct{}
+}
+
+// loadExistingPlayletChapterKeys 加载当前短剧已入库章节的去重键,避免连载同步重复插入章节
+func loadExistingPlayletChapterKeys(ctx context.Context, playletId int64) (existingPlayletChapterKeys, error) {
+ keys := existingPlayletChapterKeys{
+ mediaContentIds: make(map[uint]struct{}),
+ episodes: make(map[int64]struct{}),
+ hashes: make(map[string]struct{}),
+ videoUrls: make(map[string]struct{}),
+ }
+ list, serr := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: playletId})
+ if serr != nil {
+ return keys, fmt.Errorf("query playlet chapters failed: %+v", serr)
+ }
+ for _, chapter := range list {
+ markExistingPlayletChapterKeys(chapter, keys)
+ }
+ return keys, nil
+}
+
+// chapterDuplicateByKeys 章节跨批次去重,优先用媒资章节ID,其次用集数、hash、播放地址兜底
+func chapterDuplicateByKeys(chapter *playletchaptermod.PlayletChapter, keys existingPlayletChapterKeys) bool {
+ if chapter.MediaContentId > 0 {
+ if _, ok := keys.mediaContentIds[chapter.MediaContentId]; ok {
+ return true
+ }
+ }
+ if chapter.CurrentEpisode > 0 {
+ if _, ok := keys.episodes[chapter.CurrentEpisode]; ok {
+ return true
+ }
+ }
+ if chapter.Hash != "" {
+ if _, ok := keys.hashes[chapter.Hash]; ok {
+ return true
+ }
+ }
+ if chapter.VideoUrl != "" {
+ if _, ok := keys.videoUrls[chapter.VideoUrl]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+// markExistingPlayletChapterKeys 把已存在或本批次准备插入的章节登记到去重集合
+func markExistingPlayletChapterKeys(chapter *playletchaptermod.PlayletChapter, keys existingPlayletChapterKeys) {
+ if chapter == nil {
+ return
+ }
+ if chapter.MediaContentId > 0 {
+ keys.mediaContentIds[chapter.MediaContentId] = struct{}{}
+ }
+ if chapter.CurrentEpisode > 0 {
+ keys.episodes[chapter.CurrentEpisode] = struct{}{}
+ }
+ if chapter.Hash != "" {
+ keys.hashes[chapter.Hash] = struct{}{}
+ }
+ if chapter.VideoUrl != "" {
+ keys.videoUrls[chapter.VideoUrl] = struct{}{}
+ }
+}
+
+// applyPlayletDefaultPayToChapters 上架且连载中时,新增章节默认沿用剧级支付设置。
+func applyPlayletDefaultPayToChapters(playlet *playletmod.Playlet, chapters playletchaptermod.PlayletChapterList) {
+ if playlet == nil || len(chapters) == 0 {
+ return
+ }
+ if playlet.Status != playletmod.StatusUp || playlet.UpdateStatus != 1 {
+ return
+ }
+ payType := playletchaptermod.PayType(playlet.PayType)
+ singleCoins := playlet.SingleCoins
+ if payType != playletchaptermod.PayTypeCoin {
+ singleCoins = 0
+ }
+ for _, chapter := range chapters {
+ if chapter == nil {
+ continue
+ }
+ chapter.PayType = payType
+ chapter.SingleCoins = singleCoins
+ }
+}
+
+// lockPlayletSerialSync 锁定单个媒资ID,避免同一部连载短剧被多个 worker 同时同步
+func lockPlayletSerialSync(mediaId uint) (bool, error) {
+ return redis.Setnx_NewOK(playletSerialSyncLockKey(mediaId), true, playletSerialSyncLockTTL)
+}
+
+// unlockPlayletSerialSync 释放单个媒资ID的同步锁
+func unlockPlayletSerialSync(mediaId uint) {
+ if err := redis.Del(playletSerialSyncLockKey(mediaId)); err != nil {
+ log.Error("释放连载短剧同步锁失败", log.Any("mediaId", mediaId), log.E(err))
+ }
+}
diff --git a/web/service/srv_mwacc/acc.go b/web/service/srv_mwacc/acc.go
new file mode 100644
index 0000000..d472bef
--- /dev/null
+++ b/web/service/srv_mwacc/acc.go
@@ -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
+}
diff --git a/web/service/srv_mwacc/define.go b/web/service/srv_mwacc/define.go
new file mode 100644
index 0000000..3a86440
--- /dev/null
+++ b/web/service/srv_mwacc/define.go
@@ -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"`
+}
diff --git a/web/service/srv_mwacc/login.go b/web/service/srv_mwacc/login.go
new file mode 100644
index 0000000..322c24a
--- /dev/null
+++ b/web/service/srv_mwacc/login.go
@@ -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
+}
diff --git a/web/service/srv_mwacc/role.go b/web/service/srv_mwacc/role.go
new file mode 100644
index 0000000..21ee448
--- /dev/null
+++ b/web/service/srv_mwacc/role.go
@@ -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)
+}
diff --git a/web/service/srv_playlet/category.go b/web/service/srv_playlet/category.go
new file mode 100644
index 0000000..65b4833
--- /dev/null
+++ b/web/service/srv_playlet/category.go
@@ -0,0 +1,219 @@
+package srv_playlet
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var playletCategoryWebProjection = bson.D{
+ {Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Slug, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Icon, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Status, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Rank, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Remark, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.SeoInfo.Title, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.SeoInfo.Description, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.SeoInfo.Keyworld, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.CreatedAt, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.UpdatedAt, Value: 1},
+}
+
+// ListPlayletCategory 分页查询短剧分类列表
+func ListPlayletCategory(ctx context.Context, req *proto.PlayletCategoryListReq) (*proto.PlayletCategoryListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playletcategorymod.FN.PlayletCategory.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[playletcategorymod.FN.PlayletCategory.Status] = *req.Status
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[playletcategorymod.FN.PlayletCategory.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playletcategorymod.CountPlayletCategory(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletCategoryListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletCategoryListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playletcategorymod.FindManyPlayletCategory(ctx, filter, options.Find().
+ SetProjection(playletCategoryWebProjection).
+ SetSort(bson.D{
+ {Key: playletcategorymod.FN.PlayletCategory.Rank, Value: -1},
+ {Key: playletcategorymod.FN.PlayletCategory.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletCategoryListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.PlayletCategoryListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Icon: item.Icon,
+ Status: item.Status,
+ Rank: item.Rank,
+ Remark: item.Remark,
+ SeoInfo: normalizeSeoValue(item.SeoInfo),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlayletCategory 新增短剧分类
+func AddPlayletCategory(ctx context.Context, req *proto.PlayletCategoryAddReq) (*proto.PlayletCategoryAddResp, *srvdef.SrvErr) {
+ if req == nil || strings.TrimSpace(req.Name) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "分类名称不能为空", "")
+ }
+ doc := &playletcategorymod.PlayletCategory{
+ Name: strings.TrimSpace(req.Name),
+ Slug: req.Slug,
+ Icon: strings.TrimSpace(req.Icon),
+ Status: req.Status,
+ Rank: req.Rank,
+ Remark: strings.TrimSpace(req.Remark),
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ exists := func(s string) bool {
+ item, _ := playletcategorymod.FindOnePlayletCategory(ctx, slugx.Filter(
+ playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.Id, s, 0))
+ return item != nil
+ }
+ if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, doc.Name), exists); serr != nil {
+ return nil, serr
+ }
+ if err := playletcategorymod.InsertOnePlayletCategory(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearAppPlayletHomeCache()
+ return &proto.PlayletCategoryAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlayletCategory 更新短剧分类
+func UpdatePlayletCategory(ctx context.Context, req *proto.PlayletCategoryUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "分类ID无效", "")
+ }
+ current, err := playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: req.Id,
+ })
+ if err != nil {
+ return err
+ }
+ setDoc := bson.M{playletcategorymod.FN.PlayletCategory.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "分类名称不能为空", "")
+ }
+ setDoc[playletcategorymod.FN.PlayletCategory.Name] = name
+ }
+ if req.Icon != nil {
+ setDoc[playletcategorymod.FN.PlayletCategory.Icon] = strings.TrimSpace(*req.Icon)
+ }
+ if req.Status != nil {
+ setDoc[playletcategorymod.FN.PlayletCategory.Status] = *req.Status
+ }
+ if req.Rank != nil {
+ setDoc[playletcategorymod.FN.PlayletCategory.Rank] = *req.Rank
+ }
+ if req.Remark != nil {
+ setDoc[playletcategorymod.FN.PlayletCategory.Remark] = strings.TrimSpace(*req.Remark)
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
+ }
+ name := current.Name
+ if req.Name != nil {
+ name = strings.TrimSpace(*req.Name)
+ }
+ keywords := current.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ exists := func(s string) bool {
+ item, _ := playletcategorymod.FindOnePlayletCategory(ctx, slugx.Filter(
+ playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.Id, s, req.Id))
+ return item != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ setDoc[playletcategorymod.FN.PlayletCategory.Slug] = slug
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ if err := playletcategorymod.UpdateOnePlayletCategory(ctx,
+ bson.M{playletcategorymod.FN.PlayletCategory.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// DelPlayletCategory 删除短剧分类,被短剧引用时禁止删除
+func DelPlayletCategory(ctx context.Context, id int64) *srvdef.SrvErr {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "分类ID无效", "")
+ }
+ usedCount, err := playletmod.CountPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.CategoryIds: bson.M{"$in": []int64{id}},
+ })
+ if err != nil {
+ return err
+ }
+ if usedCount > 0 {
+ return srvdef.New(srvdef.ErrParamError, "分类已被短剧引用,无法删除", "")
+ }
+ count, err := playletcategorymod.DelOnePlayletCategory(ctx, bson.M{playletcategorymod.FN.PlayletCategory.Id: id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
diff --git a/web/service/srv_playlet/helper.go b/web/service/srv_playlet/helper.go
new file mode 100644
index 0000000..54edb55
--- /dev/null
+++ b/web/service/srv_playlet/helper.go
@@ -0,0 +1,49 @@
+package srv_playlet
+
+import (
+ "strings"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/pubdef"
+)
+
+const (
+ defaultPageSize int64 = 10
+ maxPageSize int64 = 100
+)
+
+func normalizePage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultPageSize
+ }
+ if pageSize > maxPageSize {
+ pageSize = maxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// normalizeSeoValue 规范化 SEO 字段,避免 keywords 为 null
+func normalizeSeoValue(seo pubdef.SeoValue) pubdef.SeoValue {
+ seo.Title = strings.TrimSpace(seo.Title)
+ seo.Description = strings.TrimSpace(seo.Description)
+ if seo.Keyworld == nil {
+ seo.Keyworld = make([]string, 0)
+ return seo
+ }
+ words := make([]string, 0, len(seo.Keyworld))
+ for _, word := range seo.Keyworld {
+ word = strings.TrimSpace(word)
+ if word == "" {
+ continue
+ }
+ words = append(words, word)
+ }
+ seo.Keyworld = words
+ return seo
+}
+
+func clearAppPlayletHomeCache() {
+ playletcache.ClearHomeCache()
+}
diff --git a/web/service/srv_playlet/home_config.go b/web/service/srv_playlet/home_config.go
new file mode 100644
index 0000000..7d292b6
--- /dev/null
+++ b/web/service/srv_playlet/home_config.go
@@ -0,0 +1,651 @@
+package srv_playlet
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playlethomeconfigmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// ListPlayletHomeConfig 分页查询首页配置列表
+func ListPlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigListReq) (*proto.PlayletHomeConfigListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter, err := buildPlayletHomeConfigFilter(req)
+ if err != nil {
+ return nil, err
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playlethomeconfigmod.CountPlayletHomeConfig(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletHomeConfigListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletHomeConfigListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: playlethomeconfigmod.FN.PlayletHomeConfig.Rank, Value: -1},
+ {Key: playlethomeconfigmod.FN.PlayletHomeConfig.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletHomeConfigListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, playletHomeConfigToResp(item))
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// ListPlayletHomeTopicOptions 分页查询未被其他启用模块占用的题材或视频分类
+func ListPlayletHomeTopicOptions(ctx context.Context, req *proto.PlayletHomeTopicOptionsReq) (*proto.PlayletHomeTopicOptionsResp, *srvdef.SrvErr) {
+ if req == nil || (req.DataType != playlethomeconfigmod.DataTypeAll && req.DataType != playlethomeconfigmod.DataTypeVideo) {
+ return nil, srvdef.New(srvdef.ErrParamError, "数据类型仅支持1短剧题材、2视频分类", "")
+ }
+ if req.ConfigId < 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "配置ID无效", "")
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ resp := &proto.PlayletHomeTopicOptionsResp{
+ List: make([]*proto.PlayletHomeTopicOption, 0),
+ SelectedList: make([]*proto.PlayletHomeTopicOption, 0),
+ PageNum: pageNum,
+ PageSize: pageSize,
+ }
+ var selectedIDs []int64
+ if req.ConfigId > 0 {
+ current, err := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Id: req.ConfigId})
+ if err != nil {
+ return nil, err
+ }
+ if current.ModuleType != playlethomeconfigmod.ModuleTypeTopic || current.DataType != req.DataType {
+ return nil, srvdef.New(srvdef.ErrParamError, "当前配置不是指定数据类型的专题模块", "")
+ }
+ selectedIDs = current.TopicIds
+ }
+ // 已启用的其他专题模块占用的ID不再出现在可选列表中
+ configFilter := bson.M{
+ playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true,
+ playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType: playlethomeconfigmod.ModuleTypeTopic,
+ playlethomeconfigmod.FN.PlayletHomeConfig.DataType: req.DataType,
+ }
+ if req.ConfigId > 0 {
+ configFilter[playlethomeconfigmod.FN.PlayletHomeConfig.Id] = bson.M{"$ne": req.ConfigId}
+ }
+ configs, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, configFilter,
+ options.Find().SetProjection(bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.TopicIds: 1}))
+ if err != nil {
+ return nil, err
+ }
+ occupied := make(map[int64]struct{})
+ for _, config := range configs {
+ if config == nil {
+ continue
+ }
+ for _, id := range config.TopicIds {
+ occupied[id] = struct{}{}
+ }
+ }
+ skip := (pageNum - 1) * pageSize
+ if req.DataType == playlethomeconfigmod.DataTypeVideo {
+ filter := bson.M{videocategorymod.FN.VideoCategory.Status: true}
+ if len(occupied) > 0 {
+ ids := make([]int64, 0, len(occupied))
+ for id := range occupied {
+ ids = append(ids, id)
+ }
+ filter[videocategorymod.FN.VideoCategory.Id] = bson.M{"$nin": ids}
+ }
+ resp.TotalCount, err = videocategorymod.CountVideoCategorys(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ list, err := videocategorymod.FindManyVideoCategorys(ctx, filter, options.Find().
+ SetProjection(bson.M{videocategorymod.FN.VideoCategory.Id: 1, videocategorymod.FN.VideoCategory.Name: 1, videocategorymod.FN.VideoCategory.Status: 1}).
+ SetSort(bson.D{{Key: videocategorymod.FN.VideoCategory.Rank, Value: -1}, {Key: videocategorymod.FN.VideoCategory.Id, Value: -1}}).
+ SetSkip(skip).SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ if item != nil {
+ resp.List = append(resp.List, &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status})
+ }
+ }
+ if len(selectedIDs) > 0 {
+ selected, err := videocategorymod.FindManyVideoCategorys(ctx, bson.M{videocategorymod.FN.VideoCategory.Id: bson.M{"$in": selectedIDs}},
+ options.Find().SetProjection(bson.M{videocategorymod.FN.VideoCategory.Id: 1, videocategorymod.FN.VideoCategory.Name: 1, videocategorymod.FN.VideoCategory.Status: 1}))
+ if err != nil {
+ return nil, err
+ }
+ byID := make(map[int64]*proto.PlayletHomeTopicOption, len(selected))
+ for _, item := range selected {
+ if item != nil {
+ byID[item.Id] = &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status}
+ }
+ }
+ for _, id := range selectedIDs {
+ if item := byID[id]; item != nil {
+ resp.SelectedList = append(resp.SelectedList, item)
+ } else {
+ resp.SelectedList = append(resp.SelectedList, &proto.PlayletHomeTopicOption{Id: id})
+ }
+ }
+ }
+ return resp, nil
+ }
+ filter := bson.M{playletthememod.FN.PlayletTheme.Status: true}
+ if len(occupied) > 0 {
+ ids := make([]int64, 0, len(occupied))
+ for id := range occupied {
+ ids = append(ids, id)
+ }
+ filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$nin": ids}
+ }
+ resp.TotalCount, err = playletthememod.CountPlayletTheme(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ list, err := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
+ SetProjection(bson.M{playletthememod.FN.PlayletTheme.Id: 1, playletthememod.FN.PlayletTheme.Name: 1, playletthememod.FN.PlayletTheme.Status: 1}).
+ SetSort(bson.D{{Key: playletthememod.FN.PlayletTheme.Rank, Value: -1}, {Key: playletthememod.FN.PlayletTheme.Id, Value: -1}}).
+ SetSkip(skip).SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ if item != nil {
+ resp.List = append(resp.List, &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status})
+ }
+ }
+ if len(selectedIDs) > 0 {
+ selected, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{playletthememod.FN.PlayletTheme.Id: bson.M{"$in": selectedIDs}},
+ options.Find().SetProjection(bson.M{playletthememod.FN.PlayletTheme.Id: 1, playletthememod.FN.PlayletTheme.Name: 1, playletthememod.FN.PlayletTheme.Status: 1}))
+ if err != nil {
+ return nil, err
+ }
+ byID := make(map[int64]*proto.PlayletHomeTopicOption, len(selected))
+ for _, item := range selected {
+ if item != nil {
+ byID[item.Id] = &proto.PlayletHomeTopicOption{Id: item.Id, Name: item.Name, Status: item.Status}
+ }
+ }
+ for _, id := range selectedIDs {
+ if item := byID[id]; item != nil {
+ resp.SelectedList = append(resp.SelectedList, item)
+ } else {
+ resp.SelectedList = append(resp.SelectedList, &proto.PlayletHomeTopicOption{Id: id})
+ }
+ }
+ }
+ return resp, nil
+}
+
+// AddPlayletHomeConfig 新增首页配置
+func AddPlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigAddReq) (*proto.PlayletHomeConfigAddResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ title, style, moduleType, dataType, recommendType, limit, err := normalizePlayletHomeConfig(req.Title, req.Style, req.ModuleType, req.DataType, req.RecommendType, req.Limit)
+ if err != nil {
+ return nil, err
+ }
+ doc := &playlethomeconfigmod.PlayletHomeConfig{
+ Title: title,
+ Slug: req.Slug,
+ Desc: strings.TrimSpace(req.Desc),
+ Label: strings.TrimSpace(req.Label),
+ Style: style,
+ CardBackgroundImage: strings.TrimSpace(req.CardBackgroundImage),
+ ModuleType: moduleType,
+ DataType: dataType,
+ RecommendType: recommendType,
+ CategoryId: req.CategoryId,
+ TopicIds: req.TopicIds,
+ Rank: req.Rank,
+ Enabled: req.Enabled,
+ Limit: limit,
+ }
+ if err := validateHomeConfigTopics(ctx, doc); err != nil {
+ return nil, err
+ }
+ exists := func(s string) bool {
+ item, _ := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, slugx.Filter(
+ playlethomeconfigmod.FN.PlayletHomeConfig.Slug, playlethomeconfigmod.FN.PlayletHomeConfig.Id, s, 0))
+ return item != nil
+ }
+ if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, title), exists); serr != nil {
+ return nil, serr
+ }
+ if err := playlethomeconfigmod.InsertOnePlayletHomeConfig(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearAppPlayletHomeCache()
+ return &proto.PlayletHomeConfigAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlayletHomeConfig 更新首页配置
+func UpdatePlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ setDoc := bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.UpdatedAt: time.Now()}
+ current, err := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ if req.Title != nil {
+ title := strings.TrimSpace(*req.Title)
+ if title == "" {
+ return srvdef.New(srvdef.ErrParamError, "模块标题不能为空", "")
+ }
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Title] = title
+ }
+ if req.Desc != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Desc] = strings.TrimSpace(*req.Desc)
+ }
+ if req.Label != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Label] = strings.TrimSpace(*req.Label)
+ }
+ if req.Style != nil {
+ style, err := normalizePlayletHomeConfigStyle(*req.Style)
+ if err != nil {
+ return err
+ }
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Style] = style
+ }
+ if req.CardBackgroundImage != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.CardBackgroundImage] = strings.TrimSpace(*req.CardBackgroundImage)
+ }
+ if req.ModuleType != nil {
+ moduleType, err := normalizePlayletHomeModuleType(*req.ModuleType)
+ if err != nil {
+ return err
+ }
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType] = moduleType
+ }
+ if req.DataType != nil {
+ dataType, err := normalizePlayletHomeDataType(*req.DataType)
+ if err != nil {
+ return err
+ }
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.DataType] = dataType
+ }
+ if req.RecommendType != nil {
+ if err := validatePlayletHomeRecommendType(*req.RecommendType); err != nil {
+ return err
+ }
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.RecommendType] = *req.RecommendType
+ }
+ if req.CategoryId != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.CategoryId] = *req.CategoryId
+ }
+ if req.TopicIds != nil {
+ current.TopicIds = *req.TopicIds
+ }
+ if req.ModuleType != nil {
+ current.ModuleType = *req.ModuleType
+ }
+ if req.DataType != nil {
+ current.DataType = *req.DataType
+ }
+ if req.Enabled != nil {
+ current.Enabled = *req.Enabled
+ }
+ if req.TopicIds != nil || req.ModuleType != nil || req.DataType != nil || req.Enabled != nil {
+ if err := validateHomeConfigTopics(ctx, current); err != nil {
+ return err
+ }
+ if req.TopicIds != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.TopicIds] = current.TopicIds
+ }
+ }
+ if req.Rank != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Rank] = *req.Rank
+ }
+ if req.Enabled != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Enabled] = *req.Enabled
+ }
+ if req.Limit != nil {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Limit] = normalizePlayletHomeConfigLimit(*req.Limit)
+ }
+ title := current.Title
+ if req.Title != nil {
+ title = strings.TrimSpace(*req.Title)
+ }
+ exists := func(s string) bool {
+ item, _ := playlethomeconfigmod.FindOnePlayletHomeConfig(ctx, slugx.Filter(
+ playlethomeconfigmod.FN.PlayletHomeConfig.Slug, playlethomeconfigmod.FN.PlayletHomeConfig.Id, s, req.Id))
+ return item != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(nil, title), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ setDoc[playlethomeconfigmod.FN.PlayletHomeConfig.Slug] = slug
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ if err := playlethomeconfigmod.UpdateOnePlayletHomeConfig(ctx,
+ bson.M{playlethomeconfigmod.FN.PlayletHomeConfig.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// DelPlayletHomeConfig 删除首页配置
+func DelPlayletHomeConfig(ctx context.Context, req *proto.PlayletHomeConfigDelReq) *srvdef.SrvErr {
+ if req == nil || len(req.Ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "配置ID不能为空", "")
+ }
+ ids := make([]int64, 0, len(req.Ids))
+ seen := make(map[int64]struct{}, len(req.Ids))
+ for _, id := range req.Ids {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "配置ID无效", "")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ count, err := playlethomeconfigmod.DelManyPlayletHomeConfig(ctx, bson.M{
+ playlethomeconfigmod.FN.PlayletHomeConfig.Id: bson.M{"$in": ids},
+ })
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+func buildPlayletHomeConfigFilter(req *proto.PlayletHomeConfigListReq) (bson.M, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Id] = *req.Id
+ }
+ if slug := strings.TrimSpace(req.Slug); slug != "" {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Slug] = slug
+ }
+ if title := strings.TrimSpace(req.Title); title != "" {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
+ }
+ if desc := strings.TrimSpace(req.Desc); desc != "" {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Desc] = bson.M{"$regex": regexp.QuoteMeta(desc)}
+ }
+ if label := strings.TrimSpace(req.Label); label != "" {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Label] = bson.M{"$regex": regexp.QuoteMeta(label)}
+ }
+ if style := strings.TrimSpace(req.Style); style != "" {
+ normalizedStyle, err := normalizePlayletHomeConfigStyle(style)
+ if err != nil {
+ return nil, err
+ }
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Style] = normalizedStyle
+ }
+ if req.ModuleType != nil {
+ moduleType, err := normalizePlayletHomeModuleType(*req.ModuleType)
+ if err != nil {
+ return nil, err
+ }
+ if moduleType == playlethomeconfigmod.ModuleTypePlaylet {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType] = bson.M{"$in": []interface{}{nil, int64(0), playlethomeconfigmod.ModuleTypePlaylet}}
+ } else {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType] = moduleType
+ }
+ }
+ if req.DataType != nil {
+ dataType, err := normalizePlayletHomeDataType(*req.DataType)
+ if err != nil {
+ return nil, err
+ }
+ if dataType == playlethomeconfigmod.DataTypeAll {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.DataType] = bson.M{"$in": []interface{}{nil, int64(0), playlethomeconfigmod.DataTypeAll}}
+ } else {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.DataType] = dataType
+ }
+ }
+ if req.RecommendType != nil {
+ if err := validatePlayletHomeRecommendType(*req.RecommendType); err != nil {
+ return nil, err
+ }
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.RecommendType] = *req.RecommendType
+ }
+ if req.CategoryId != nil && *req.CategoryId > 0 {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.CategoryId] = *req.CategoryId
+ }
+ if req.Enabled != nil {
+ filter[playlethomeconfigmod.FN.PlayletHomeConfig.Enabled] = *req.Enabled
+ }
+ return filter, nil
+}
+
+func playletHomeConfigToResp(item *playlethomeconfigmod.PlayletHomeConfig) *proto.PlayletHomeConfigListResp {
+ if item == nil {
+ return nil
+ }
+ return &proto.PlayletHomeConfigListResp{
+ Id: item.Id,
+ Slug: item.Slug,
+ Title: item.Title,
+ Desc: item.Desc,
+ Label: item.Label,
+ Style: item.Style,
+ CardBackgroundImage: item.CardBackgroundImage,
+ ModuleType: normalizePlayletHomeModuleTypeForResp(item.ModuleType),
+ DataType: normalizePlayletHomeDataTypeForResp(item.DataType),
+ RecommendType: item.RecommendType,
+ CategoryId: item.CategoryId,
+ TopicIds: item.TopicIds,
+ Rank: item.Rank,
+ Enabled: item.Enabled,
+ Limit: item.Limit,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+// validateHomeConfigTopics 校验专题归属和已启用模块间的专题唯一性
+func validateHomeConfigTopics(ctx context.Context, doc *playlethomeconfigmod.PlayletHomeConfig) *srvdef.SrvErr {
+ if doc.ModuleType != playlethomeconfigmod.ModuleTypeTopic {
+ if len(doc.TopicIds) > 0 {
+ return srvdef.New(srvdef.ErrParamError, "列表模块不能配置专题ID", "")
+ }
+ return nil
+ }
+ seen := make(map[int64]struct{}, len(doc.TopicIds))
+ ids := make([]int64, 0, len(doc.TopicIds))
+ for _, id := range doc.TopicIds {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "专题ID必须大于0", "")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ doc.TopicIds = ids
+ if !doc.Enabled {
+ return nil
+ }
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "启用的专题模块必须配置专题ID", "")
+ }
+ if doc.DataType == playlethomeconfigmod.DataTypeVideo {
+ categories, err := videocategorymod.FindManyVideoCategorys(ctx, bson.M{
+ videocategorymod.FN.VideoCategory.Id: bson.M{"$in": ids},
+ videocategorymod.FN.VideoCategory.Status: true,
+ })
+ if err != nil {
+ return err
+ }
+ if len(categories) != len(ids) {
+ return srvdef.New(srvdef.ErrParamError, "视频分类不存在或未上架", "")
+ }
+ } else {
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids},
+ playletthememod.FN.PlayletTheme.Status: true,
+ })
+ if err != nil {
+ return err
+ }
+ if len(themes) != len(ids) {
+ return srvdef.New(srvdef.ErrParamError, "短剧题材不存在或未上架", "")
+ }
+ }
+ configs, err := playlethomeconfigmod.FindManyPlayletHomeConfig(ctx, bson.M{
+ playlethomeconfigmod.FN.PlayletHomeConfig.Enabled: true,
+ playlethomeconfigmod.FN.PlayletHomeConfig.ModuleType: playlethomeconfigmod.ModuleTypeTopic,
+ playlethomeconfigmod.FN.PlayletHomeConfig.DataType: doc.DataType,
+ playlethomeconfigmod.FN.PlayletHomeConfig.TopicIds: bson.M{"$in": ids},
+ })
+ if err != nil {
+ return err
+ }
+ for _, other := range configs {
+ if other != nil && other.Id != doc.Id {
+ return srvdef.New(srvdef.ErrParamError, "专题已分配给其他启用的首页模块", "")
+ }
+ }
+ return nil
+}
+
+func normalizePlayletHomeConfig(title string, style string, moduleType int64, dataType int64, recommendType int64, limit int64) (string, string, int64, int64, int64, int64, *srvdef.SrvErr) {
+ title = strings.TrimSpace(title)
+ if title == "" {
+ return "", "", 0, 0, 0, 0, srvdef.New(srvdef.ErrParamError, "模块标题不能为空", "")
+ }
+ normalizedStyle, err := normalizePlayletHomeConfigStyle(style)
+ if err != nil {
+ return "", "", 0, 0, 0, 0, err
+ }
+ normalizedModuleType, err := normalizePlayletHomeModuleType(moduleType)
+ if err != nil {
+ return "", "", 0, 0, 0, 0, err
+ }
+ normalizedDataType, err := normalizePlayletHomeDataType(dataType)
+ if err != nil {
+ return "", "", 0, 0, 0, 0, err
+ }
+ if err := validatePlayletHomeRecommendType(recommendType); err != nil {
+ return "", "", 0, 0, 0, 0, err
+ }
+ return title, normalizedStyle, normalizedModuleType, normalizedDataType, recommendType, normalizePlayletHomeConfigLimit(limit), nil
+}
+
+func normalizePlayletHomeConfigStyle(style string) (string, *srvdef.SrvErr) {
+ style = strings.ToUpper(strings.TrimSpace(style))
+ switch style {
+ case playlethomeconfigmod.StyleA,
+ playlethomeconfigmod.StyleB,
+ playlethomeconfigmod.StyleC,
+ playlethomeconfigmod.StyleD,
+ playlethomeconfigmod.StyleE,
+ playlethomeconfigmod.StyleF,
+ playlethomeconfigmod.StyleG,
+ playlethomeconfigmod.StyleH:
+ return style, nil
+ default:
+ return "", srvdef.New(srvdef.ErrParamError, "样式无效,仅支持A-H", "")
+ }
+}
+
+func validatePlayletHomeRecommendType(recommendType int64) *srvdef.SrvErr {
+ switch recommendType {
+ case playlethomeconfigmod.RecommendTypeUpdated,
+ playlethomeconfigmod.RecommendTypeLike,
+ playlethomeconfigmod.RecommendTypeBrowse,
+ playlethomeconfigmod.RecommendTypeHeat,
+ playlethomeconfigmod.RecommendTypeCategory:
+ return nil
+ default:
+ return srvdef.New(srvdef.ErrParamError, "推荐类型无效,仅支持1按更新、2按点赞、3按观看、4按热度、5按分类", "")
+ }
+}
+
+func normalizePlayletHomeModuleType(moduleType int64) (int64, *srvdef.SrvErr) {
+ if moduleType == 0 {
+ return playlethomeconfigmod.ModuleTypePlaylet, nil
+ }
+ switch moduleType {
+ case playlethomeconfigmod.ModuleTypePlaylet, playlethomeconfigmod.ModuleTypeTopic:
+ return moduleType, nil
+ default:
+ return 0, srvdef.New(srvdef.ErrParamError, "模块类型无效,仅支持1短剧列表、2专题", "")
+ }
+}
+
+func normalizePlayletHomeModuleTypeForResp(moduleType int64) int64 {
+ if moduleType == playlethomeconfigmod.ModuleTypeTopic {
+ return playlethomeconfigmod.ModuleTypeTopic
+ }
+ return playlethomeconfigmod.ModuleTypePlaylet
+}
+
+func normalizePlayletHomeDataType(dataType int64) (int64, *srvdef.SrvErr) {
+ if dataType == 0 {
+ return playlethomeconfigmod.DataTypeAll, nil
+ }
+ switch dataType {
+ case playlethomeconfigmod.DataTypeAll, playlethomeconfigmod.DataTypeVideo:
+ return dataType, nil
+ default:
+ return 0, srvdef.New(srvdef.ErrParamError, "数据类型无效,仅支持1全部上架短剧、2全部上架视频", "")
+ }
+}
+
+func normalizePlayletHomeDataTypeForResp(dataType int64) int64 {
+ if dataType == playlethomeconfigmod.DataTypeVideo {
+ return playlethomeconfigmod.DataTypeVideo
+ }
+ return playlethomeconfigmod.DataTypeAll
+}
+
+func normalizePlayletHomeConfigLimit(limit int64) int64 {
+ if limit <= 0 {
+ return playlethomeconfigmod.DefaultLimit
+ }
+ if limit > playlethomeconfigmod.MaxLimit {
+ return playlethomeconfigmod.MaxLimit
+ }
+ return limit
+}
diff --git a/web/service/srv_playlet/playlet.go b/web/service/srv_playlet/playlet.go
new file mode 100644
index 0000000..7b5df0b
--- /dev/null
+++ b/web/service/srv_playlet/playlet.go
@@ -0,0 +1,1813 @@
+package srv_playlet
+
+import (
+ "context"
+ "math/rand"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletfavoritemod"
+ "videoweb/pub/models/playletlikemod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_contenttag"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/service/srv_search"
+ "videoweb/web/service/srv_seopush"
+ "videoweb/web/skd/playletwatermark"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var playletWebProjection = 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.IsOriginal, Value: 1},
+ {Key: playletmod.FN.Playlet.ShowOriginal, Value: 1},
+ {Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
+ {Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1},
+ {Key: playletmod.FN.Playlet.Banner, Value: 1},
+ {Key: playletmod.FN.Playlet.ReviewAt, Value: 1},
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, 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.Preview, Value: 1},
+ {Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
+ {Key: playletmod.FN.Playlet.CategoryBinds, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.ContentType, Value: 1},
+ {Key: playletmod.FN.Playlet.PayType, Value: 1},
+ {Key: playletmod.FN.Playlet.SingleCoins, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.FreeEpisodeCount, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.TopRank, Value: 1},
+ {Key: playletmod.FN.Playlet.RecommendRank, Value: 1},
+ {Key: playletmod.FN.Playlet.CountComment, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBuy, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualHeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.CountDisLike, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Description, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Keyworld, Value: 1},
+ {Key: playletmod.FN.Playlet.OperatorId, Value: 1},
+ {Key: playletmod.FN.Playlet.OperatorName, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+}
+
+// ListPlaylet 分页查询短剧列表
+func ListPlaylet(ctx context.Context, req *proto.PlayletListReq) (*proto.PlayletListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := buildPlayletListFilter(req)
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ var list playletmod.PlayletList
+ if req.CategoryId != nil && *req.CategoryId > 0 {
+ list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, *req.CategoryId, (pageNum-1)*pageSize, pageSize, playletmod.CategoryRankAdminSort(), playletWebProjection)
+ } else {
+ list, err = playletmod.FindManyPlaylet(ctx, filter, options.Find().
+ SetProjection(playletWebProjection).
+ SetSort(bson.D{
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ }
+ if err != nil {
+ return nil, err
+ }
+ categoryNameMap, err := buildCategoryNameMap(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildThemeNameMap(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ tagNameMap, err := buildTagNameMap(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ webItem := playletToWebResp(item, categoryNameMap, themeNameMap, tagNameMap)
+ if req.CategoryId != nil {
+ webItem.CategoryRank = playletmod.CategoryRankOf(playletmod.EffectiveCategoryBinds(item), *req.CategoryId)
+ }
+ items = append(items, webItem)
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlaylet 新增短剧
+func AddPlaylet(ctx context.Context, req *proto.PlayletAddReq, operatorId int64) (*proto.PlayletAddResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ title := strings.TrimSpace(req.Title)
+ if title == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧标题不能为空", "")
+ }
+ categoryBinds, err := resolveAddCategoryBinds(req.CategoryBinds, req.CategoryIds)
+ if err != nil {
+ return nil, err
+ }
+ categoryIds := playletmod.CategoryIdsFromBinds(categoryBinds)
+ if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil {
+ return nil, err
+ }
+ themeIds := dedupePositiveInt64Ids(req.ThemeIds)
+ if len(themeIds) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "")
+ }
+ if err := validatePlayletThemeIds(ctx, themeIds); err != nil {
+ return nil, err
+ }
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(req.TagIds)
+ if err != nil {
+ return nil, err
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil {
+ return nil, err
+ }
+ if !isValidPlayletStatus(req.Status) {
+ return nil, srvdef.New(srvdef.ErrParamError, "上下架状态无效", "")
+ }
+ if !isValidPlayletContentType(req.ContentType) {
+ return nil, srvdef.New(srvdef.ErrParamError, "内容类型无效", "")
+ }
+ payType, singleCoins, err := normalizePlayletPaySetting(req.PayType, &req.SingleCoins, true)
+ if err != nil {
+ return nil, err
+ }
+ freeEpisodeCount := int64(2)
+ if req.FreeEpisodeCount != nil {
+ if !validOfficialFreeEpisodeCount(*req.FreeEpisodeCount) {
+ return nil, srvdef.New(srvdef.ErrParamError, "官网开放集数无效", "")
+ }
+ freeEpisodeCount = *req.FreeEpisodeCount
+ }
+ banner := strings.TrimSpace(req.Banner)
+ if req.ShowOnBanner && banner == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "显示首页banner时banner图不能为空", "")
+ }
+ now := time.Now()
+ operatorId, operatorName := resolvePlayletOperator(ctx, operatorId)
+ doc := &playletmod.Playlet{
+ Status: req.Status,
+ IsFeatured: req.IsFeatured,
+ IsOriginal: true, // 管理后台新增的短剧都是原创
+ ShowOriginal: req.ShowOriginal,
+ IsRecommend: req.IsRecommend,
+ ShowOnBanner: req.ShowOnBanner,
+ Banner: banner,
+ ReviewAt: now,
+ OnlineAt: playletOnlineAt(req.Status, now),
+ CategoryIds: categoryIds,
+ CategoryBinds: categoryBinds,
+ ThemeIds: themeIds,
+ Title: title,
+ Slug: req.Slug,
+ Desc: strings.TrimSpace(req.Desc),
+ Anchor: strings.TrimSpace(req.Anchor),
+ Cover: strings.TrimSpace(req.Cover),
+ Preview: strings.TrimSpace(req.Preview),
+ TagIds: tagIds,
+ ContentType: req.ContentType,
+ PayType: payType,
+ SingleCoins: singleCoins,
+ TotalEpisode: req.TotalEpisode,
+ FreeEpisodeCount: freeEpisodeCount,
+ TopRank: req.TopRank,
+ RecommendRank: req.RecommendRank,
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ OperatorId: operatorId,
+ OperatorName: operatorName,
+ }
+ exists := func(s string) bool {
+ item, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, 0))
+ return item != nil
+ }
+ if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, title), exists); serr != nil {
+ return nil, serr
+ }
+ if err := playletmod.InsertOnePlaylet(ctx, doc); err != nil {
+ return nil, err
+ }
+ if err := srv_contenttag.SyncContentTagBindCount(ctx, nil, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil {
+ return nil, err
+ }
+ go srv_mediaresource.FillPlayletCompressedCover(context.Background(), doc)
+ clearAppPlayletHomeCache()
+ if req.ShowOriginal {
+ playletcache.ClearRankCache()
+ }
+ srv_seopush.NotifyPlayletAfterWrite(ctx, doc.Id, nil, doc.Status, doc.OnlineAt)
+ if doc.Status == playletmod.StatusUp {
+ asyncSyncPlayletESByStatus([]int64{doc.Id}, doc.Status, "短剧新增")
+ }
+ return &proto.PlayletAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlaylet 更新短剧
+func UpdatePlaylet(ctx context.Context, req *proto.PlayletUpdateReq, operatorId int64) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID无效", "")
+ }
+ current, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: req.Id,
+ })
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ setDoc := bson.M{playletmod.FN.Playlet.UpdatedAt: now}
+ if req.Status != nil {
+ if !isValidPlayletStatus(*req.Status) {
+ return srvdef.New(srvdef.ErrParamError, "上下架状态无效", "")
+ }
+ setDoc[playletmod.FN.Playlet.Status] = *req.Status
+ setDoc[playletmod.FN.Playlet.ReviewAt] = now
+ if *req.Status == playletmod.StatusUp {
+ setDoc[playletmod.FN.Playlet.OnlineAt] = now
+ setDoc[playletmod.FN.Playlet.IndexNow] = int64(0)
+ }
+ }
+ if req.FreeEpisodeCount != nil {
+ if !validOfficialFreeEpisodeCount(*req.FreeEpisodeCount) {
+ return srvdef.New(srvdef.ErrParamError, "官网开放集数无效", "")
+ }
+ setDoc[playletmod.FN.Playlet.FreeEpisodeCount] = *req.FreeEpisodeCount
+ }
+ if req.IsFeatured != nil {
+ setDoc[playletmod.FN.Playlet.IsFeatured] = *req.IsFeatured
+ }
+ if req.ShowOriginal != nil {
+ setDoc[playletmod.FN.Playlet.ShowOriginal] = *req.ShowOriginal
+ }
+ if req.IsRecommend != nil {
+ setDoc[playletmod.FN.Playlet.IsRecommend] = *req.IsRecommend
+ }
+ if req.ShowOnBanner != nil {
+ setDoc[playletmod.FN.Playlet.ShowOnBanner] = *req.ShowOnBanner
+ }
+ if req.Banner != nil {
+ setDoc[playletmod.FN.Playlet.Banner] = strings.TrimSpace(*req.Banner)
+ }
+ if req.CategoryBinds != nil || req.CategoryIds != nil {
+ categoryBinds, err := resolveCategoryBinds(req.CategoryBinds, req.CategoryIds, playletmod.EffectiveCategoryBinds(current))
+ if err != nil {
+ return err
+ }
+ categoryIds := playletmod.CategoryIdsFromBinds(categoryBinds)
+ if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil {
+ return err
+ }
+ setDoc[playletmod.FN.Playlet.CategoryIds] = categoryIds
+ setDoc[playletmod.FN.Playlet.CategoryBinds] = categoryBinds
+ }
+ if req.ThemeIds != nil {
+ themeIds := dedupePositiveInt64Ids(*req.ThemeIds)
+ if len(themeIds) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "")
+ }
+ if err := validatePlayletThemeIds(ctx, themeIds); err != nil {
+ return err
+ }
+ setDoc[playletmod.FN.Playlet.ThemeIds] = themeIds
+ }
+ if req.Title != nil {
+ title := strings.TrimSpace(*req.Title)
+ if title == "" {
+ return srvdef.New(srvdef.ErrParamError, "短剧标题不能为空", "")
+ }
+ setDoc[playletmod.FN.Playlet.Title] = title
+ }
+ if req.Desc != nil {
+ setDoc[playletmod.FN.Playlet.Desc] = strings.TrimSpace(*req.Desc)
+ }
+ if req.Anchor != nil {
+ setDoc[playletmod.FN.Playlet.Anchor] = strings.TrimSpace(*req.Anchor)
+ }
+ coverChanged := false
+ if req.Cover != nil {
+ newCover := strings.TrimSpace(*req.Cover)
+ setDoc[playletmod.FN.Playlet.Cover] = newCover
+ if strings.TrimSpace(current.Cover) != newCover {
+ setDoc[playletmod.FN.Playlet.CompressedCover] = ""
+ coverChanged = true
+ }
+ }
+ if req.Preview != nil {
+ setDoc[playletmod.FN.Playlet.Preview] = strings.TrimSpace(*req.Preview)
+ }
+ var oldTagIds []int64
+ var newTagIds []int64
+ if req.TagIds != nil {
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds)
+ if err != nil {
+ return err
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil {
+ return err
+ }
+ oldTagIds = append([]int64{}, current.TagIds...)
+ newTagIds = tagIds
+ setDoc[playletmod.FN.Playlet.TagIds] = tagIds
+ }
+ if req.ContentType != nil {
+ if !isValidPlayletContentType(*req.ContentType) {
+ return srvdef.New(srvdef.ErrParamError, "内容类型无效", "")
+ }
+ setDoc[playletmod.FN.Playlet.ContentType] = *req.ContentType
+ }
+ if req.PayType != nil {
+ payType, singleCoins, err := normalizePlayletPaySetting(*req.PayType, req.SingleCoins, true)
+ if err != nil {
+ return err
+ }
+ setDoc[playletmod.FN.Playlet.PayType] = payType
+ setDoc[playletmod.FN.Playlet.SingleCoins] = singleCoins
+ } else if req.SingleCoins != nil {
+ if *req.SingleCoins < 0 {
+ return srvdef.New(srvdef.ErrParamError, "单集金币不能小于0", "")
+ }
+ setDoc[playletmod.FN.Playlet.SingleCoins] = *req.SingleCoins
+ }
+ if req.TotalEpisode != nil {
+ setDoc[playletmod.FN.Playlet.TotalEpisode] = *req.TotalEpisode
+ }
+ if req.TopRank != nil {
+ setDoc[playletmod.FN.Playlet.TopRank] = *req.TopRank
+ }
+ if req.RecommendRank != nil {
+ setDoc[playletmod.FN.Playlet.RecommendRank] = *req.RecommendRank
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
+ }
+ title := current.Title
+ if req.Title != nil {
+ title = strings.TrimSpace(*req.Title)
+ }
+ keywords := current.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ exists := func(s string) bool {
+ item, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, req.Id))
+ return item != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, title), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ setDoc[playletmod.FN.Playlet.Slug] = slug
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ operatorId, operatorName := resolvePlayletOperator(ctx, operatorId)
+ setDoc[playletmod.FN.Playlet.OperatorId] = operatorId
+ setDoc[playletmod.FN.Playlet.OperatorName] = operatorName
+ if err := playletmod.UpdateOnePlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ if req.TagIds != nil {
+ if err := srv_contenttag.SyncContentTagBindCount(ctx, oldTagIds, newTagIds, contenttagmod.ContentTagTypePlaylet); err != nil {
+ return err
+ }
+ }
+ if coverChanged {
+ go func(playletId int64, cover string) {
+ srv_mediaresource.FillPlayletCompressedCover(context.Background(), &playletmod.Playlet{
+ Id: playletId,
+ Cover: cover,
+ })
+ }(req.Id, strings.TrimSpace(*req.Cover))
+ }
+ clearAppPlayletHomeCache()
+ if req.ShowOriginal != nil {
+ playletcache.ClearRankCache()
+ }
+ status := current.Status
+ onlineAt := current.OnlineAt
+ if req.Status != nil {
+ status = *req.Status
+ if status == playletmod.StatusUp {
+ onlineAt = now
+ }
+ }
+ prevStatus := current.Status
+ srv_seopush.NotifyPlayletAfterWrite(ctx, req.Id, &prevStatus, status, onlineAt)
+ asyncSyncPlayletESByLatestStatus([]int64{req.Id}, "短剧更新")
+ return nil
+}
+
+// DelPlaylet 删除短剧、剧集及关联的用户数据
+func DelPlaylet(ctx context.Context, req *proto.PlayletDelReq) *srvdef.SrvErr {
+ if req == nil || len(req.Ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ for _, id := range req.Ids {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID无效", "")
+ }
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ playletFilter := bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}}
+ chapterFilter := bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": ids}}
+ var serviceErr *srvdef.SrvErr
+ var chapterIDs []int64
+ txnErr := mongos.Shop.Transaction(ctx, func(sctx mongo.SessionContext) error {
+ serviceErr = nil
+ playlets, err := playletmod.FindManyPlaylet(sctx, playletFilter, options.Find().SetProjection(bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.TagIds: 1,
+ }))
+ if err != nil {
+ serviceErr = err
+ return err
+ }
+ changes := make([]srv_contenttag.ContentTagBindChange, 0, len(playlets))
+ for _, playlet := range playlets {
+ if playlet != nil && len(playlet.TagIds) > 0 {
+ changes = append(changes, srv_contenttag.ContentTagBindChange{OldTagIds: playlet.TagIds})
+ }
+ }
+ if len(changes) > 0 {
+ if err := srv_contenttag.SyncContentTagBindCountBatch(sctx, changes, contenttagmod.ContentTagTypePlaylet); err != nil {
+ serviceErr = err
+ return err
+ }
+ }
+ chapters, err := playletchaptermod.FindManyPlayletChapter(sctx, chapterFilter, options.Find().SetProjection(bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: 1,
+ }))
+ if err != nil {
+ serviceErr = err
+ return err
+ }
+ commentFilters := bson.A{bson.M{
+ commentmod.FN.Comment.ObjectType: pubdef.CommentObjectTypePlaylet,
+ commentmod.FN.Comment.ObjectId: bson.M{"$in": ids},
+ }}
+ chapterIDs = make([]int64, 0, len(chapters))
+ for _, chapter := range chapters {
+ if chapter != nil {
+ chapterIDs = append(chapterIDs, chapter.Id)
+ }
+ }
+ if len(chapterIDs) > 0 {
+ commentFilters = append(commentFilters, bson.M{
+ commentmod.FN.Comment.ObjectType: pubdef.CommentObjectTypePlayletChapter,
+ commentmod.FN.Comment.ObjectId: bson.M{"$in": chapterIDs},
+ })
+ }
+ commentFilter := bson.M{"$or": commentFilters}
+ comments, err := commentmod.FindManyComment(sctx, commentFilter, options.Find().SetProjection(bson.M{
+ commentmod.FN.Comment.Id: 1,
+ }))
+ if err != nil {
+ serviceErr = err
+ return err
+ }
+ commentIDs := make([]int64, 0, len(comments))
+ for _, comment := range comments {
+ if comment != nil {
+ commentIDs = append(commentIDs, comment.Id)
+ }
+ }
+ if err := commentlikemod.DeleteByCommentIds(sctx, commentIDs); err != nil {
+ serviceErr = err
+ return err
+ }
+ deletions := []struct {
+ collection string
+ filter bson.M
+ }{
+ {commentmod.TN.Comment, commentFilter},
+ {playletlikemod.TN.PlayletLike, bson.M{playletlikemod.FN.PlayletLike.PlayletID: bson.M{"$in": ids}}},
+ {playletfavoritemod.CollectionName, bson.M{playletfavoritemod.FieldPlayletID: bson.M{"$in": ids}}},
+ {models.WatchHistoryCollectionName, bson.M{models.WatchHistoryDataType: models.WatchHistoryTypePlaylet, models.WatchHistoryContentID: bson.M{"$in": ids}}},
+ {playletchaptermod.TN.PlayletChapter, chapterFilter},
+ {playletmod.TN.Playlet, playletFilter},
+ }
+ for _, deletion := range deletions {
+ if _, err := mongos.Shop.Coll(deletion.collection).DeleteMany(sctx, deletion.filter); err != nil {
+ log.Warn("删除短剧关联数据失败", log.Any("collection", deletion.collection), log.E(err))
+ serviceErr = srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ return err
+ }
+ }
+ return nil
+ })
+ if txnErr != nil {
+ if serviceErr != nil {
+ return serviceErr
+ }
+ log.Warn("删除短剧事务失败", log.E(txnErr))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ clearAppPlayletHomeCache()
+ playletcache.ClearRankCache()
+ if redis.Client() != nil {
+ for _, id := range ids {
+ if err := redis.Del(redisconst.CommentListCacheKey(id, pubdef.CommentObjectTypePlaylet)); err != nil {
+ log.Warn("清理短剧评论缓存失败", log.Any("playletId", id), log.E(err))
+ }
+ }
+ for _, id := range chapterIDs {
+ if err := redis.Del(redisconst.CommentListCacheKey(id, pubdef.CommentObjectTypePlayletChapter)); err != nil {
+ log.Warn("清理剧集评论缓存失败", log.Any("chapterId", id), log.E(err))
+ }
+ }
+ }
+ srv_seopush.MarkSitemapDirty(ctx)
+ for _, id := range ids {
+ srv_seopush.EnqueueDeleted(ctx, srv_seopush.KindPlaylet, id)
+ }
+ if err := srv_search.SyncPlayletStatusToES(ctx, ids, playletmod.StatusDown); err != nil {
+ return err
+ }
+ return nil
+}
+
+// SetPlayletStatus 单/批量设置短剧上下架
+func SetPlayletStatus(ctx context.Context, req *proto.PlayletStatusSetReq) *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 !isValidPlayletStatus(req.Status) {
+ return srvdef.New(srvdef.ErrParamError, "上下架状态无效", "")
+ }
+ now := time.Now()
+ setDoc := bson.M{
+ playletmod.FN.Playlet.Status: req.Status,
+ playletmod.FN.Playlet.ReviewAt: now,
+ playletmod.FN.Playlet.UpdatedAt: now,
+ }
+ if req.Status == playletmod.StatusUp {
+ setDoc[playletmod.FN.Playlet.OnlineAt] = now
+ setDoc[playletmod.FN.Playlet.IndexNow] = int64(0)
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ srv_seopush.NotifyPlayletAfterStatus(ctx, ids, req.Status, now)
+ asyncSyncPlayletESByStatus(ids, req.Status, "短剧上下架")
+ return nil
+}
+
+// SetPlayletRecommend 单/批量设置短剧是否推荐
+func SetPlayletRecommend(ctx context.Context, req *proto.PlayletRecommendSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.IsRecommend: req.IsRecommend,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧推荐状态变更")
+ return nil
+}
+
+// SetPlayletFeatured 单/批量设置短剧是否爆剧
+func SetPlayletFeatured(ctx context.Context, req *proto.PlayletFeaturedSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.IsFeatured: req.IsFeatured,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧爆剧状态变更")
+ return nil
+}
+
+// SetPlayletBanner 单/批量设置短剧是否显示在首页banner
+func SetPlayletBanner(ctx context.Context, req *proto.PlayletBannerSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.ShowOnBanner: req.ShowOnBanner,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧banner状态变更")
+ return nil
+}
+
+// SetPlayletTag 单/批量设置短剧标签
+func SetPlayletTag(ctx context.Context, req *proto.PlayletTagSetReq) *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 req.TagIds == nil {
+ return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
+ }
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds)
+ if err != nil {
+ return err
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet); err != nil {
+ return err
+ }
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ }))
+ if err != nil {
+ return err
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.TagIds: tagIds,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ changes = append(changes, srv_contenttag.ContentTagBindChange{
+ OldTagIds: append([]int64{}, item.TagIds...),
+ NewTagIds: append([]int64{}, tagIds...),
+ })
+ }
+ if err := srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypePlaylet); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧标签变更")
+ return nil
+}
+
+// SetPlayletCategory 单/批量设置短剧分类
+func SetPlayletCategory(ctx context.Context, req *proto.PlayletCategorySetReq) *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 req.CategoryIds == nil {
+ return srvdef.New(srvdef.ErrParamError, "分类ID不能为空", "")
+ }
+ categoryIds, err := normalizePlayletCategoryIds(*req.CategoryIds)
+ if err != nil {
+ return err
+ }
+ if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil {
+ return err
+ }
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}})
+ if err != nil {
+ return err
+ }
+ for _, item := range list {
+ binds := playletmod.MergeCategoryBindsByIds(playletmod.EffectiveCategoryBinds(item), categoryIds)
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: item.Id}, bson.M{"$set": bson.M{
+ playletmod.FN.Playlet.CategoryIds: categoryIds,
+ playletmod.FN.Playlet.CategoryBinds: binds,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }}); err != nil {
+ return err
+ }
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧分类变更")
+ return nil
+}
+
+// SetPlayletTheme 单/批量设置短剧题材
+func SetPlayletTheme(ctx context.Context, req *proto.PlayletThemeSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ themeIds := dedupePositiveInt64Ids(req.ThemeIds)
+ if len(themeIds) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "")
+ }
+ if err := validatePlayletThemeIds(ctx, themeIds); err != nil {
+ return err
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.ThemeIds: themeIds,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧题材变更")
+ return nil
+}
+
+// SetPlayletVirtualLike 单/批量设置短剧虚拟点赞数
+func SetPlayletVirtualLike(ctx context.Context, req *proto.PlayletVirtualLikeSetReq) *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 req.MinVirtualCountLike < 0 || req.MaxVirtualCountLike < req.MinVirtualCountLike {
+ return srvdef.New(srvdef.ErrParamError, "虚拟点赞数范围无效", "")
+ }
+ if req.MaxVirtualCountLike-req.MinVirtualCountLike < int64(len(ids))-1 {
+ return srvdef.New(srvdef.ErrParamError, "虚拟点赞数范围不足,无法保证每条短剧数值不同", "")
+ }
+ values := randomUniqueVirtualLikeValues(req.MinVirtualCountLike, req.MaxVirtualCountLike, len(ids))
+ now := time.Now()
+ docs := make([]mongo.WriteModel, 0, len(ids))
+ for index, id := range ids {
+ setDoc := bson.M{
+ playletmod.FN.Playlet.VirtualCountLike: values[index],
+ playletmod.FN.Playlet.UpdatedAt: now,
+ }
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{playletmod.FN.Playlet.Id: id}).
+ SetUpdate(bson.M{"$set": setDoc}))
+ }
+ if err := playletmod.BulkWritePlaylet(ctx, docs); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ asyncSyncPlayletESByLatestStatus(ids, "短剧虚拟点赞数变更")
+ return nil
+}
+
+// SetPlayletVirtualBrowse 单/批量设置短剧虚拟观看数
+func SetPlayletVirtualBrowse(ctx context.Context, req *proto.PlayletVirtualBrowseSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ return setPlayletVirtualMetric(ctx, req.Ids, req.MinVirtualCountBrowse, req.MaxVirtualCountBrowse,
+ playletmod.FN.Playlet.VirtualCountBrowse, "短剧虚拟观看数变更")
+}
+
+// SetPlayletVirtualHeat 单/批量设置短剧虚拟热度
+func SetPlayletVirtualHeat(ctx context.Context, req *proto.PlayletVirtualHeatSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ return setPlayletVirtualMetric(ctx, req.Ids, req.MinVirtualHeatValue, req.MaxVirtualHeatValue,
+ playletmod.FN.Playlet.VirtualHeatValue, "短剧虚拟热度变更")
+}
+
+// setPlayletVirtualMetric 按区间为每部短剧设置不同虚拟数值
+func setPlayletVirtualMetric(ctx context.Context, rawIds []int64, min, max int64, field, reason string) *srvdef.SrvErr {
+ ids := dedupePositiveInt64Ids(rawIds)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ if min < 0 || max < min || max-min < int64(len(ids))-1 {
+ return srvdef.New(srvdef.ErrParamError, "虚拟数值区间无效或不足", "")
+ }
+ values := randomUniqueVirtualLikeValues(min, max, len(ids))
+ now := time.Now()
+ docs := make([]mongo.WriteModel, 0, len(ids))
+ for index, id := range ids {
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{playletmod.FN.Playlet.Id: id}).
+ SetUpdate(bson.M{"$set": bson.M{field: values[index], playletmod.FN.Playlet.UpdatedAt: now}}))
+ }
+ if err := playletmod.BulkWritePlaylet(ctx, docs); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ playletcache.ClearRankCache()
+ asyncSyncPlayletESByLatestStatus(ids, reason)
+ return nil
+}
+
+// SetPlayletPayType 单/批量设置短剧默认支付方式
+func SetPlayletPayType(ctx context.Context, req *proto.PlayletPayTypeSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ payType, singleCoins, err := normalizePlayletPaySetting(req.PayType, req.SingleCoins, true)
+ if err != nil {
+ return err
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.PayType: payType,
+ playletmod.FN.Playlet.SingleCoins: singleCoins,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// ResetPlayletWatermark 按短剧ID批量重置其下全部剧集水印,等待定时任务重新加水印。
+// 已有水印地址不清理,新水印完成回写时直接覆盖。
+// 原创短剧保留 mediaSourceId / videoUrl / mediaTaskId,直接进入已登记后按原片重跑水印。
+// 非原创清空 media 关联并回到未处理,由 SyncMediaFile 重新登记。
+func ResetPlayletWatermark(ctx context.Context, req *proto.PlayletWatermarkResetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ originalIds, otherIds, err := splitPlayletIdsByOriginal(ctx, ids)
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ if resetErr := resetOriginalChaptersWatermark(ctx, originalIds, now); resetErr != nil {
+ return resetErr
+ }
+ return resetChaptersWatermark(ctx, otherIds, playletchaptermod.WatermarkStatusPending, now)
+}
+
+func splitPlayletIdsByOriginal(ctx context.Context, ids []int64) (originalIds, otherIds []int64, err *srvdef.SrvErr) {
+ originalList, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": ids},
+ playletmod.FN.Playlet.IsOriginal: true,
+ }, options.Find().SetProjection(bson.M{playletmod.FN.Playlet.Id: 1}))
+ if err != nil {
+ return nil, nil, err
+ }
+ originalSet := make(map[int64]struct{}, len(originalList))
+ for _, item := range originalList {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ originalSet[item.Id] = struct{}{}
+ originalIds = append(originalIds, item.Id)
+ }
+ for _, id := range ids {
+ if _, ok := originalSet[id]; !ok {
+ otherIds = append(otherIds, id)
+ }
+ }
+ return originalIds, otherIds, nil
+}
+
+func resetOriginalChaptersWatermark(ctx context.Context, playletIds []int64, now time.Time) *srvdef.SrvErr {
+ if len(playletIds) == 0 {
+ return nil
+ }
+ // 原创剧集保留 mediaSourceId / videoUrl / mediaTaskId,以及已有水印地址。
+ // 新水印完成回写时会直接覆盖 WatermarkVideoUrl。
+ err := playletchaptermod.UpdateManyPlayletChapter(ctx,
+ bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": playletIds}},
+ bson.M{"$set": bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusRegistered,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: now,
+ }},
+ )
+ if err != nil && err.Code == srvdef.ErrNoData {
+ return nil
+ }
+ return err
+}
+
+func resetChaptersWatermark(ctx context.Context, playletIds []int64, status playletchaptermod.WatermarkStatus, now time.Time) *srvdef.SrvErr {
+ if len(playletIds) == 0 {
+ return nil
+ }
+ err := playletchaptermod.UpdateManyPlayletChapter(ctx,
+ bson.M{playletchaptermod.FN.PlayletChapter.PlayletId: bson.M{"$in": playletIds}},
+ bson.M{"$set": bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: status,
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: "",
+ playletchaptermod.FN.PlayletChapter.MediaTaskId: "",
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: now,
+ }},
+ )
+ if err != nil && err.Code == srvdef.ErrNoData {
+ return nil
+ }
+ return err
+}
+
+// ListPlayletChapter 分页查询短剧剧集列表
+func ListPlayletChapter(ctx context.Context, req *proto.PlayletChapterListReq) (*proto.PlayletChapterListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playletchaptermod.FN.PlayletChapter.Id] = *req.Id
+ }
+ if req.PlayletId != nil && *req.PlayletId > 0 {
+ filter[playletchaptermod.FN.PlayletChapter.PlayletId] = *req.PlayletId
+ }
+ if req.WatermarkStatus != nil {
+ filter[playletchaptermod.FN.PlayletChapter.WatermarkStatus] = *req.WatermarkStatus
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playletchaptermod.CountPlayletChapter(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletChapterListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletChapterListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletChapterListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, chapterToWebResp(item))
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlayletChapter 新增短剧剧集
+func AddPlayletChapter(ctx context.Context, req *proto.PlayletChapterAddReq) (*proto.PlayletChapterAddResp, *srvdef.SrvErr) {
+ if req == nil || req.PlayletId <= 0 || req.CurrentEpisode <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "playletId/currentEpisode无效", "")
+ }
+ if !isValidPlayletChapterPayType(req.PayType) {
+ return nil, srvdef.New(srvdef.ErrParamError, "支付方式无效", "")
+ }
+ if req.PayType == playletchaptermod.PayTypeCoin && req.SingleCoins <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "金币支付时单集金币必须大于0", "")
+ }
+ singleCoins := req.SingleCoins
+ if req.PayType != playletchaptermod.PayTypeCoin {
+ singleCoins = 0
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.PlayletId})
+ if err != nil {
+ return nil, err
+ }
+ if playlet == nil || !playlet.IsOriginal {
+ return nil, srvdef.New(srvdef.ErrParamError, "只有原创短剧支持新增剧集", "")
+ }
+ doc := &playletchaptermod.PlayletChapter{
+ PlayletId: req.PlayletId,
+ CurrentEpisode: req.CurrentEpisode,
+ Title: strings.TrimSpace(req.Title),
+ Md5: strings.TrimSpace(req.Md5),
+ Hash: strings.TrimSpace(req.Hash),
+ Height: req.Height,
+ Width: req.Width,
+ Ratio: req.Ratio,
+ VideoUrl: strings.TrimSpace(req.VideoUrl),
+ MediaSize: req.MediaSize,
+ MediaTime: req.MediaTime,
+ PayType: req.PayType,
+ SingleCoins: singleCoins,
+ }
+ mediaSourceId := strings.TrimSpace(req.MediaSourceId)
+ if playlet.IsOriginal && (doc.VideoUrl != "" || mediaSourceId != "") {
+ doc.WatermarkStatus = playletchaptermod.WatermarkStatusRegistered
+ if mediaSourceId != "" {
+ // 后台上传正片已在 media 创建原片,直接沿用该 ID,避免再 SyncMediaFile 重复处理。
+ doc.MediaSourceId = mediaSourceId
+ } else {
+ sourceID, regErr := playletwatermark.RegisterChapterSource(doc)
+ if regErr != nil {
+ log.Error("AddPlayletChapter RegisterChapterSource 失败",
+ log.Any("playletId", req.PlayletId),
+ log.Any("videoUrl", doc.VideoUrl),
+ log.E(regErr))
+ } else {
+ doc.MediaSourceId = sourceID
+ }
+ }
+ }
+ if err := playletchaptermod.InsertOnePlayletChapter(ctx, doc); err != nil {
+ return nil, err
+ }
+ return &proto.PlayletChapterAddResp{Id: doc.Id}, nil
+}
+
+// DelPlayletChapter 单/批量删除原创短剧剧集。
+func DelPlayletChapter(ctx context.Context, req *proto.PlayletChapterDelReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "剧集ID不能为空", "")
+ }
+ filter := bson.M{playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids}}
+ chapters, findErr := playletchaptermod.FindManyPlayletChapter(ctx, filter)
+ if findErr != nil {
+ return findErr
+ }
+ if len(chapters) == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ playletIds := make([]int64, 0, len(chapters))
+ seenPlaylet := make(map[int64]struct{}, len(chapters))
+ for _, item := range chapters {
+ if item == nil || item.PlayletId <= 0 {
+ continue
+ }
+ if _, ok := seenPlaylet[item.PlayletId]; ok {
+ continue
+ }
+ seenPlaylet[item.PlayletId] = struct{}{}
+ playletIds = append(playletIds, item.PlayletId)
+ }
+ if len(playletIds) > 0 {
+ playlets, playletErr := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
+ }, options.Find().SetProjection(bson.M{
+ playletmod.FN.Playlet.Id: 1,
+ playletmod.FN.Playlet.IsOriginal: 1,
+ }))
+ if playletErr != nil {
+ return playletErr
+ }
+ for _, item := range playlets {
+ if item == nil || !item.IsOriginal {
+ return srvdef.New(srvdef.ErrParamError, "只有原创短剧支持删除剧集", "")
+ }
+ }
+ }
+ count, err := playletchaptermod.DelManyPlayletChapter(ctx, filter)
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdatePlayletChapter 编辑短剧剧集
+func UpdatePlayletChapter(ctx context.Context, req *proto.PlayletChapterUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ setDoc := bson.M{playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now()}
+ if req.CurrentEpisode != nil {
+ if *req.CurrentEpisode <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "集数无效", "")
+ }
+ setDoc[playletchaptermod.FN.PlayletChapter.CurrentEpisode] = *req.CurrentEpisode
+ }
+ if req.Title != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Title] = strings.TrimSpace(*req.Title)
+ }
+ if req.Md5 != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Md5] = strings.TrimSpace(*req.Md5)
+ }
+ if req.Hash != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Hash] = strings.TrimSpace(*req.Hash)
+ }
+ if req.Height != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Height] = *req.Height
+ }
+ if req.Width != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Width] = *req.Width
+ }
+ if req.Ratio != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Ratio] = *req.Ratio
+ }
+ if req.VideoUrl != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.VideoUrl] = strings.TrimSpace(*req.VideoUrl)
+ }
+ if req.MediaSize != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.MediaSize] = *req.MediaSize
+ }
+ if req.MediaTime != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.MediaTime] = *req.MediaTime
+ }
+ if req.PayType != nil {
+ if !isValidPlayletChapterPayType(*req.PayType) {
+ return srvdef.New(srvdef.ErrParamError, "支付方式无效", "")
+ }
+ setDoc[playletchaptermod.FN.PlayletChapter.PayType] = *req.PayType
+ if *req.PayType != playletchaptermod.PayTypeCoin && req.SingleCoins == nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = int64(0)
+ }
+ }
+ if req.SingleCoins != nil {
+ if *req.SingleCoins < 0 {
+ return srvdef.New(srvdef.ErrParamError, "单集金币不能小于0", "")
+ }
+ setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = *req.SingleCoins
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ return playletchaptermod.UpdateOnePlayletChapter(ctx,
+ bson.M{playletchaptermod.FN.PlayletChapter.Id: req.Id},
+ bson.M{"$set": setDoc},
+ )
+}
+
+// SetPlayletChapterPayType 单/批量设置短剧剧集支付方式
+func SetPlayletChapterPayType(ctx context.Context, req *proto.PlayletChapterPayTypeSetReq) *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 !isValidPlayletChapterPayType(req.PayType) {
+ return srvdef.New(srvdef.ErrParamError, "支付方式无效", "")
+ }
+ setDoc := bson.M{
+ playletchaptermod.FN.PlayletChapter.PayType: req.PayType,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now(),
+ }
+ if req.PayType == playletchaptermod.PayTypeCoin {
+ if req.SingleCoins == nil || *req.SingleCoins <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "金币支付时单集金币必须大于0", "")
+ }
+ setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = *req.SingleCoins
+ } else {
+ setDoc[playletchaptermod.FN.PlayletChapter.SingleCoins] = int64(0)
+ }
+ return playletchaptermod.UpdateManyPlayletChapter(ctx,
+ bson.M{playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ )
+}
+
+func buildPlayletListFilter(req *proto.PlayletListReq) bson.M {
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playletmod.FN.Playlet.Id] = *req.Id
+ }
+ if req.CategoryId != nil && *req.CategoryId > 0 {
+ filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{*req.CategoryId}}
+ }
+ if req.TagId != nil && *req.TagId > 0 {
+ filter[playletmod.FN.Playlet.TagIds] = bson.M{"$in": []int64{*req.TagId}}
+ }
+ if req.ThemeId != nil && *req.ThemeId > 0 {
+ filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{*req.ThemeId}}
+ }
+ if req.ContentType != nil {
+ filter[playletmod.FN.Playlet.ContentType] = *req.ContentType
+ }
+ if req.PayType != nil {
+ filter[playletmod.FN.Playlet.PayType] = *req.PayType
+ }
+ if req.Status != nil {
+ filter[playletmod.FN.Playlet.Status] = *req.Status
+ }
+ if req.IsFeatured != nil {
+ filter[playletmod.FN.Playlet.IsFeatured] = *req.IsFeatured
+ }
+ if req.IsOriginal != nil {
+ if *req.IsOriginal {
+ filter[playletmod.FN.Playlet.IsOriginal] = true
+ } else {
+ // 历史入库数据可能没有该字段,按非原创处理
+ filter[playletmod.FN.Playlet.IsOriginal] = bson.M{"$ne": true}
+ }
+ }
+ if req.ShowOriginal != nil {
+ if *req.ShowOriginal {
+ filter[playletmod.FN.Playlet.ShowOriginal] = true
+ } else {
+ filter[playletmod.FN.Playlet.ShowOriginal] = bson.M{"$ne": true}
+ }
+ }
+ if req.IsRecommend != nil {
+ filter[playletmod.FN.Playlet.IsRecommend] = *req.IsRecommend
+ }
+ if req.ShowOnBanner != nil {
+ filter[playletmod.FN.Playlet.ShowOnBanner] = *req.ShowOnBanner
+ }
+ if title := strings.TrimSpace(req.Title); title != "" {
+ filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
+ }
+ if operatorName := strings.TrimSpace(req.OperatorName); operatorName != "" {
+ filter[playletmod.FN.Playlet.OperatorName] = bson.M{"$regex": regexp.QuoteMeta(operatorName)}
+ }
+ return filter
+}
+
+func playletToWebResp(item *playletmod.Playlet, categoryNameMap map[int64]string, themeNameMap map[int64]string, tagNameMap map[int64]string) *proto.PlayletListResp {
+ return &proto.PlayletListResp{
+ Id: item.Id,
+ Status: item.Status,
+ IsFeatured: item.IsFeatured,
+ IsOriginal: item.IsOriginal,
+ ShowOriginal: item.ShowOriginal,
+ MediaBusiness: item.MediaBusiness,
+ IsRecommend: item.IsRecommend,
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: item.Banner,
+ ReviewAt: item.ReviewAt,
+ OnlineAt: item.OnlineAt,
+ Title: item.Title,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: item.Cover,
+ CompressedCover: item.CompressedCover,
+ Preview: item.Preview,
+ CategoryIds: item.CategoryIds,
+ CategoryBinds: toProtoCategoryBinds(playletmod.EffectiveCategoryBinds(item)),
+ CategoryNames: categoryNamesFromMap(item.CategoryIds, categoryNameMap),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: themeNamesFromMap(item.ThemeIds, themeNameMap),
+ TagIds: item.TagIds,
+ TagNames: tagNamesFromMap(item.TagIds, tagNameMap),
+ OldTags: normalizePlayletOldTags(item.OldTags),
+ ContentType: item.ContentType,
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ TotalEpisode: item.TotalEpisode,
+ FreeEpisodeCount: item.EffectiveFreeEpisodeCount(),
+ UpdateStatus: item.UpdateStatus,
+ TopRank: item.TopRank,
+ RecommendRank: item.RecommendRank,
+ CountComment: item.CountComment,
+ CountBuy: item.CountBuy,
+ CountBrowse: item.CountBrowse,
+ VirtualCountBrowse: item.VirtualCountBrowse,
+ DisplayCountBrowse: item.CountBrowse + item.VirtualCountBrowse,
+ HeatValue: item.HeatValue,
+ VirtualHeatValue: item.VirtualHeatValue,
+ DisplayHeatValue: item.HeatValue + item.VirtualHeatValue,
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ VirtualCountLike: item.VirtualCountLike,
+ DisplayCountLike: playletDisplayCountLike(item),
+ CountDisLike: item.CountDisLike,
+ SeoInfo: normalizeSeoValue(item.SeoInfo),
+ OperatorId: item.OperatorId,
+ OperatorName: item.OperatorName,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+func validOfficialFreeEpisodeCount(count int64) bool {
+ return count == -1 || count >= 1 && count <= 1000
+}
+
+func playletDisplayCountLike(item *playletmod.Playlet) int64 {
+ if item == nil {
+ return 0
+ }
+ return item.CountLike + item.VirtualCountLike
+}
+
+func chapterToWebResp(item *playletchaptermod.PlayletChapter) *proto.PlayletChapterListResp {
+ if item == nil {
+ return nil
+ }
+ return &proto.PlayletChapterListResp{
+ Id: item.Id,
+ PlayletId: item.PlayletId,
+ MediaContentId: item.MediaContentId,
+ CurrentEpisode: item.CurrentEpisode,
+ Title: item.Title,
+ Md5: item.Md5,
+ Hash: item.Hash,
+ Height: item.Height,
+ Width: item.Width,
+ Ratio: item.Ratio,
+ VideoUrl: item.VideoUrl,
+ WatermarkVideoUrl: item.WatermarkVideoUrl,
+ MediaSize: item.MediaSize,
+ MediaTime: item.MediaTime,
+ CountComment: item.CountComment,
+ CountBuy: item.CountBuy,
+ CountBrowse: item.CountBrowse,
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ CountDisLike: item.CountDisLike,
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ WatermarkStatus: item.WatermarkStatus,
+ MediaSourceId: item.MediaSourceId,
+ MediaTaskId: item.MediaTaskId,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+func buildCategoryNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ categoryIds := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, categoryId := range item.CategoryIds {
+ if categoryId <= 0 {
+ continue
+ }
+ if _, ok := seen[categoryId]; ok {
+ continue
+ }
+ seen[categoryId] = struct{}{}
+ categoryIds = append(categoryIds, categoryId)
+ }
+ }
+ result := make(map[int64]string, len(categoryIds))
+ if len(categoryIds) == 0 {
+ return result, nil
+ }
+ categories, err := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": categoryIds},
+ }, options.Find().SetProjection(bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: 1,
+ playletcategorymod.FN.PlayletCategory.Name: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, category := range categories {
+ if category != nil {
+ result[category.Id] = category.Name
+ }
+ }
+ return result, nil
+}
+
+func categoryNamesFromMap(categoryIds []int64, categoryNameMap map[int64]string) []string {
+ result := make([]string, 0, len(categoryIds))
+ for _, categoryId := range categoryIds {
+ if name := categoryNameMap[categoryId]; name != "" {
+ result = append(result, name)
+ }
+ }
+ return result
+}
+
+func buildThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ themeIds := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, themeId := range item.ThemeIds {
+ if themeId <= 0 {
+ continue
+ }
+ if _, ok := seen[themeId]; ok {
+ continue
+ }
+ seen[themeId] = struct{}{}
+ themeIds = append(themeIds, themeId)
+ }
+ }
+ result := make(map[int64]string, len(themeIds))
+ if len(themeIds) == 0 {
+ return result, nil
+ }
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds},
+ }, options.Find().SetProjection(bson.M{
+ playletthememod.FN.PlayletTheme.Id: 1,
+ playletthememod.FN.PlayletTheme.Name: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, theme := range themes {
+ if theme != nil {
+ result[theme.Id] = theme.Name
+ }
+ }
+ return result, nil
+}
+
+func themeNamesFromMap(themeIds []int64, themeNameMap map[int64]string) []string {
+ result := make([]string, 0, len(themeIds))
+ for _, themeId := range themeIds {
+ if name := themeNameMap[themeId]; name != "" {
+ result = append(result, name)
+ }
+ }
+ return result
+}
+
+func buildTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ tagIds := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, tagId := range item.TagIds {
+ if tagId <= 0 {
+ continue
+ }
+ if _, ok := seen[tagId]; ok {
+ continue
+ }
+ seen[tagId] = struct{}{}
+ tagIds = append(tagIds, tagId)
+ }
+ }
+ result := make(map[int64]string, len(tagIds))
+ if len(tagIds) == 0 {
+ return result, nil
+ }
+ tags, err := contenttagmod.GetMapTags(ctx, tagIds)
+ if err != nil {
+ return nil, err
+ }
+ for _, tag := range tags {
+ if tag != nil {
+ result[tag.Id] = tag.Name
+ }
+ }
+ return result, nil
+}
+
+func tagNamesFromMap(tagIds []int64, tagNameMap map[int64]string) []string {
+ result := make([]string, 0, len(tagIds))
+ for _, tagId := range tagIds {
+ if name := tagNameMap[tagId]; name != "" {
+ result = append(result, name)
+ }
+ }
+ return result
+}
+
+func normalizePlayletOldTags(oldTags []string) []string {
+ if oldTags == nil {
+ return make([]string, 0)
+ }
+ return oldTags
+}
+
+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
+}
+
+func isValidPlayletStatus(status playletmod.Status) bool {
+ return status == playletmod.StatusDown || status == playletmod.StatusUp
+}
+
+func isValidPlayletPayType(payType playletmod.PayType) bool {
+ return payType == playletmod.PayTypeFree ||
+ payType == playletmod.PayTypeCoin ||
+ payType == playletmod.PayTypeVip
+}
+
+func isValidPlayletChapterPayType(payType playletchaptermod.PayType) bool {
+ return payType == playletchaptermod.PayTypeFree ||
+ payType == playletchaptermod.PayTypeCoin ||
+ payType == playletchaptermod.PayTypeVip
+}
+
+func isValidPlayletContentType(contentType playletmod.ContentType) bool {
+ return contentType == playletmod.ContentTypeTheater ||
+ contentType == playletmod.ContentTypeAlbum ||
+ contentType == playletmod.ContentTypeComic ||
+ contentType == playletmod.ContentTypeAnime ||
+ contentType == playletmod.ContentTypeManga
+}
+
+func normalizePlayletPaySetting(payType playletmod.PayType, singleCoins *int64, requireCoinAmount bool) (playletmod.PayType, int64, *srvdef.SrvErr) {
+ if !isValidPlayletPayType(payType) {
+ return 0, 0, srvdef.New(srvdef.ErrParamError, "支付方式无效", "")
+ }
+ if payType == playletmod.PayTypeCoin {
+ if requireCoinAmount && (singleCoins == nil || *singleCoins <= 0) {
+ return 0, 0, srvdef.New(srvdef.ErrParamError, "金币支付时单集金币必须大于0", "")
+ }
+ if singleCoins == nil {
+ return payType, 0, nil
+ }
+ return payType, *singleCoins, nil
+ }
+ return payType, 0, nil
+}
+
+func normalizePlayletTagIds(tagIds []int64) ([]int64, *srvdef.SrvErr) {
+ result := make([]int64, 0, len(tagIds))
+ seen := make(map[int64]struct{}, len(tagIds))
+ for _, tagId := range tagIds {
+ if tagId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
+ }
+ if _, ok := seen[tagId]; ok {
+ continue
+ }
+ seen[tagId] = struct{}{}
+ result = append(result, tagId)
+ }
+ return result, nil
+}
+
+func validatePlayletTagIds(ctx context.Context, tagIds []int64) *srvdef.SrvErr {
+ if len(tagIds) == 0 {
+ return nil
+ }
+ return srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypePlaylet)
+}
+
+// resolveAddCategoryBinds 新增短剧时根据分类绑定或分类ID生成排序配置
+func resolveAddCategoryBinds(binds []proto.PlayletCategoryBind, ids []int64) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) {
+ if len(binds) > 0 {
+ return resolveCategoryBinds(&binds, nil, nil)
+ }
+ return resolveCategoryBinds(nil, &ids, nil)
+}
+
+// resolveCategoryBinds 保留旧分类排序,新增分类默认排序为0
+func resolveCategoryBinds(binds *[]proto.PlayletCategoryBind, ids *[]int64, old []playletmod.PlayletCategoryBind) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) {
+ if binds != nil {
+ result := make([]playletmod.PlayletCategoryBind, 0, len(*binds))
+ seen := make(map[int64]bool, len(*binds))
+ for _, bind := range *binds {
+ if bind.Id <= 0 || seen[bind.Id] {
+ return nil, srvdef.New(srvdef.ErrParamError, "分类ID无效或重复", "")
+ }
+ seen[bind.Id] = true
+ result = append(result, playletmod.PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank})
+ }
+ return result, nil
+ }
+ if ids != nil {
+ categoryIds, err := normalizePlayletCategoryIds(*ids)
+ if err != nil {
+ return nil, err
+ }
+ return playletmod.MergeCategoryBindsByIds(old, categoryIds), nil
+ }
+ return nil, nil
+}
+
+func toProtoCategoryBinds(binds []playletmod.PlayletCategoryBind) []proto.PlayletCategoryBind {
+ result := make([]proto.PlayletCategoryBind, 0, len(binds))
+ for _, bind := range binds {
+ result = append(result, proto.PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank})
+ }
+ return result
+}
+
+func normalizePlayletCategoryIds(categoryIds []int64) ([]int64, *srvdef.SrvErr) {
+ result := make([]int64, 0, len(categoryIds))
+ seen := make(map[int64]struct{}, len(categoryIds))
+ for _, categoryId := range categoryIds {
+ if categoryId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "分类ID无效", "")
+ }
+ if _, ok := seen[categoryId]; ok {
+ continue
+ }
+ seen[categoryId] = struct{}{}
+ result = append(result, categoryId)
+ }
+ return result, nil
+}
+
+func validatePlayletCategoryIds(ctx context.Context, categoryIds []int64) *srvdef.SrvErr {
+ if len(categoryIds) == 0 {
+ return nil
+ }
+ count, err := playletcategorymod.CountPlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": categoryIds},
+ })
+ if err != nil {
+ return err
+ }
+ if count != int64(len(categoryIds)) {
+ return srvdef.New(srvdef.ErrParamError, "分类不存在", "")
+ }
+ return nil
+}
+
+func validatePlayletThemeIds(ctx context.Context, themeIds []int64) *srvdef.SrvErr {
+ if len(themeIds) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "")
+ }
+ count, err := playletthememod.CountPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds},
+ })
+ if err != nil {
+ return err
+ }
+ if count != int64(len(themeIds)) {
+ return srvdef.New(srvdef.ErrParamError, "题材不存在", "")
+ }
+ return nil
+}
+
+func playletOnlineAt(status playletmod.Status, now time.Time) time.Time {
+ if status == playletmod.StatusUp {
+ return now
+ }
+ return time.Time{}
+}
+
+func resolvePlayletOperator(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
+}
+
+func asyncSyncPlayletESByStatus(ids []int64, status playletmod.Status, action string) {
+ syncIds := append([]int64(nil), ids...)
+ go func() {
+ if err1 := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, status); err1 != nil {
+ log.Error(action+"同步ES失败", log.Any("短剧ID", syncIds), log.Any("状态", status), log.E(err1))
+ }
+ }()
+}
+
+func asyncSyncPlayletESByLatestStatus(ids []int64, action string) {
+ syncIds := append([]int64(nil), ids...)
+ go func() {
+ if err1 := srv_search.SyncPlayletsToESByLatestStatus(context.Background(), syncIds); err1 != nil {
+ log.Error(action+"同步ES失败", log.Any("短剧ID", syncIds), log.E(err1))
+ }
+ }()
+}
+
+func randomUniqueVirtualLikeValues(min int64, max int64, count int) []int64 {
+ if count <= 0 {
+ return nil
+ }
+ rng := rand.New(rand.NewSource(time.Now().UnixNano()))
+ result := make([]int64, 0, count)
+ selected := make(map[int64]struct{}, count)
+ start := max - int64(count) + 1
+ for offset := int64(0); offset < int64(count); offset++ {
+ current := start + offset
+ picked := min + rng.Int63n(current-min+1)
+ if _, ok := selected[picked]; ok {
+ picked = current
+ }
+ selected[picked] = struct{}{}
+ result = append(result, picked)
+ }
+ return result
+}
diff --git a/web/service/srv_playlet/tag.go b/web/service/srv_playlet/tag.go
new file mode 100644
index 0000000..8794986
--- /dev/null
+++ b/web/service/srv_playlet/tag.go
@@ -0,0 +1,152 @@
+package srv_playlet
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// ListPlayletTag 分页查询标签列表
+func ListPlayletTag(ctx context.Context, req *proto.PlayletTagListReq) (*proto.PlayletTagListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playlettagmod.FN.PlayletTag.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[playlettagmod.FN.PlayletTag.Status] = *req.Status
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[playlettagmod.FN.PlayletTag.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playlettagmod.CountPlayletTag(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletTagListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletTagListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playlettagmod.FindManyPlayletTag(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: playlettagmod.FN.PlayletTag.Rank, Value: -1},
+ {Key: playlettagmod.FN.PlayletTag.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletTagListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.PlayletTagListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Status: item.Status,
+ Rank: item.Rank,
+ SeoInfo: normalizeSeoValue(item.SeoInfo),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlayletTag 新增标签
+func AddPlayletTag(ctx context.Context, req *proto.PlayletTagAddReq) (*proto.PlayletTagAddResp, *srvdef.SrvErr) {
+ if req == nil || strings.TrimSpace(req.Name) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
+ }
+ doc := &playlettagmod.PlayletTag{
+ Name: strings.TrimSpace(req.Name),
+ Status: req.Status,
+ Rank: req.Rank,
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ if err := playlettagmod.InsertOnePlayletTag(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearAppPlayletHomeCache()
+ return &proto.PlayletTagAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlayletTag 更新标签
+func UpdatePlayletTag(ctx context.Context, req *proto.PlayletTagUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ setDoc := bson.M{playlettagmod.FN.PlayletTag.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
+ }
+ setDoc[playlettagmod.FN.PlayletTag.Name] = name
+ }
+ if req.Status != nil {
+ setDoc[playlettagmod.FN.PlayletTag.Status] = *req.Status
+ }
+ if req.Rank != nil {
+ setDoc[playlettagmod.FN.PlayletTag.Rank] = *req.Rank
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ if err := playlettagmod.UpdateOnePlayletTag(ctx,
+ bson.M{playlettagmod.FN.PlayletTag.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// DelPlayletTag 删除标签(被短剧引用时禁止删除)
+func DelPlayletTag(ctx context.Context, id int64) *srvdef.SrvErr {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ usedCount, err := playletmod.CountPlaylet(ctx, bson.M{playletmod.FN.Playlet.TagIds: id})
+ if err != nil {
+ return err
+ }
+ if usedCount > 0 {
+ return srvdef.New(srvdef.ErrParamError, "标签已被短剧引用,无法删除", "")
+ }
+ count, err := playlettagmod.DelOnePlayletTag(ctx, bson.M{playlettagmod.FN.PlayletTag.Id: id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
diff --git a/web/service/srv_playlet/theme.go b/web/service/srv_playlet/theme.go
new file mode 100644
index 0000000..24f767c
--- /dev/null
+++ b/web/service/srv_playlet/theme.go
@@ -0,0 +1,213 @@
+package srv_playlet
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var playletThemeWebProjection = bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Slug, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Icon, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Status, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Rank, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.SeoInfo.Title, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.SeoInfo.Description, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.SeoInfo.Keyworld, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.CreatedAt, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.UpdatedAt, Value: 1},
+}
+
+// ListPlayletTheme 分页查询题材列表
+func ListPlayletTheme(ctx context.Context, req *proto.PlayletThemeListReq) (*proto.PlayletThemeListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playletthememod.FN.PlayletTheme.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[playletthememod.FN.PlayletTheme.Status] = *req.Status
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[playletthememod.FN.PlayletTheme.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playletthememod.CountPlayletTheme(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletThemeListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletThemeListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
+ SetProjection(playletThemeWebProjection).
+ SetSort(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Rank, Value: -1},
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletThemeListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.PlayletThemeListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Icon: item.Icon,
+ Status: item.Status,
+ Rank: item.Rank,
+ SeoInfo: normalizeSeoValue(item.SeoInfo),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlayletTheme 新增题材
+func AddPlayletTheme(ctx context.Context, req *proto.PlayletThemeAddReq) (*proto.PlayletThemeAddResp, *srvdef.SrvErr) {
+ if req == nil || strings.TrimSpace(req.Name) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "题材名称不能为空", "")
+ }
+ doc := &playletthememod.PlayletTheme{
+ Name: strings.TrimSpace(req.Name),
+ Slug: req.Slug,
+ Icon: strings.TrimSpace(req.Icon),
+ Status: req.Status,
+ Rank: req.Rank,
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ exists := func(s string) bool {
+ item, _ := playletthememod.FindOnePlayletTheme(ctx, slugx.Filter(
+ playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.Id, s, 0))
+ return item != nil
+ }
+ if serr := slugx.Ensure(&doc.Slug, utils.SlugSourceFromKeywordsOrTitle(doc.SeoInfo.Keyworld, doc.Name), exists); serr != nil {
+ return nil, serr
+ }
+ if err := playletthememod.InsertOnePlayletTheme(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearAppPlayletHomeCache()
+ return &proto.PlayletThemeAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlayletTheme 更新题材
+func UpdatePlayletTheme(ctx context.Context, req *proto.PlayletThemeUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材ID无效", "")
+ }
+ current, err := playletthememod.FindOnePlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: req.Id,
+ })
+ if err != nil {
+ return err
+ }
+ setDoc := bson.M{playletthememod.FN.PlayletTheme.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "题材名称不能为空", "")
+ }
+ setDoc[playletthememod.FN.PlayletTheme.Name] = name
+ }
+ if req.Icon != nil {
+ setDoc[playletthememod.FN.PlayletTheme.Icon] = strings.TrimSpace(*req.Icon)
+ }
+ if req.Status != nil {
+ setDoc[playletthememod.FN.PlayletTheme.Status] = *req.Status
+ }
+ if req.Rank != nil {
+ setDoc[playletthememod.FN.PlayletTheme.Rank] = *req.Rank
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
+ }
+ name := current.Name
+ if req.Name != nil {
+ name = strings.TrimSpace(*req.Name)
+ }
+ keywords := current.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ exists := func(s string) bool {
+ item, _ := playletthememod.FindOnePlayletTheme(ctx, slugx.Filter(
+ playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.Id, s, req.Id))
+ return item != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, req.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ setDoc[playletthememod.FN.PlayletTheme.Slug] = slug
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ if err := playletthememod.UpdateOnePlayletTheme(ctx,
+ bson.M{playletthememod.FN.PlayletTheme.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// DelPlayletTheme 删除题材,被短剧引用时禁止删除
+func DelPlayletTheme(ctx context.Context, id int64) *srvdef.SrvErr {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材ID无效", "")
+ }
+ usedCount, err := playletmod.CountPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.ThemeIds: bson.M{"$in": []int64{id}},
+ })
+ if err != nil {
+ return err
+ }
+ if usedCount > 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材已被短剧引用,无法删除", "")
+ }
+ count, err := playletthememod.DelOnePlayletTheme(ctx, bson.M{playletthememod.FN.PlayletTheme.Id: id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
diff --git a/web/service/srv_questionnaire/define.go b/web/service/srv_questionnaire/define.go
new file mode 100644
index 0000000..d4b82a4
--- /dev/null
+++ b/web/service/srv_questionnaire/define.go
@@ -0,0 +1,28 @@
+package srv_questionnaire
+
+import (
+ "videoweb/pub/models/questionnairemod"
+ "videoweb/pub/pubdef"
+)
+
+// ListReq 查询问卷列表请求
+type ListReq struct {
+ pubdef.Page
+ QuestionType *string `json:"questionType"` // 模糊匹配:问题类型(选填)
+ Status *string `json:"status"` // 精确匹配:处理状态(选填)
+ StartTime *int64 `json:"startTime"` // 创建时间范围-开始,Unix 时间戳(秒,选填)
+ EndTime *int64 `json:"endTime"` // 创建时间范围-结束,Unix 时间戳(秒,选填)
+}
+
+// ListResp 查询问卷列表响应
+type ListResp struct {
+ Total int64 `json:"total"` // 满足条件的总条数
+ List []*questionnairemod.QuestionnaireModel `json:"list"` // 当前页数据
+}
+
+// HandleReq 处理问卷请求
+type HandleReq struct {
+ Id string `json:"id" binding:"required"` // 问卷ID(ObjectID 字符串)
+ Status string `json:"status" binding:"required"` // 目标状态:processed 已处理 / reject 拒绝
+ Remark string `json:"remark"` // 备注(选填)
+}
diff --git a/web/service/srv_questionnaire/questionnaire.go b/web/service/srv_questionnaire/questionnaire.go
new file mode 100644
index 0000000..ddbc18b
--- /dev/null
+++ b/web/service/srv_questionnaire/questionnaire.go
@@ -0,0 +1,97 @@
+package srv_questionnaire
+
+import (
+ "context"
+ "time"
+
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/questionnairemod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// List 查询问卷列表(分页 + 条件筛选)
+func List(ctx context.Context, req *ListReq) (*ListResp, *srvdef.SrvErr) {
+ if req.PageNum < 1 {
+ req.PageNum = 1
+ }
+ if req.PageSize < 1 {
+ req.PageSize = 20
+ }
+
+ filter := bson.M{}
+ if req.QuestionType != nil && *req.QuestionType != "" {
+ filter[questionnairemod.FN.Questionnaire.QuestionType] = bson.M{
+ "$regex": *req.QuestionType,
+ }
+ }
+ if req.Status != nil && *req.Status != "" {
+ filter[questionnairemod.FN.Questionnaire.Status] = *req.Status
+ }
+ if req.StartTime != nil || req.EndTime != nil {
+ timeRange := bson.M{}
+ if req.StartTime != nil {
+ timeRange["$gte"] = time.Unix(*req.StartTime, 0)
+ }
+ if req.EndTime != nil {
+ timeRange["$lte"] = time.Unix(*req.EndTime, 0)
+ }
+ filter[questionnairemod.FN.Questionnaire.CreatedAt] = timeRange
+ }
+
+ total, serr := questionnairemod.Count(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ if total == 0 {
+ return &ListResp{Total: 0, List: make([]*questionnairemod.QuestionnaireModel, 0)}, nil
+ }
+
+ opt := options.Find().
+ SetSort(bson.D{{Key: questionnairemod.FN.Questionnaire.CreatedAt, Value: -1}}).
+ SetSkip((req.PageNum - 1) * req.PageSize).
+ SetLimit(req.PageSize)
+
+ list, serr := questionnairemod.FindMany(ctx, filter, opt)
+ if serr != nil {
+ return nil, serr
+ }
+
+ return &ListResp{Total: total, List: list}, nil
+}
+
+// Handle 处理问卷(仅允许"待处理"状态的问卷被更新)
+func Handle(ctx context.Context, req *HandleReq) *srvdef.SrvErr {
+ objId, err := primitive.ObjectIDFromHex(req.Id)
+ if err != nil {
+ return srvdef.New(srvdef.ErrParamError, "问卷ID格式错误", "")
+ }
+
+ filter := bson.M{
+ questionnairemod.FN.Questionnaire.Id: objId,
+ questionnairemod.FN.Questionnaire.Status: questionnairemod.StatusPending,
+ }
+ update := bson.M{
+ "$set": bson.M{
+ questionnairemod.FN.Questionnaire.Status: req.Status,
+ questionnairemod.FN.Questionnaire.Remark: req.Remark,
+ questionnairemod.FN.Questionnaire.UpdatedAt: time.Now(),
+ },
+ }
+
+ matched, serr := questionnairemod.UpdateOne(ctx, filter, update)
+ if serr != nil {
+ return serr
+ }
+ if matched == 0 {
+ // 区分"不存在"与"状态不是待处理"
+ _, serr2 := questionnairemod.FindOne(ctx, bson.M{questionnairemod.FN.Questionnaire.Id: objId})
+ if serr2 != nil {
+ return srvdef.New(srvdef.ErrNoData, "问卷不存在", "")
+ }
+ return srvdef.New(srvdef.Err, "该问卷已处理,不可重复操作", "该问卷已处理,不可重复操作")
+ }
+ return nil
+}
diff --git a/web/service/srv_report/report.go b/web/service/srv_report/report.go
new file mode 100644
index 0000000..e53813c
--- /dev/null
+++ b/web/service/srv_report/report.go
@@ -0,0 +1,75 @@
+package srv_report
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// ReportVideoStatusChangeEvent 视频状态变更事件上报
+func ReportVideoStatusChangeEvent(media *videoinformationmod.VideoInformation, flag, userId int64) {
+ if media == nil || media.Id == 0 || flag == 0 {
+ return
+ }
+
+ var lsjId string
+
+ var videoTypeID = "default"
+ var videoTypeName = "默认分类"
+ if media.CategoryId > 0 {
+ // 复用 reportser 中的分类解析逻辑
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ category, _ := videocategorymod.FindOneVideoCategory(ctx, bson.M{
+ videocategorymod.FN.VideoCategory.Id: media.CategoryId,
+ })
+ if category != nil {
+ videoTypeID = fmt.Sprintf("%v", category.Id)
+ videoTypeName = category.Name
+ }
+ }
+ uaInfo := useragent.UA{SysType: pubdef.ST_PC}
+ event := dataReport.NewCommonField(dataReport.EventTypeVideoStatusChange, "", uaInfo, strconv.FormatInt(userId, 10), userId)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoStatusChangeEvent{
+ MediaId: lsjId,
+ VideoID: fmt.Sprintf("%v", media.Id),
+ VideoTitle: media.Title,
+ VideoTypeID: videoTypeID,
+ VideoTypeName: videoTypeName,
+ Flag: dataReport.JsonNumber(flag),
+ })
+ b, _ := json.Marshal(event)
+ if err := eventmod.InsertOneEvent(context.Background(), &eventmod.Event{
+ EventId: event.EventID,
+ EventType: event.Event,
+ Data: string(b),
+ Status: eventmod.Default,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }); err != nil {
+ log.Error("ReportVideoStatusChangeEvent fail", log.Any("mediaId", media.Id), log.E(err))
+ }
+}
+
+// VideoStatusFlag 将媒体状态映射为上报flag
+func VideoStatusFlag(status pubdef.InformationStatus) int64 {
+ switch status {
+ case pubdef.InformationStatusVerifyOK:
+ return 1 // 上架
+ case pubdef.InformationStatusVerifyCancel:
+ return 2 // 未通过
+ default:
+ return 0
+ }
+}
diff --git a/web/service/srv_richtextcontent/define.go b/web/service/srv_richtextcontent/define.go
new file mode 100644
index 0000000..04cc8f8
--- /dev/null
+++ b/web/service/srv_richtextcontent/define.go
@@ -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
+}
diff --git a/web/service/srv_richtextcontent/richtextcontent.go b/web/service/srv_richtextcontent/richtextcontent.go
new file mode 100644
index 0000000..4658a53
--- /dev/null
+++ b/web/service/srv_richtextcontent/richtextcontent.go
@@ -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
+}
diff --git a/web/service/srv_search/content_tag.go b/web/service/srv_search/content_tag.go
new file mode 100644
index 0000000..6e95011
--- /dev/null
+++ b/web/service/srv_search/content_tag.go
@@ -0,0 +1,13 @@
+package srv_search
+
+import "strings"
+
+func contentTagNamesString(tagIds []int64, tagMap map[int64]string) string {
+ names := make([]string, 0, len(tagIds))
+ for _, tagId := range tagIds {
+ if name := tagMap[tagId]; name != "" {
+ names = append(names, name)
+ }
+ }
+ return strings.Join(names, ",")
+}
diff --git a/web/service/srv_search/define.go b/web/service/srv_search/define.go
new file mode 100644
index 0000000..2f24b25
--- /dev/null
+++ b/web/service/srv_search/define.go
@@ -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
+}
\ No newline at end of file
diff --git a/web/service/srv_search/es.go b/web/service/srv_search/es.go
new file mode 100644
index 0000000..41b1e67
--- /dev/null
+++ b/web/service/srv_search/es.go
@@ -0,0 +1,187 @@
+package srv_search
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+var syncIsRuining = false
+
+func StartES() {
+ log.Info("srv_search:Start...")
+ defer log.Info("srv_search:Start!!!")
+ startSyncEs()
+ _, _ = skdCron.AddFunc("0 0/17 * * * ?", func() {
+ startSyncEs()
+ })
+ skdCron.Start()
+}
+
+func startSyncEs() {
+ ctx := context.Background()
+ updateEsData(ctx, esmod.GetVideoIndex(), getVideoEsData)
+ updateEsData(ctx, esmod.GetPlayletIndex(), getPlayletEsData)
+ updateEsData(ctx, esmod.GetActressIndex(), getActressEsData)
+}
+
+func StopES() {
+ log.Info("srv_search:Stop...")
+ defer log.Info("srv_search:Stop!!!")
+}
+
+func updateEsData(ctx context.Context, esIndex string, fn func(ctx context.Context, startTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr)) {
+ log.Info("srv_search:do...", log.Any("ESIndex", esIndex))
+ defer log.Info("srv_search:do end", log.Any("EsIndex", esIndex))
+ if fn == nil {
+ return
+ }
+ esRecord, serr := esmod.GetRecordTimeByESIndex(ctx, esIndex)
+ if serr != nil {
+ log.Error(fmt.Sprintf("srv_search:startEngine GetRecordByESIndex failed! Index:%v,err:%+v:", esIndex, serr))
+ return
+ }
+ isNew := false
+ if esRecord == nil {
+ isNew = true
+ esRecord = &esmod.ESRecord{
+ Id: primitive.NewObjectID(),
+ EsIndex: esIndex,
+ RecordTime: time.Time{},
+ Timestamp: esmod.ESTimestamp{},
+ UpdatedAt: time.Time{},
+ CreatedAt: time.Now(),
+ }
+ }
+ var now = time.Now()
+ var esData esmanager.M
+ // 跨天凌晨5点重置所有数据
+ if isNew || ((now.Hour() >= 5 && esRecord.UpdatedAt.Local().Hour() < 5) || esRecord.UpdatedAt.Local().Day() < now.Day()) {
+ log.Info("srv_search:updateEsData 全量同步", log.Any("index", esIndex), log.Any("isNew", isNew))
+ esData, serr = fn(ctx, nil, nil, esIndex)
+ if serr != nil {
+ log.Warn("updateEsData fn return err", log.Any("index", esIndex), log.E(serr))
+ return
+ }
+ log.Info("srv_search:updateEsData 全量数据拉取完成", log.Any("index", esIndex), log.Any("count", len(esData)))
+ if len(esData) == 0 {
+ if err := esmod.SetOneEsRecordTime(nil, esIndex, now); err != nil {
+ return
+ }
+ return
+ }
+ isUpdate, err := commit(esData, esIndex, time.Now())
+ if err != nil {
+ log.Error("Elastic sync video data RunSync commit err! ", log.E(err))
+ return
+ }
+ log.Info("srv_search:updateEsData 全量写入ES完成", log.Any("index", esIndex), log.Any("isUpdate", isUpdate))
+ if isUpdate {
+ if err := esmod.SetOneEsRecordTime(nil, esIndex, now); err != nil {
+ return
+ }
+ }
+ } else {
+ timestamp := esRecord.RecordTime
+ var recentMinute = utils.RecentMinute(now, esmod.FiveMinuteScale)
+ log.Info("srv_search:updateEsData 增量同步", log.Any("index", esIndex),
+ log.Any("from", timestamp.Format("2006-01-02 15:04:05")),
+ log.Any("to", recentMinute.Format("2006-01-02 15:04:05")))
+ totalIncr := 0
+ for recentMinute.After(timestamp) {
+ var end = timestamp.Add(esmod.FiveMinuteScale * time.Minute)
+ if end.After(recentMinute) {
+ end = recentMinute
+ }
+ esData, serr = fn(ctx, ×tamp, &end, esIndex)
+ if serr != nil {
+ log.Warn("updateEsData fn return err", log.Any("index", esIndex), log.E(serr))
+ return
+ }
+ if esData != nil && len(esData) > 0 {
+ totalIncr += len(esData)
+ log.Info("srv_search:updateEsData 增量批次",
+ log.Any("index", esIndex),
+ log.Any("window_start", timestamp.Format("2006-01-02 15:04:05")),
+ log.Any("window_end", end.Format("2006-01-02 15:04:05")),
+ log.Any("count", len(esData)))
+ isUpdate, err := commit(esData, esIndex, time.Now())
+ if err != nil {
+ log.Error("Elastic sync video data RunSync commit err! ", log.E(err))
+ return
+ }
+ if isUpdate {
+ if err := esmod.SetOneEsRecordTime(nil, esIndex, end); err != nil {
+ return
+ }
+ }
+ } else {
+ if err := esmod.SetOneEsRecordTime(nil, esIndex, end); err != nil {
+ return
+ }
+ }
+ timestamp = end
+ }
+ log.Info("srv_search:updateEsData 增量同步完成", log.Any("index", esIndex), log.Any("totalCount", totalIncr))
+ }
+}
+
+func commit(data esmanager.M, esIndex string, syncTime time.Time) (bool, error) {
+ bulkData := handleBulkCountLimit(data)
+ isHaveSuccess := false
+ failedBatches := make([]int, 0)
+ log.Info("srv_search:commit 开始写入ES", log.Any("index", esIndex),
+ log.Any("totalDocs", len(data)), log.Any("batches", len(bulkData)))
+ if bulkData != nil && len(bulkData) > 0 {
+ for i, doc := range bulkData {
+ if len(doc) == 0 {
+ continue
+ }
+ if err := esmanager.Instance.Bulk(esIndex, doc); err != nil {
+ log.Warn(fmt.Sprintf("Elastic sync commit video data SyncDataToES failed!, err:%+v\n", err),
+ log.Any("index", esIndex), log.Any("batch", i), log.Any("batchSize", len(doc)))
+ failedBatches = append(failedBatches, i)
+ } else {
+ isHaveSuccess = true
+ log.Info("srv_search:commit 批次写入成功", log.Any("index", esIndex),
+ log.Any("batch", i), log.Any("batchSize", len(doc)))
+ }
+ }
+ }
+ if len(failedBatches) > 0 {
+ return isHaveSuccess, fmt.Errorf("elastic sync commit failed batches: %v", failedBatches)
+ }
+ if isHaveSuccess {
+ if err := esmanager.Instance.Refresh(esIndex); err != nil {
+ return isHaveSuccess, err
+ }
+ return isHaveSuccess, nil
+ } else {
+ return isHaveSuccess, errors.New("Elastic sync commit video data UpsertOneTrans failed!")
+ }
+}
+
+func handleBulkCountLimit(data esmanager.M) []esmanager.M {
+ if data == nil || len(data) == 0 {
+ return make([]esmanager.M, 0)
+ }
+ rsl := make([]esmanager.M, 0)
+ rsl = append(rsl, make(esmanager.M, batchSize))
+ index := 0
+ for k, v := range data {
+ rsl[index][k] = v
+ if len(rsl[index]) >= batchSize {
+ index++
+ rsl = append(rsl, make(esmanager.M, batchSize))
+ }
+ }
+ return rsl
+}
diff --git a/web/service/srv_search/es_playlet.go b/web/service/srv_search/es_playlet.go
new file mode 100644
index 0000000..5f3278c
--- /dev/null
+++ b/web/service/srv_search/es_playlet.go
@@ -0,0 +1,378 @@
+package srv_search
+
+import (
+ "context"
+ "strconv"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const playletESPageSize int64 = 1000
+
+var playletESProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Status, Value: 1},
+ {Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
+ {Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
+ {Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1},
+ {Key: playletmod.FN.Playlet.Banner, Value: 1},
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
+ {Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.Desc, Value: 1},
+ {Key: playletmod.FN.Playlet.Anchor, Value: 1},
+ {Key: playletmod.FN.Playlet.Cover, Value: 1},
+ {Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualHeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Description, Value: 1},
+ {Key: playletmod.FN.Playlet.SeoInfo.Keyworld, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+}
+
+// getPlayletEsData 查询短剧增量数据并转换为ES批量写入数据
+func getPlayletEsData(ctx context.Context, minTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr) {
+ list, err1 := getPlayletData(ctx, minTime, endTime)
+ if err1 != nil {
+ return nil, err1
+ }
+ return playletToEsData(ctx, list)
+}
+
+// SyncPlayletsToESByLatestStatus 根据短剧最新上下架状态同步ES搜索数据
+func SyncPlayletsToESByLatestStatus(ctx context.Context, ids []int64) *srvdef.SrvErr {
+ ids = dedupePlayletESIds(ids)
+ if len(ids) == 0 {
+ return nil
+ }
+ list, err1 := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Status, Value: 1},
+ }))
+ if err1 != nil {
+ return err1
+ }
+ foundIds := make(map[int64]struct{}, len(list))
+ upIds := make([]int64, 0, len(list))
+ downIds := make([]int64, 0, len(ids))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ foundIds[item.Id] = struct{}{}
+ if item.Status == playletmod.StatusUp {
+ upIds = append(upIds, item.Id)
+ } else {
+ downIds = append(downIds, item.Id)
+ }
+ }
+ for _, id := range ids {
+ if _, ok := foundIds[id]; !ok {
+ downIds = append(downIds, id)
+ }
+ }
+ if err1 := SyncPlayletStatusToES(ctx, upIds, playletmod.StatusUp); err1 != nil {
+ return err1
+ }
+ if err1 := SyncPlayletStatusToES(ctx, downIds, playletmod.StatusDown); err1 != nil {
+ return err1
+ }
+ return nil
+}
+
+// SyncPlayletStatusToES 根据短剧上下架状态同步ES搜索数据
+func SyncPlayletStatusToES(ctx context.Context, ids []int64, status playletmod.Status) *srvdef.SrvErr {
+ ids = dedupePlayletESIds(ids)
+ if len(ids) == 0 {
+ return nil
+ }
+ if esmanager.Instance == nil {
+ log.Error("短剧ES同步失败,ES未初始化", log.Any("短剧ID", ids), log.Any("状态", status))
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ switch status {
+ case playletmod.StatusUp:
+ return upsertPlayletsToES(ctx, ids)
+ case playletmod.StatusDown:
+ return deletePlayletsFromES(ids)
+ default:
+ return nil
+ }
+}
+
+func getPlayletData(ctx context.Context, minTime, endTime *time.Time) (playletmod.PlayletList, *srvdef.SrvErr) {
+ pageNum := int64(1)
+ result := make(playletmod.PlayletList, 0)
+ filter := bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ if minTime != nil && endTime != nil {
+ filter[playletmod.FN.Playlet.UpdatedAt] = bson.M{
+ "$gte": minTime,
+ "$lte": endTime,
+ }
+ }
+ for {
+ list, err1 := playletmod.FindManyPlaylet(ctx, filter, options.Find().
+ SetProjection(playletESProjection).
+ SetSort(bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: -1}}).
+ SetSkip((pageNum-1)*playletESPageSize).
+ SetLimit(playletESPageSize))
+ if err1 != nil {
+ return nil, err1
+ }
+ result = append(result, list...)
+ if int64(len(list)) < playletESPageSize {
+ break
+ }
+ pageNum++
+ }
+ return result, nil
+}
+
+func playletToEsData(ctx context.Context, list playletmod.PlayletList) (esmanager.M, *srvdef.SrvErr) {
+ categoryNameMap, err1 := buildPlayletESCategoryNameMap(ctx, list)
+ if err1 != nil {
+ return nil, err1
+ }
+ themeNameMap, err1 := buildPlayletESThemeNameMap(ctx, list)
+ if err1 != nil {
+ return nil, err1
+ }
+ tagNameMap, err1 := buildPlayletESTagNameMap(ctx, list)
+ if err1 != nil {
+ return nil, err1
+ }
+ result := make(esmanager.M, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result[strconv.FormatInt(item.Id, 10)] = playletToESDoc(item, categoryNameMap, themeNameMap, tagNameMap)
+ }
+ return result, nil
+}
+
+// upsertPlayletsToES 批量写入已上架短剧ES文档
+func upsertPlayletsToES(ctx context.Context, ids []int64) *srvdef.SrvErr {
+ list, err1 := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": ids},
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.Find().SetProjection(playletESProjection))
+ if err1 != nil {
+ return err1
+ }
+ if len(list) == 0 {
+ return nil
+ }
+ esData, err1 := playletToEsData(ctx, list)
+ if err1 != nil {
+ return err1
+ }
+ if len(esData) == 0 {
+ return nil
+ }
+ if err1 = AddMultiDataToES(esmod.GetPlayletIndex(), esData); err1 != nil {
+ log.Error("批量写入短剧ES失败", log.Any("短剧ID", ids), log.E(err1))
+ return err1
+ }
+ log.Info("批量写入短剧ES成功", log.Any("数量", len(esData)), log.Any("短剧ID", ids))
+ return nil
+}
+
+// deletePlayletsFromES 批量删除短剧ES文档
+func deletePlayletsFromES(ids []int64) *srvdef.SrvErr {
+ strIds := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if id <= 0 {
+ continue
+ }
+ strIds = append(strIds, strconv.FormatInt(id, 10))
+ }
+ if len(strIds) == 0 {
+ return nil
+ }
+ if err1 := batchDelFromES(esmod.GetPlayletIndex(), strIds); err1 != nil {
+ log.Error("批量删除短剧ES失败", log.Any("短剧ID", ids), log.E(err1))
+ return err1
+ }
+ log.Info("批量删除短剧ES成功", log.Any("数量", len(strIds)), log.Any("短剧ID", ids))
+ return nil
+}
+
+// playletToESDoc 转换短剧Mongo数据为ES文档
+func playletToESDoc(item *playletmod.Playlet, categoryNameMap map[int64]string, themeNameMap map[int64]string, tagNameMap map[int64]string) *esmod.ESPlaylet {
+ return &esmod.ESPlaylet{
+ Id: item.Id,
+ Status: item.Status,
+ IsFeatured: item.IsFeatured,
+ IsRecommend: item.IsRecommend,
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: item.Banner,
+ OnlineAt: item.OnlineAt,
+ CategoryIds: item.CategoryIds,
+ CategoryNames: playletESNamesByIds(item.CategoryIds, categoryNameMap),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: playletESNamesByIds(item.ThemeIds, themeNameMap),
+ Title: item.Title,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: item.DisplayCover(),
+ TagIds: item.TagIds,
+ TagNames: playletESNamesByIds(item.TagIds, tagNameMap),
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.DisplayCountBrowse(),
+ HeatValue: item.DisplayHeatValue(),
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike + item.VirtualCountLike,
+ SeoInfo: item.SeoInfo,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+// buildPlayletESCategoryNameMap 批量查询短剧分类名称
+func buildPlayletESCategoryNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.CategoryIds
+ })
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ categories, err1 := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
+ }))
+ if err1 != nil {
+ return nil, err1
+ }
+ for _, category := range categories {
+ if category != nil {
+ result[category.Id] = category.Name
+ }
+ }
+ return result, nil
+}
+
+// buildPlayletESThemeNameMap 批量查询短剧题材名称
+func buildPlayletESThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.ThemeIds
+ })
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ themes, err1 := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
+ }))
+ if err1 != nil {
+ return nil, err1
+ }
+ for _, theme := range themes {
+ if theme != nil {
+ result[theme.Id] = theme.Name
+ }
+ }
+ return result, nil
+}
+
+// buildPlayletESTagNameMap 批量查询短剧标签名称
+func buildPlayletESTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.TagIds
+ })
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ tags, err1 := contenttagmod.GetMapTags(ctx, ids)
+ if err1 != nil {
+ return nil, err1
+ }
+ for _, tag := range tags {
+ if tag != nil {
+ result[tag.Id] = tag.Name
+ }
+ }
+ return result, nil
+}
+
+// collectPlayletESIds 汇总并去重短剧关联ID
+func collectPlayletESIds(list playletmod.PlayletList, pick func(item *playletmod.Playlet) []int64) []int64 {
+ result := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, id := range pick(item) {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ }
+ return result
+}
+
+func dedupePlayletESIds(ids []int64) []int64 {
+ result := make([]int64, 0, len(ids))
+ seen := make(map[int64]struct{}, len(ids))
+ for _, id := range ids {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
+
+// playletESNamesByIds 按ID顺序返回ES文档名称集合
+func playletESNamesByIds(ids []int64, nameMap map[int64]string) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if name := nameMap[id]; name != "" {
+ result = append(result, name)
+ }
+ }
+ return result
+}
diff --git a/web/service/srv_search/esactress.go b/web/service/srv_search/esactress.go
new file mode 100644
index 0000000..e8cbb21
--- /dev/null
+++ b/web/service/srv_search/esactress.go
@@ -0,0 +1,77 @@
+package srv_search
+
+import (
+ "context"
+ "strconv"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/esmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func getActressEsData(ctx context.Context, minTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr) {
+ data, serr := getActressData(ctx, minTime, endTime)
+ if serr != nil {
+ return nil, serr
+ }
+ return actressToEsData(data), nil
+}
+func getActressData(ctx context.Context, minTime, endTime *time.Time) ([]*actress.Actress, *srvdef.SrvErr) {
+ pageNumber := 1
+ pageSize := 1000
+ rsl := make([]*actress.Actress, 0)
+ filter := bson.M{}
+ if minTime != nil && endTime != nil {
+ filter[actress.FN.Actress.UpdatedAt] = bson.M{
+ "$gte": minTime,
+ "$lte": endTime,
+ }
+ }
+ for {
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{actress.FN.Actress.UpdatedAt: -1})
+ opts.SetSkip(int64(pageSize * (pageNumber - 1)))
+ opts.SetLimit(int64(pageSize))
+ data, serr := actress.FindManyActresss(ctx, filter, opts)
+ if serr != nil {
+ return nil, serr
+ }
+ rsl = append(rsl, data...)
+ if len(data) < pageSize {
+ break
+ }
+ pageNumber++
+ }
+ return rsl, nil
+}
+func actressToEsData(data []*actress.Actress) esmanager.M {
+ rsl := make(map[string]interface{}, len(data))
+ for _, v := range data {
+ esInfo := esmod.ESActress{
+ Id: v.Id,
+ Alias: v.Alias,
+ Avatar: v.Avatar,
+ Meas: v.Meas,
+ Desc: v.Desc,
+ Office: v.Office,
+ DebutDate: v.DebutDate,
+ BornDate: v.BornDate,
+ Region: v.Region,
+ Score: v.Score,
+ Works: v.Works,
+ HotValue: v.HotValue,
+ Real: v.Real,
+ Status: v.Status,
+ CreatedAt: v.CreatedAt,
+ UpdatedAt: v.UpdatedAt,
+ TranslationCategoryId: v.TranslationCategoryId,
+ Rank: v.Rank,
+ }
+ rsl[strconv.FormatInt(v.Id, 10)] = esInfo
+ }
+ return rsl
+}
diff --git a/web/service/srv_search/esmedia.go b/web/service/srv_search/esmedia.go
new file mode 100644
index 0000000..69db9c0
--- /dev/null
+++ b/web/service/srv_search/esmedia.go
@@ -0,0 +1,92 @@
+package srv_search
+
+import (
+ "context"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func getVideoEsData(ctx context.Context, minTime, endTime *time.Time, esIndex string) (esmanager.M, *srvdef.SrvErr) {
+ data, serr := getVideoData(ctx, minTime, endTime)
+ if serr != nil {
+ return nil, serr
+ }
+ return videoToEsData(data), nil
+}
+func getVideoData(ctx context.Context, minTime, endTime *time.Time) ([]*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
+ pageNumber := 1
+ pageSize := 1000
+ rsl := make([]*videoinformationmod.VideoInformation, 0)
+ filter := bson.M{}
+ if minTime != nil && endTime != nil {
+ filter[videoinformationmod.FN.VideoInformation.UpdatedAt] = bson.M{
+ "$gte": minTime,
+ "$lte": endTime,
+ }
+ }
+ for {
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1})
+ opts.SetSkip(int64(pageSize * (pageNumber - 1)))
+ opts.SetLimit(int64(pageSize))
+ data, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if serr != nil {
+ return nil, serr
+ }
+ rsl = append(rsl, data...)
+ if len(data) < pageSize {
+ break
+ }
+ pageNumber++
+ }
+ return rsl, nil
+}
+func videoToEsData(data []*videoinformationmod.VideoInformation) esmanager.M {
+ rsl := make(map[string]interface{}, len(data))
+ for _, v := range data {
+ esInfo := esmod.ESVideo{
+ Id: v.Id,
+ Title: v.Title,
+ MediaType: v.MediaType,
+ AddedTime: v.AddedTime,
+ CreatorName: v.CreatorName,
+ Keyworlds: "",
+ Desc: v.Desc,
+ Remark: v.Remark,
+ Status: v.Status,
+ IsHot: v.IsHot,
+ Sort: v.Sort,
+ WatchTimes: v.Real.WatchTimes,
+ ShowTimes: v.Real.ShowTimes,
+ Comments: v.Real.Comments,
+ UpdatedAt: v.UpdatedAt,
+ CreatedAt: v.CreatedAt,
+ Keyworld: v.Keyworld,
+ CreatorId: v.CreatorId,
+ FirstTag: v.FirstTag,
+ SecondTags: v.SecondTags,
+ CoverImg: v.CoverImg,
+ Nodes: v.Nodes,
+ Video: v.Video,
+ Tags: v.Tags,
+ Real: v.Real,
+ VideoCount: v.VideoCount,
+ IssuedBrand: v.IssuedBrand,
+ Bango: v.Bango,
+ IssuedAt: v.IssuedAt,
+ }
+ if v.Keyworld != nil && len(v.Keyworld) > 0 {
+ esInfo.Keyworlds = strings.Join(v.Keyworld, " ")
+ }
+ rsl[strconv.FormatInt(v.Id, 10)] = esInfo
+ }
+ return rsl
+}
diff --git a/web/service/srv_search/search.go b/web/service/srv_search/search.go
new file mode 100644
index 0000000..0a95247
--- /dev/null
+++ b/web/service/srv_search/search.go
@@ -0,0 +1,27 @@
+package srv_search
+
+import (
+ "github.com/robfig/cron/v3"
+ "videoweb/pub/common/log"
+)
+
+var (
+ skdCron *cron.Cron
+ batchSize int = 100
+ opMaxDiff int32 = 10
+)
+
+func Start() {
+ skdCron = cron.New(cron.WithSeconds())
+ log.Info("srv_search:Start...")
+ defer log.Info("srv_search:Start!!!")
+ go func() {
+ StartES()
+ }()
+}
+
+func Stop() {
+ log.Info("srv_search:Stop...")
+ defer log.Info("srv_search:Stop!!!")
+ StopES()
+}
\ No newline at end of file
diff --git a/web/service/srv_search/updater.go b/web/service/srv_search/updater.go
new file mode 100644
index 0000000..193a113
--- /dev/null
+++ b/web/service/srv_search/updater.go
@@ -0,0 +1,229 @@
+package srv_search
+
+import (
+ "context"
+ "fmt"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "strconv"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/mongos"
+)
+
+// resetAllDocsToES 重设所有文档到数据库
+func resetAllDocsToES(ctx context.Context, param *Param) *srvdef.SrvErr {
+ log.Info("srv_search:resetAllDocsToES...", log.Any("ESIndex", param.EsIndex))
+ defer log.Info("srv_search:startActress!!!")
+ //删除所有记录
+ delAllFromES(param.EsIndex)
+
+ var pageNum int64 = 1
+ var pageSize int64 = 200
+ updSize := 0
+ for {
+ findOpt := constructFindOption(pageNum, pageSize)
+ docs, serr := findManyFromDb(ctx, param, findOpt)
+ if serr != nil {
+ return serr
+ }
+ if len(docs) == 0 {
+ break
+ }
+ updSize = updSize + len(docs)
+ updateESData, serr := sliceMapToJsonMap(docs, param)
+ log.Info("srv_search:resetAllDocsToES", log.Any("updateESData", updateESData))
+ if serr = batchAddToES(param.EsIndex, updateESData); serr != nil {
+ return serr
+ }
+ pageNum++
+ log.Info("resetAllToES>>>>>>>>>>>>>>>>>>", log.Any("updateSize:", updSize))
+ }
+ if updSize > 0 {
+ if err := esmanager.Instance.Refresh(param.EsIndex); err != nil {
+ log.Warn("srv_search:resetAllDocsToES Refresh failed", log.Any("ESIndex", param.EsIndex), log.E(err))
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ }
+ return nil
+}
+
+// constructMatch 构建match
+func constructMatch(param *Param) bson.M {
+ updateFilter := bson.A{}
+ for _, fieldName := range param.CollFields {
+ filter := bson.M{"updateDescription.updatedFields." + fieldName: bson.M{"$exists": true}}
+ updateFilter = append(updateFilter, filter)
+ }
+ match := bson.M{"$or": bson.A{
+ bson.M{"operationType": bson.M{"$in": []string{"delete", "insert", "replace"}}},
+ bson.M{"$and": bson.A{
+ bson.M{"operationType": bson.M{"$in": []string{"update"}}},
+ bson.M{"$or": updateFilter}},
+ }}}
+ return match
+}
+
+// constructActtressOption 查找选项
+func constructFindOption(pageNum int64, pageSize int64) *options.FindOptions {
+ opt := &options.FindOptions{}
+ opt.SetSort(bson.M{"createdAt": -1})
+ skip := (pageNum - 1) * pageSize
+ opt.SetSkip(skip)
+ opt.SetLimit(pageSize)
+ return opt
+}
+
+// AddMultiDataToES 添加一批Ex
+func AddMultiDataToES(esIndex string, esvs esmanager.M) *srvdef.SrvErr {
+ if err := esmanager.Instance.Bulk(esIndex, esvs); err != nil {
+ info := fmt.Sprintf("(srv_search:AddMultiDataToES) ES Bulk failed! err:%+v:", err)
+ log.Warn(info)
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// findManyFromDb 查找一批数据
+func findManyFromDb(ctx context.Context, param *Param,
+ opts ...*options.FindOptions) ([]bson.M, *srvdef.SrvErr) {
+ docs := make([]bson.M, 0)
+ if err := param.Coll.Find(ctx, &docs, param.CollFilter, opts...); err != nil {
+ log.Warn("srv_search:findManyFromDb db.Find failed", log.E(err), log.Any("filter", param.CollFilter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return docs, nil
+}
+
+// sliceMapToJsonMap 从map切片构建Map对象
+func sliceMapToJsonMap(docs []bson.M, param *Param) (esmanager.M, *srvdef.SrvErr) {
+ esMap := make(esmanager.M)
+ for _, doc := range docs {
+ fieldMap := make(esmanager.M)
+ for key, value := range doc {
+ if utils.IncludeString(param.CollFields, key) {
+ fieldMap[key] = value
+ } else if key == "_id" {
+ fieldMap["id"] = value
+ }
+ }
+ if id, ok := doc["_id"].(int64); ok {
+ strId := strconv.FormatInt(id, 10)
+ esMap[strId] = fieldMap
+ } else {
+ log.Warn("srv_search:sliceMapToJsonMap Key doesn't exist", log.Any("keyId", "_id"))
+ }
+ }
+ return esMap, nil
+}
+
+// getInfoFromChangeData 从改变流中获取数据信息
+func getInfoFromChangeData(changeData bson.M, param *Param) (
+ string, *esmod.ESTimestamp, string, esmanager.M, *srvdef.SrvErr) {
+ operationType, ok := changeData["operationType"].(string)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData operationType type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ ptimestamp, ok := changeData["clusterTime"].(primitive.Timestamp)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData clusterTime type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ estimestamp := &esmod.ESTimestamp{T: ptimestamp.T, I: ptimestamp.I}
+ documentKey, ok := changeData["documentKey"].(primitive.M)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData documentKey type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ docKey, ok := documentKey["_id"].(int64)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData documentKey[_id] type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ strId := strconv.FormatInt(docKey, 10)
+ esDocMap := make(esmanager.M)
+ if changeData["fullDocument"] != nil {
+ docMap, ok := changeData["fullDocument"].(primitive.M)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData fullDocument type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+
+ for key, value := range docMap {
+ if utils.IncludeString(param.CollFields, key) {
+ esDocMap[key] = value
+ } else if key == "_id" {
+ esDocMap["id"] = value
+ }
+ }
+ }
+ return operationType, estimestamp, strId, esDocMap, nil
+}
+
+// delAllFromES 删除所有数据
+func delAllFromES(esIndex string) *srvdef.SrvErr {
+ query := esmanager.M{
+ "query": esmanager.M{
+ "match_all": esmanager.M{},
+ },
+ }
+ if err := esmanager.Instance.DelMany([]string{esIndex}, query); err != nil {
+ log.Warn(fmt.Sprintf("(srv_search:DelAllFromEs) Es DelMany failed! err:%+v:", err),
+ log.Any("query", query))
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// batchAddToES 添加一批数据到ES
+func batchAddToES(esIndex string, esvs esmanager.M) *srvdef.SrvErr {
+ log.Info("srv_search:resetAllDocsToES", log.Any("esIndex", esIndex), log.Any("esvs", esvs))
+ if err := esmanager.Instance.Bulk(esIndex, esvs); err != nil {
+ info := fmt.Sprintf("(srv_search:batchAddToES) ES Bulk failed! err:%+v:", err)
+ log.Warn(info)
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// batchDelFromES 删除一批数据从ES
+func batchDelFromES(esIndex string, ids []string) *srvdef.SrvErr {
+ query := esmanager.M{
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {
+ "ids": esmanager.M{
+ "values": ids,
+ },
+ },
+ },
+ },
+ },
+ }
+ if err := esmanager.Instance.DelMany([]string{esIndex}, query); err != nil {
+ info := fmt.Sprintf(
+ "(srv_search:batchDelFromES) Es DelMany failed! err:%+v:", err)
+ log.Warn(info)
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// watchOpLog 监控表变化
+func watchOpLog(ctx context.Context, db *mongos.Collection, pipeline []bson.M,
+ opt *options.ChangeStreamOptions) (*mongo.ChangeStream, *srvdef.SrvErr) {
+ changeStream, err := db.WatchOpLog(ctx, pipeline, opt)
+ if err != nil {
+ log.Warn("srv_search:watchOpLog db.WatchOpLog failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return changeStream, nil
+}
diff --git a/web/service/srv_seopush/notify.go b/web/service/srv_seopush/notify.go
new file mode 100644
index 0000000..f9a8e60
--- /dev/null
+++ b/web/service/srv_seopush/notify.go
@@ -0,0 +1,60 @@
+package srv_seopush
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/seo/sitemapdirty"
+)
+
+// MarkSitemapDirty 通知 app 进程重新生成 sitemap。
+func MarkSitemapDirty(ctx context.Context) {
+ sitemapdirty.Mark(ctx)
+}
+
+// NotifyPlayletAfterWrite 短剧写入后:刷新 sitemap 标记。
+// 搜索引擎只在新上架(含新增即上架)或从已上架变为下架时推送,普通改内容不推。
+func NotifyPlayletAfterWrite(ctx context.Context, playletId int64, prevStatus *playletmod.Status, status playletmod.Status, onlineAt time.Time) {
+ MarkSitemapDirty(ctx)
+ wasUp := prevStatus != nil && *prevStatus == playletmod.StatusUp
+ if status == playletmod.StatusUp {
+ if !wasUp {
+ EnqueueUpdated(ctx, KindPlaylet, playletId, PageVisiblePlaylet(status, onlineAt))
+ }
+ return
+ }
+ if wasUp {
+ EnqueueDeleted(ctx, KindPlaylet, playletId)
+ }
+}
+
+// NotifyPlayletAfterStatus 批量上下架后的 SEO 通知
+func NotifyPlayletAfterStatus(ctx context.Context, ids []int64, status playletmod.Status, onlineAt time.Time) {
+ MarkSitemapDirty(ctx)
+ if status == playletmod.StatusUp {
+ for _, id := range ids {
+ EnqueueUpdated(ctx, KindPlaylet, id, PageVisiblePlaylet(status, onlineAt))
+ }
+ return
+ }
+ for _, id := range ids {
+ EnqueueDeleted(ctx, KindPlaylet, id)
+ }
+}
+
+// NotifyInfoAfterWrite 视频写入后的 SEO 通知。
+// 搜索引擎只在新上架或从已上架变为下架时推送,普通改内容不推。
+func NotifyInfoAfterWrite(ctx context.Context, kind string, id int64, prevStatus, status pubdef.InformationStatus) {
+ MarkSitemapDirty(ctx)
+ wasUp := prevStatus == pubdef.InformationStatusVerifyOK
+ if PageVisibleInfo(status) {
+ if !wasUp {
+ EnqueueUpdated(ctx, kind, id, true)
+ }
+ return
+ }
+ if wasUp {
+ EnqueueDeleted(ctx, kind, id)
+ }
+}
diff --git a/web/service/srv_seopush/push.go b/web/service/srv_seopush/push.go
new file mode 100644
index 0000000..1260ac9
--- /dev/null
+++ b/web/service/srv_seopush/push.go
@@ -0,0 +1,347 @@
+// Package srv_seopush 负责内容新上架/下架时向搜索引擎推送页面地址。
+// 业务回调里即时推送一次,失败的任务由 web/skd/job/seopush 兜底重试。
+package srv_seopush
+
+import (
+ "context"
+ "errors"
+ "strings"
+ "time"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/seopushmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/seo/googleindex"
+ seoindexnow "videoweb/pub/seo/indexnow"
+ "videoweb/web/service/srv_cfg"
+ "videoweb/web/webg"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ KindVideo = "video"
+ KindPlaylet = "playlet"
+)
+
+// channelFields 把两个推送通道的字段名抽象出来,共用同一套状态更新逻辑
+type channelFields struct {
+ name string
+ status string
+ retry string
+ errMsg string
+ at string
+}
+
+var googleChannel = channelFields{
+ name: "google",
+ status: seopushmod.FN.SeoPushTask.GoogleStatus,
+ retry: seopushmod.FN.SeoPushTask.GoogleRetry,
+ errMsg: seopushmod.FN.SeoPushTask.GoogleErr,
+ at: seopushmod.FN.SeoPushTask.GoogleAt,
+}
+
+var indexNowChannel = channelFields{
+ name: "indexnow",
+ status: seopushmod.FN.SeoPushTask.IndexNowStatus,
+ retry: seopushmod.FN.SeoPushTask.IndexNowRetry,
+ errMsg: seopushmod.FN.SeoPushTask.IndexNowErr,
+ at: seopushmod.FN.SeoPushTask.IndexNowAt,
+}
+
+func kindPrefix(kind string) string {
+ switch kind {
+ case KindVideo:
+ return "/video-detail"
+ case KindPlaylet:
+ return "/drama-detail"
+ default:
+ return ""
+ }
+}
+
+// DetailURL 拼装详情页完整地址,路径与 sitemap 一致:/{prefix}/{slug},无 slug 回退 ID。
+func DetailURL(webAddr, prefix, slug string, id int64) string {
+ seg := utils.PathSegment(slug, id)
+ if seg == "" {
+ return ""
+ }
+ return utils.JoinUrlPathKeepSlash(webAddr, strings.TrimRight(prefix, "/")+"/"+seg)
+}
+
+// PageVisiblePlaylet 判断短剧详情页当前是否对外可访问。
+func PageVisiblePlaylet(status playletmod.Status, onlineAt time.Time) bool {
+ if status != playletmod.StatusUp {
+ return false
+ }
+ if onlineAt.IsZero() {
+ return true
+ }
+ return !onlineAt.After(time.Now())
+}
+
+// PageVisibleInfo 判断视频详情页是否已上架。
+func PageVisibleInfo(status pubdef.InformationStatus) bool {
+ return status == pubdef.InformationStatusVerifyOK
+}
+
+// SEOBaseURL 取站点对外地址
+func SEOBaseURL(ctx context.Context) string {
+ sysCfg, serr := srv_cfg.GetSystemConfig(ctx)
+ if serr != nil || sysCfg == nil {
+ log.Warn("seopush: get system config failed", log.E(serr))
+ return ""
+ }
+ base := sysCfg.SEOBaseURL()
+ if base == "" {
+ log.Warn("seopush: SEO base URL is empty, skip push")
+ }
+ return base
+}
+
+// Enabled 业务开关是否打开(不含环境判断)。
+func Enabled() bool {
+ return pubdef.Enabled()
+}
+
+func canPushExternally() bool {
+ return pubdef.Enabled() && webg.Conf.RunMod == "prod"
+}
+
+// EnqueueUpdated 内容新上架后登记并立即推送,仅在页面已可访问时生效
+func EnqueueUpdated(ctx context.Context, kind string, id int64, visible bool) {
+ if !pubdef.Enabled() || !visible {
+ return
+ }
+ enqueue(ctx, kind, id, seopushmod.ActionUpdated, true)
+}
+
+// RegisterUpdated 只登记不立即推送,供批量补推使用
+func RegisterUpdated(ctx context.Context, kind string, id int64, visible bool) {
+ if !pubdef.Enabled() || !visible {
+ return
+ }
+ enqueue(ctx, kind, id, seopushmod.ActionUpdated, false)
+}
+
+// EnqueueDeleted 内容下架/删除后登记并立即推送
+func EnqueueDeleted(ctx context.Context, kind string, id int64) {
+ if !pubdef.Enabled() {
+ return
+ }
+ enqueue(ctx, kind, id, seopushmod.ActionDeleted, true)
+}
+
+func enqueue(ctx context.Context, kind string, id int64, action string, pushNow bool) {
+ webAddr := SEOBaseURL(ctx)
+ if webAddr == "" {
+ return
+ }
+ prefix := kindPrefix(kind)
+ if prefix == "" {
+ return
+ }
+ pageURL := DetailURL(webAddr, prefix, loadSlug(ctx, kind, id), id)
+ if pageURL == "" {
+ return
+ }
+ now := time.Now()
+ task := &seopushmod.SeoPushTask{
+ Url: pageURL,
+ VideoId: id,
+ Action: action,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if action == seopushmod.ActionDeleted {
+ _, _ = seopushmod.DelManySeoPushTask(ctx, bson.M{
+ seopushmod.FN.SeoPushTask.Url: pageURL,
+ seopushmod.FN.SeoPushTask.Action: seopushmod.ActionUpdated,
+ })
+ }
+ if serr := seopushmod.UpsertPendingSeoPushTask(ctx, task); serr != nil {
+ log.Warn("seopush: enqueue task failed", log.Any("url", pageURL), log.E(serr))
+ return
+ }
+ if action == seopushmod.ActionUpdated {
+ markQueued(ctx, kind, id)
+ }
+ if !pushNow {
+ return
+ }
+
+ godef.Go(func() {
+ PushTask(context.Background(), webAddr, task)
+ })
+}
+
+func loadSlug(ctx context.Context, kind string, id int64) string {
+ if id <= 0 {
+ return ""
+ }
+ switch kind {
+ case KindVideo:
+ item, err := videoinformationmod.FindOneVideoInformation(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Id: id},
+ options.FindOne().SetProjection(bson.D{{Key: videoinformationmod.FN.VideoInformation.Slug, Value: 1}}))
+ if err != nil || item == nil {
+ return ""
+ }
+ return strings.TrimSpace(item.Slug)
+ case KindPlaylet:
+ item, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: id},
+ options.FindOne().SetProjection(bson.D{{Key: playletmod.FN.Playlet.Slug, Value: 1}}))
+ if err != nil || item == nil {
+ return ""
+ }
+ return strings.TrimSpace(item.Slug)
+ default:
+ return ""
+ }
+}
+
+func markQueued(ctx context.Context, kind string, id int64) {
+ var serr error
+ switch kind {
+ case KindVideo:
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Id: id},
+ bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.IndexNow: int64(1)}})
+ case KindPlaylet:
+ serr = playletmod.UpdateOnePlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: id},
+ bson.M{"$set": bson.M{playletmod.FN.Playlet.IndexNow: int64(1)}})
+ }
+ if serr != nil {
+ log.Warn("seopush: mark queued failed", log.Any("kind", kind), log.Any("id", id), log.E(serr))
+ }
+}
+
+// PushTask 推送任务中尚未成功的通道
+func PushTask(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
+ if task == nil || !pubdef.Enabled() {
+ return
+ }
+ if strings.Contains(task.Url, "?") {
+ log.Warn("seopush: skip query-url task", log.Any("url", task.Url), log.Any("action", task.Action))
+ return
+ }
+ if !canPushExternally() {
+ dryRun(ctx, task)
+ return
+ }
+ if task.GoogleStatus != seopushmod.PushStatusSuccess {
+ pushGoogle(ctx, task)
+ }
+ if task.IndexNowStatus != seopushmod.PushStatusSuccess {
+ pushIndexNow(ctx, webAddr, task)
+ }
+}
+
+func dryRun(ctx context.Context, task *seopushmod.SeoPushTask) {
+ log.Info("seopush: dry-run, external push skipped in non-prod",
+ log.Any("url", task.Url), log.Any("action", task.Action))
+ if task.GoogleStatus != seopushmod.PushStatusSuccess {
+ markSuccess(ctx, task, googleChannel)
+ }
+ if task.IndexNowStatus != seopushmod.PushStatusSuccess {
+ markSuccess(ctx, task, indexNowChannel)
+ }
+}
+
+func pushGoogle(ctx context.Context, task *seopushmod.SeoPushTask) {
+ notifyType := googleindex.TypeUpdated
+ if task.Action == seopushmod.ActionDeleted {
+ notifyType = googleindex.TypeDeleted
+ }
+
+ resp, err := googleindex.Publish(ctx, task.Url, notifyType)
+ if err == nil {
+ markSuccess(ctx, task, googleChannel)
+ log.Info("seopush=google=pushed",
+ log.Any("url", task.Url),
+ log.Any("type", notifyType),
+ log.Any("resp", resp))
+ return
+ }
+ if errors.Is(err, googleindex.ErrQuotaExceeded) {
+ log.Warn("seopush=google=quota_exceeded",
+ log.Any("url", task.Url), log.Any("resp", err.Error()))
+ return
+ }
+ if errors.Is(err, googleindex.ErrNotEnabled) {
+ log.Warn("seopush=google=not_ready", log.Any("url", task.Url))
+ return
+ }
+ markFailed(ctx, task, googleChannel, err)
+}
+
+func pushIndexNow(ctx context.Context, webAddr string, task *seopushmod.SeoPushTask) {
+ if strings.TrimSpace(webAddr) == "" {
+ return
+ }
+ config, err := seoindexnow.BuildConfig(webAddr)
+ if err != nil {
+ log.Warn("seopush: build indexnow config failed", log.Any("webAddr", webAddr), log.E(err))
+ return
+ }
+
+ result := seoindexnow.NewBatchIndexNowSubmitter(config).SubmitBatch([]string{task.Url})
+ if result.SuccessCount > 0 {
+ markSuccess(ctx, task, indexNowChannel)
+ log.Info("seopush=indexnow=pushed",
+ log.Any("url", task.Url),
+ log.Any("statusCode", result.StatusCodes))
+ return
+ }
+ markFailed(ctx, task, indexNowChannel, errors.New(strings.Join(result.Errors, "; ")))
+}
+
+func markSuccess(ctx context.Context, task *seopushmod.SeoPushTask, ch channelFields) {
+ now := time.Now()
+ if serr := seopushmod.UpdateOneSeoPushTask(ctx, taskFilter(task), bson.M{
+ "$set": bson.M{
+ ch.status: seopushmod.PushStatusSuccess,
+ ch.errMsg: "",
+ ch.at: now,
+ seopushmod.FN.SeoPushTask.UpdatedAt: now,
+ },
+ }); serr != nil {
+ log.Warn("seopush: mark success failed",
+ log.Any("channel", ch.name), log.Any("url", task.Url), log.E(serr))
+ }
+}
+
+func markFailed(ctx context.Context, task *seopushmod.SeoPushTask, ch channelFields, pushErr error) {
+ now := time.Now()
+ errText := ""
+ if pushErr != nil {
+ errText = pushErr.Error()
+ }
+ log.Warn("seopush: push failed",
+ log.Any("channel", ch.name), log.Any("url", task.Url), log.Any("err", errText))
+
+ if serr := seopushmod.UpdateOneSeoPushTask(ctx, taskFilter(task), bson.M{
+ "$set": bson.M{
+ ch.errMsg: errText,
+ ch.at: now,
+ seopushmod.FN.SeoPushTask.UpdatedAt: now,
+ },
+ "$inc": bson.M{ch.retry: 1},
+ }); serr != nil {
+ log.Warn("seopush: mark failed failed",
+ log.Any("channel", ch.name), log.Any("url", task.Url), log.E(serr))
+ }
+}
+
+func taskFilter(task *seopushmod.SeoPushTask) bson.M {
+ return bson.M{
+ seopushmod.FN.SeoPushTask.Url: task.Url,
+ seopushmod.FN.SeoPushTask.Action: task.Action,
+ }
+}
diff --git a/web/service/srv_slug/sync.go b/web/service/srv_slug/sync.go
new file mode 100644
index 0000000..e94c8fc
--- /dev/null
+++ b/web/service/srv_slug/sync.go
@@ -0,0 +1,403 @@
+package srv_slug
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ syncSlugDefaultSize = int64(500)
+ syncSlugMaxSize = int64(2000)
+
+ TypeVideo = "video"
+ TypeVideoCategory = "videoCategory"
+ TypeActress = "actress"
+ TypeVideoCreater = "videoCreater"
+ TypePlaylet = "playlet"
+ TypePlayletCategory = "playletCategory"
+ TypePlayletTheme = "playletTheme"
+ TypeContentTag = "contentTag"
+)
+
+type slugJob struct {
+ kind string
+ set func(*proto.SlugSyncResp, int64)
+ run func(context.Context, int64) (int64, bool, *srvdef.SrvErr)
+ hasMore func(context.Context) bool
+}
+
+// SyncEmptySlugs 为视频、分类、女优、作者、短剧、统一标签中空 slug 自动生成。可重复调用,hasMore 为 true 时继续请求。
+func SyncEmptySlugs(ctx context.Context, req *proto.SlugSyncReq) (*proto.SlugSyncResp, *srvdef.SrvErr) {
+ maxSize := syncSlugDefaultSize
+ if req != nil && req.MaxSize > 0 {
+ maxSize = req.MaxSize
+ }
+ if maxSize > syncSlugMaxSize {
+ maxSize = syncSlugMaxSize
+ }
+ kind := ""
+ if req != nil {
+ kind = strings.TrimSpace(req.Type)
+ }
+
+ jobs := allSlugJobs()
+ if kind != "" {
+ job, ok := jobByKind(jobs, kind)
+ if !ok {
+ return nil, srvdef.New(srvdef.ErrParamError, "不支持的type", "不支持的type")
+ }
+ jobs = []slugJob{job}
+ }
+
+ resp := &proto.SlugSyncResp{}
+ remain := maxSize
+ for i, job := range jobs {
+ if remain <= 0 {
+ resp.HasMore = anyHasEmpty(ctx, jobs[i:])
+ resp.TotalUpdated = sumUpdated(resp)
+ return resp, nil
+ }
+ updated, more, err := job.run(ctx, remain)
+ if err != nil {
+ return nil, err
+ }
+ job.set(resp, updated)
+ remain -= updated
+ if more {
+ resp.HasMore = true
+ resp.TotalUpdated = sumUpdated(resp)
+ return resp, nil
+ }
+ }
+ resp.TotalUpdated = sumUpdated(resp)
+ return resp, nil
+}
+
+func allSlugJobs() []slugJob {
+ return []slugJob{
+ {TypeVideoCategory, func(r *proto.SlugSyncResp, n int64) { r.VideoCategoryUpdated = n }, syncVideoCategoryEmptySlugs, hasVideoCategoryEmpty},
+ {TypePlayletCategory, func(r *proto.SlugSyncResp, n int64) { r.PlayletCategoryUpdated = n }, syncPlayletCategoryEmptySlugs, hasPlayletCategoryEmpty},
+ {TypePlayletTheme, func(r *proto.SlugSyncResp, n int64) { r.PlayletThemeUpdated = n }, syncPlayletThemeEmptySlugs, hasPlayletThemeEmpty},
+ {TypeContentTag, func(r *proto.SlugSyncResp, n int64) { r.ContentTagUpdated = n }, syncContentTagEmptySlugs, hasContentTagEmpty},
+ {TypeActress, func(r *proto.SlugSyncResp, n int64) { r.ActressUpdated = n }, syncActressEmptySlugs, hasActressEmpty},
+ {TypeVideoCreater, func(r *proto.SlugSyncResp, n int64) { r.VideoCreaterUpdated = n }, syncVideoCreaterEmptySlugs, hasVideoCreaterEmpty},
+ {TypePlaylet, func(r *proto.SlugSyncResp, n int64) { r.PlayletUpdated = n }, syncPlayletEmptySlugs, hasPlayletEmpty},
+ {TypeVideo, func(r *proto.SlugSyncResp, n int64) { r.VideoUpdated = n }, syncVideoEmptySlugs, hasVideoEmpty},
+ }
+}
+
+func jobByKind(jobs []slugJob, kind string) (slugJob, bool) {
+ for _, job := range jobs {
+ if job.kind == kind {
+ return job, true
+ }
+ }
+ return slugJob{}, false
+}
+
+func anyHasEmpty(ctx context.Context, jobs []slugJob) bool {
+ for _, job := range jobs {
+ if job.hasMore(ctx) {
+ return true
+ }
+ }
+ return false
+}
+
+func sumUpdated(resp *proto.SlugSyncResp) int64 {
+ return resp.VideoUpdated + resp.VideoCategoryUpdated + resp.ActressUpdated +
+ resp.VideoCreaterUpdated +
+ resp.PlayletUpdated + resp.PlayletCategoryUpdated +
+ resp.PlayletThemeUpdated + resp.ContentTagUpdated
+}
+
+func findLimit(maxSize int64) *options.FindOptions {
+ return options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).SetLimit(maxSize + 1)
+}
+
+func fillEmptySlugs[T any](
+ ctx context.Context,
+ list []T,
+ maxSize int64,
+ idOf func(T) int64,
+ sourceOf func(T) string,
+ exists func(slug string, excludeId int64) bool,
+ update func(context.Context, int64, string) *srvdef.SrvErr,
+) (int64, bool, *srvdef.SrvErr) {
+ hasMore := int64(len(list)) > maxSize
+ if hasMore {
+ list = list[:maxSize]
+ }
+ var updated int64
+ for _, item := range list {
+ if any(item) == nil {
+ continue
+ }
+ id := idOf(item)
+ if id <= 0 {
+ continue
+ }
+ slug := ""
+ if serr := slugx.Ensure(&slug, sourceOf(item), func(s string) bool {
+ return exists(s, id)
+ }); serr != nil {
+ return updated, hasMore, serr
+ }
+ if serr := update(ctx, id, slug); serr != nil {
+ return updated, hasMore, serr
+ }
+ updated++
+ }
+ return updated, hasMore, nil
+}
+
+func setSlug(ctx context.Context, updateOne func(context.Context, bson.M, bson.M) *srvdef.SrvErr, idField, slugField, updatedAtField string, id int64, slug string) *srvdef.SrvErr {
+ setDoc := bson.M{slugField: slug}
+ if updatedAtField != "" {
+ setDoc[updatedAtField] = time.Now()
+ }
+ return updateOne(ctx, bson.M{idField: id}, bson.M{"$set": setDoc})
+}
+
+func videoEmptySlugFilter(ctx context.Context) bson.M {
+ filter := slugx.EmptyFilter()
+ playletIDs := playletchaptermod.ListLinkedMediaIDs(ctx)
+ if len(playletIDs) == 0 {
+ return filter
+ }
+ return bson.M{
+ "$and": []bson.M{
+ filter,
+ {
+ "$nor": []bson.M{
+ {videoinformationmod.FN.VideoInformation.SyncId: bson.M{"$in": playletIDs}},
+ {videoinformationmod.FN.VideoInformation.Video.FileId: bson.M{"$in": playletIDs}},
+ },
+ },
+ },
+ }
+}
+
+func hasVideoEmpty(ctx context.Context) bool {
+ n, _ := videoinformationmod.CountVideoInformations(ctx, videoEmptySlugFilter(ctx))
+ return n > 0
+}
+func hasVideoCategoryEmpty(ctx context.Context) bool {
+ n, _ := videocategorymod.CountVideoCategorys(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+func hasActressEmpty(ctx context.Context) bool {
+ n, _ := actress.CountActresss(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+func hasVideoCreaterEmpty(ctx context.Context) bool {
+ n, _ := videocreatermod.CountVideoCreaterInfos(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+func hasPlayletEmpty(ctx context.Context) bool {
+ n, _ := playletmod.CountPlaylet(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+func hasPlayletCategoryEmpty(ctx context.Context) bool {
+ n, _ := playletcategorymod.CountPlayletCategory(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+func hasPlayletThemeEmpty(ctx context.Context) bool {
+ n, _ := playletthememod.CountPlayletTheme(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+func hasContentTagEmpty(ctx context.Context) bool {
+ n, _ := contenttagmod.CountContentTag(ctx, slugx.EmptyFilter())
+ return n > 0
+}
+
+func syncVideoEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, videoEmptySlugFilter(ctx), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*videoinformationmod.VideoInformation(list), maxSize,
+ func(item *videoinformationmod.VideoInformation) int64 { return item.Id },
+ func(item *videoinformationmod.VideoInformation) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.Keyworld, item.Title)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
+ videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, videoinformationmod.UpdateOneVideoInformation, videoinformationmod.FN.VideoInformation.Id,
+ videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncVideoCategoryEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := videocategorymod.FindManyVideoCategorys(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*videocategorymod.VideoCategory(list), maxSize,
+ func(item *videocategorymod.VideoCategory) int64 { return item.Id },
+ func(item *videocategorymod.VideoCategory) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := videocategorymod.FindOneVideoCategory(ctx, slugx.Filter(
+ videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, videocategorymod.UpdateOneVideoCategory, videocategorymod.FN.VideoCategory.Id,
+ videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncActressEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := actress.FindManyActresss(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*actress.Actress(list), maxSize,
+ func(item *actress.Actress) int64 { return item.Id },
+ func(item *actress.Actress) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Alias)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := actress.FindOneActress(ctx, slugx.Filter(actress.FN.Actress.Slug, actress.FN.Actress.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, actress.UpdateOneActress, actress.FN.Actress.Id,
+ actress.FN.Actress.Slug, actress.FN.Actress.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncVideoCreaterEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := videocreatermod.FindManyVideoCreaterInfos(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*videocreatermod.VideoCreaterInfo(list), maxSize,
+ func(item *videocreatermod.VideoCreaterInfo) int64 { return item.Id },
+ func(item *videocreatermod.VideoCreaterInfo) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
+ videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, videocreatermod.UpdateOneVideoCreaterInfo, videocreatermod.FN.VideoCreaterInfo.Id,
+ videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncPlayletEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playletmod.FindManyPlaylet(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*playletmod.Playlet(list), maxSize,
+ func(item *playletmod.Playlet) int64 { return item.Id },
+ func(item *playletmod.Playlet) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Title)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := playletmod.FindOnePlaylet(ctx, slugx.Filter(playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, playletmod.UpdateOnePlaylet, playletmod.FN.Playlet.Id,
+ playletmod.FN.Playlet.Slug, playletmod.FN.Playlet.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncPlayletCategoryEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playletcategorymod.FindManyPlayletCategory(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*playletcategorymod.PlayletCategory(list), maxSize,
+ func(item *playletcategorymod.PlayletCategory) int64 { return item.Id },
+ func(item *playletcategorymod.PlayletCategory) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := playletcategorymod.FindOnePlayletCategory(ctx, slugx.Filter(
+ playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, playletcategorymod.UpdateOnePlayletCategory, playletcategorymod.FN.PlayletCategory.Id,
+ playletcategorymod.FN.PlayletCategory.Slug, playletcategorymod.FN.PlayletCategory.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncPlayletThemeEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playletthememod.FindManyPlayletTheme(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*playletthememod.PlayletTheme(list), maxSize,
+ func(item *playletthememod.PlayletTheme) int64 { return item.Id },
+ func(item *playletthememod.PlayletTheme) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := playletthememod.FindOnePlayletTheme(ctx, slugx.Filter(
+ playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, playletthememod.UpdateOnePlayletTheme, playletthememod.FN.PlayletTheme.Id,
+ playletthememod.FN.PlayletTheme.Slug, playletthememod.FN.PlayletTheme.UpdatedAt, id, slug)
+ },
+ )
+}
+
+func syncContentTagEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := contenttagmod.FindManyContentTag(ctx, slugx.EmptyFilter(), findLimit(maxSize))
+ if err != nil {
+ return 0, false, err
+ }
+ return fillEmptySlugs(ctx, []*contenttagmod.ContentTag(list), maxSize,
+ func(item *contenttagmod.ContentTag) int64 { return item.Id },
+ func(item *contenttagmod.ContentTag) string {
+ return utils.SlugSourceFromKeywordsOrTitle(item.SeoInfo.Keyworld, item.Name)
+ },
+ func(s string, excludeId int64) bool {
+ doc, _ := contenttagmod.FindOneContentTag(ctx, slugx.Filter(
+ contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.Id, s, excludeId))
+ return doc != nil
+ },
+ func(ctx context.Context, id int64, slug string) *srvdef.SrvErr {
+ return setSlug(ctx, contenttagmod.UpdateOneContentTag, contenttagmod.FN.ContentTag.Id,
+ contenttagmod.FN.ContentTag.Slug, contenttagmod.FN.ContentTag.UpdatedAt, id, slug)
+ },
+ )
+}
diff --git a/web/service/srv_translation/define.go b/web/service/srv_translation/define.go
new file mode 100644
index 0000000..6c62834
--- /dev/null
+++ b/web/service/srv_translation/define.go
@@ -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"`
+}
diff --git a/web/service/srv_translation/translation.go b/web/service/srv_translation/translation.go
new file mode 100644
index 0000000..5a15dfb
--- /dev/null
+++ b/web/service/srv_translation/translation.go
@@ -0,0 +1,93 @@
+package srv_translation
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetTranslationInfos 按照参数搜索视频标签
+func GetTranslationInfos(ctx context.Context, param *TranslationInfoReq) (*TranslationInfoResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+ resp := &TranslationInfoResp{}
+
+ if param.Id > 0 {
+ filter[translationmod.FN.TranslationInfo.Id] = param.Id
+ }
+ if name := strings.TrimSpace(param.Name); name != "" {
+ filter[translationmod.FN.TranslationInfo.Name] = bson.M{"$regex": name}
+ }
+ if param.TransCategoryId > 0 {
+ filter[translationmod.FN.TranslationInfo.TransCategoryId] = param.TransCategoryId
+ }
+
+ opt.SetSort(bson.M{translationmod.FN.TranslationInfo.Rank: -1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, err1 := translationmod.CountTranslationInfos(ctx, filter)
+ if err1 != nil {
+ return resp, err1
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, err1 := translationmod.FindManyTranslationInfos(ctx, filter, opt)
+ if err1 != nil {
+ return resp, err1
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddTranslationInfo 添加一视频标签
+func AddTranslationInfo(ctx context.Context,
+ ac *translationmod.TranslationInfo) *srvdef.SrvErr {
+
+ ac.Id = mongos.Lid.Translation.Get()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ serr := translationmod.InsertOneTranslationInfo(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+// DelTranslationInfo 删除指定视频标签
+func DelTranslationInfo(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{translationmod.FN.TranslationInfo.Id: id}
+ //删除
+ count, serr := translationmod.DelOneTranslationInfo(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateTranslationInfo 更新指定视频标签
+func UpdateTranslationInfo(ctx context.Context, id int64, param TranslationInfoUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{translationmod.FN.TranslationInfo.Id: id}
+ updateDoc := bson.M{"$set": param}
+ serr := translationmod.UpdateOneTranslationInfo(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
diff --git a/web/service/srv_translationcategory/define.go b/web/service/srv_translationcategory/define.go
new file mode 100644
index 0000000..d21f952
--- /dev/null
+++ b/web/service/srv_translationcategory/define.go
@@ -0,0 +1,35 @@
+package srv_translationcategory
+
+import (
+ "time"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/pubdef"
+)
+
+type TranslationCategoryReq struct {
+ pubdef.Page
+ Id int64 `json:"id" form:"id"` // 导航分类ID
+ Name string `json:"name" form:"name"` // 导航分类名称
+}
+type TranslationCategoryResp struct {
+ Total int64 `json:"total"`
+ List translationcategorymod.TranslationCategoryList `json:"list"`
+}
+type TranslationCategoryUpdate struct {
+ AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
+ Name *string `json:"name" bson:"name,omitempty"` // 名称
+ Icon *string `json:"icon" bson:"icon,omitempty"` // Icon
+ Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
+ Rank *int64 `json:"rank" bson:"rank,omitempty"` //
+ CategoryType *int `json:"categoryType" bson:"categoryType"` // 导航类型 1=url跳转 2=二级导航 3=底部导航
+ UrlValue *string `json:"urlValue" bson:"urlValue"` // 跳转URL
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+}
+type TranslationCategoryUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param TranslationCategoryUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type TranslationCategoryDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_translationcategory/translation.go b/web/service/srv_translationcategory/translation.go
new file mode 100644
index 0000000..7af34b4
--- /dev/null
+++ b/web/service/srv_translationcategory/translation.go
@@ -0,0 +1,90 @@
+package srv_translationcategory
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetTranslationCategorys 按照参数搜索视频标签
+func GetTranslationCategorys(ctx context.Context, param *TranslationCategoryReq) (*TranslationCategoryResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+ resp := &TranslationCategoryResp{}
+
+ if param.Id > 0 {
+ filter[translationcategorymod.FN.TranslationCategory.Id] = param.Id
+ }
+ if name := strings.TrimSpace(param.Name); name != "" {
+ filter[translationcategorymod.FN.TranslationCategory.Name] = bson.M{"$regex": name}
+ }
+
+ opt.SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, err1 := translationcategorymod.CountTranslationCategorys(ctx, filter)
+ if err1 != nil {
+ return resp, err1
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, err1 := translationcategorymod.FindManyTranslationCategorys(ctx, filter, opt)
+ if err1 != nil {
+ return resp, err1
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddTranslationCategory 添加一视频标签
+func AddTranslationCategory(ctx context.Context,
+ ac *translationcategorymod.TranslationCategory) *srvdef.SrvErr {
+
+ ac.Id = mongos.Lid.TransCategory.Get()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ serr := translationcategorymod.InsertOneTranslationCategory(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+// DelTranslationCategory 删除指定视频标签
+func DelTranslationCategory(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{translationcategorymod.FN.TranslationCategory.Id: id}
+ //删除
+ count, serr := translationcategorymod.DelOneTranslationCategory(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateTranslationCategory 更新指定视频标签
+func UpdateTranslationCategory(ctx context.Context, id int64, param TranslationCategoryUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{translationcategorymod.FN.TranslationCategory.Id: id}
+ updateDoc := bson.M{"$set": param}
+ serr := translationcategorymod.UpdateOneTranslationCategory(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
diff --git a/web/service/srv_user/define.go b/web/service/srv_user/define.go
new file mode 100644
index 0000000..177fde6
--- /dev/null
+++ b/web/service/srv_user/define.go
@@ -0,0 +1,23 @@
+package srv_user
+
+import (
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/pubdef"
+)
+
+type UserListReq struct {
+ pubdef.Page
+ Id *int64 `json:"id"` // 用户ID
+ NickName string `json:"nickName"` // 昵称(模糊)
+ Status *pubdef.UserStatus `json:"status"` // 状态
+}
+
+type UserListResp struct {
+ Total int64 `json:"total"`
+ List usermod.UserList `json:"list"`
+}
+
+type UserUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param usermod.UserUpdate `json:"param"`
+}
diff --git a/web/service/srv_user/user.go b/web/service/srv_user/user.go
new file mode 100644
index 0000000..94819e6
--- /dev/null
+++ b/web/service/srv_user/user.go
@@ -0,0 +1,56 @@
+package srv_user
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/usermod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetUsers 分页查询用户列表
+func GetUsers(ctx context.Context, req *UserListReq) (*UserListResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[usermod.FN.User.Id] = *req.Id
+ }
+ if req.NickName != "" {
+ filter[usermod.FN.User.NickName] = bson.M{"$regex": req.NickName}
+ }
+ if req.Status != nil {
+ filter[usermod.FN.User.Status] = *req.Status
+ }
+
+ opts := options.Find().
+ SetSort(bson.M{usermod.FN.User.Id: -1}).
+ SetSkip((req.PageNum - 1) * req.PageSize).
+ SetLimit(req.PageSize)
+
+ resp := &UserListResp{}
+ total, serr := usermod.CountUsers(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ list, serr := usermod.FindManyUser(ctx, filter, opts)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = list
+ return resp, nil
+}
+
+// UpdateUser 更新指定用户信息
+func UpdateUser(ctx context.Context, id int64, param usermod.UserUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = &now
+ return usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: id},
+ bson.M{"$set": param},
+ )
+}
diff --git a/web/service/srv_videocategory/category.go b/web/service/srv_videocategory/category.go
new file mode 100644
index 0000000..ad7e2d2
--- /dev/null
+++ b/web/service/srv_videocategory/category.go
@@ -0,0 +1,172 @@
+package srv_videocategory
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/mongos"
+ "videoweb/web/service/srv_contenttag"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetVideoCategorys 按照参数搜索视频分类
+func GetVideoCategorys(ctx context.Context, param *VideoCategoryReq) (*VideoCategoryResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if param.Id != nil && *param.Id > 0 {
+ filter[videocategorymod.FN.VideoCategory.Id] = *param.Id
+ }
+ if param.Name != nil && *param.Name != "" {
+ filter[videocategorymod.FN.VideoCategory.Name] = bson.M{"$regex": *param.Name}
+ }
+ if param.DisplayPosition != nil {
+ filter[videocategorymod.FN.VideoCategory.DisplayPosition] = *param.DisplayPosition
+ }
+ if param.SortType != nil {
+ filter[videocategorymod.FN.VideoCategory.SortType] = *param.SortType
+ }
+ opt := &options.FindOptions{}
+ resp := &VideoCategoryResp{}
+
+ opt.SetSort(bson.M{videocategorymod.FN.VideoCategory.Rank: -1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := videocategorymod.CountVideoCategorys(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := videocategorymod.FindManyVideoCategorys(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddVideoCategory 添加一个视频分类
+func AddVideoCategory(ctx context.Context,
+ ac *videocategorymod.VideoCategory) *srvdef.SrvErr {
+ //if ac.Name != "" {
+ // count, serr := videocategorymod.CountVideoCategorys(ctx, bson.M{videocategorymod.FN.VideoCategory.Name: ac.Name})
+ // if serr != nil {
+ // return serr
+ // }
+ // if count > 0 {
+ // return srvdef.New(srvdef.ErrDbInputExist, "视频分类已存在")
+ // }
+ //}
+
+ ac.Id = mongos.Lid.VideoCategory.Get()
+ if ac.Tags == nil {
+ ac.Tags = make([]int64, 0)
+ }
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(ac.Tags)
+ if err != nil {
+ return err
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
+ return err
+ }
+ ac.Tags = tagIds
+ exists := func(s string) bool {
+ doc, _ := videocategorymod.FindOneVideoCategory(ctx, slugx.Filter(
+ videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.Id, s, 0))
+ return doc != nil
+ }
+ if serr := slugx.Ensure(&ac.Slug, utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name), exists); serr != nil {
+ return serr
+ }
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ serr := videocategorymod.InsertOneVideoCategory(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
+
+// DelVideoCategory 删除指定视频分类
+func DelVideoCategory(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{videocategorymod.FN.VideoCategory.Id: id}
+ //删除
+ count, serr := videocategorymod.DelOneVideoCategory(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
+
+// UpdateVideoCategory 更新指定视频分类
+func UpdateVideoCategory(ctx context.Context, id int64, param VideoCategoryUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{videocategorymod.FN.VideoCategory.Id: id}
+ current, serr := videocategorymod.FindOneVideoCategory(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ name := current.Name
+ if param.Name != nil {
+ name = *param.Name
+ }
+ keywords := current.SeoInfo.Keyworld
+ if param.SeoInfo != nil {
+ keywords = param.SeoInfo.Keyworld
+ }
+ exists := func(s string) bool {
+ doc, _ := videocategorymod.FindOneVideoCategory(ctx, slugx.Filter(
+ videocategorymod.FN.VideoCategory.Slug, videocategorymod.FN.VideoCategory.Id, s, id))
+ return doc != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, param.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ param.Slug = &slug
+ } else {
+ param.Slug = nil
+ }
+ if param.Tags != nil {
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(*param.Tags)
+ if err != nil {
+ return err
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
+ return err
+ }
+ param.Tags = &tagIds
+ }
+ param.IndexNow = nil
+ if param.IsReIndexNow != nil && *param.IsReIndexNow {
+ indexNow := int64(0)
+ param.IndexNow = &indexNow
+ }
+ updateDoc := bson.M{"$set": param}
+ serr = videocategorymod.UpdateOneVideoCategory(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
diff --git a/web/service/srv_videocategory/define.go b/web/service/srv_videocategory/define.go
new file mode 100644
index 0000000..14401f4
--- /dev/null
+++ b/web/service/srv_videocategory/define.go
@@ -0,0 +1,44 @@
+package srv_videocategory
+
+import (
+ "time"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoCategoryReq struct {
+ pubdef.Page
+ Id *int64 `json:"id"` // 分类 ID
+ Name *string `json:"name"` // 分类名称(模糊匹配)
+ DisplayPosition *pubdef.CategoryDisplayPosition `json:"displayPosition" bson:"displayPosition"` // 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ SortType *pubdef.CategorySortType `json:"sortType" bson:"sortType"` // 排序方式
+}
+type VideoCategoryResp struct {
+ Total int64 `json:"total"`
+ List videocategorymod.VideoCategoryList `json:"list"`
+}
+type VideoCategoryUpdate struct {
+ Name *string `json:"name" bson:"name,omitempty"` // 名称
+ Slug *string `json:"slug" bson:"slug,omitempty"` // slug
+ Title *string `json:"title" bson:"title,omitempty"` // APP显示
+ Desc *string `json:"desc" bson:"desc,omitempty"` // 描述
+ DisplayPosition *pubdef.CategoryDisplayPosition `json:"displayPosition" bson:"displayPosition,omitempty"` // 显示位置 0=仅TAB 1=仅首页 2=两者都显示
+ SortType *pubdef.CategorySortType `json:"sortType" bson:"sortType,omitempty"` // 排序方式
+ Tags *[]int64 `json:"tags" bson:"tags,omitempty"` // 关联视频标签
+ Rank *int64 `json:"rank" bson:"rank,omitempty"` //
+ TranslationCategoryId *int64 `json:"translationCategoryId" bson:"translationCategoryId,omitempty"` // 导航分类
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
+ Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
+ DisplayMode *pubdef.DisplayMode `json:"displayMode" bson:"displayMode,omitempty"` // 展示方式
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow,omitempty"` // 是否重新提交索引
+ IndexNow *int64 `bson:"indexNow,omitempty"`
+}
+type VideoCategoryUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param VideoCategoryUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type VideoCategoryDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_videocollection/define.go b/web/service/srv_videocollection/define.go
new file mode 100644
index 0000000..4a18036
--- /dev/null
+++ b/web/service/srv_videocollection/define.go
@@ -0,0 +1,42 @@
+package srv_videocollection
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoCollectionOptionMediaReq struct {
+ CollectionID int64 `json:"collectionID" bson:"collectionID"` // 合集ID
+ IsAdd bool `json:"isAdd" bson:"isAdd"` // 增加还是移除
+ VideoIds []int64 `json:"videoIds" bson:"videoIds"` // 视频ID
+}
+
+type UpdateCollectionMediaRankReq struct {
+ CollectionID int64 `json:"collectionID" bson:"collectionID"` //合集ID
+ VideoId int64 `json:"videoId" bson:"videoId"` // 视频ID
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+}
+
+type GetCollectionMediaListReq struct {
+ CollectionID int64 `json:"collectionID" bson:"collectionID"`
+ pubdef.Page
+}
+type GetCollectionMediaListResp struct {
+ Total int64 `json:"total" bson:"total"`
+ List []CollectionMediaInfo `json:"list" bson:"list"`
+}
+
+type CollectionMediaInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Nodes []videoinformationmod.Node `json:"nodes" bson:"nodes"` // 节点
+ Status pubdef.InformationStatus `json:"status" bson:"status"` // 帖子上架状态
+ Video videoinformationmod.VideoInfo `json:"video" bson:"video"` // 视频
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
diff --git a/web/service/srv_videocollection/videocollection.go b/web/service/srv_videocollection/videocollection.go
new file mode 100644
index 0000000..68f044e
--- /dev/null
+++ b/web/service/srv_videocollection/videocollection.go
@@ -0,0 +1,166 @@
+package srv_videocollection
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videocollectionmediamod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func VideoCollectionOptionMedia(ctx context.Context, req *VideoCollectionOptionMediaReq) *srvdef.SrvErr {
+ _, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.CollectionID,
+ videoinformationmod.FN.VideoInformation.MediaType: videoinformationmod.MediaType_Collection,
+ })
+ if err != nil {
+ return err
+ }
+ if req.IsAdd {
+ // 追加视频
+ videos, err := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
+ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: bson.M{"$in": req.VideoIds},
+ })
+ if err != nil {
+ return err
+ }
+ existVideoIds := make(map[int64]struct{}, len(videos))
+ for _, video := range videos {
+ existVideoIds[video.VideoId] = struct{}{}
+ }
+ addIds := make([]int64, 0, len(req.VideoIds))
+ for _, id := range req.VideoIds {
+ if _, ok := existVideoIds[id]; !ok {
+ addIds = append(addIds, id)
+ existVideoIds[id] = struct{}{}
+ }
+ }
+ if len(addIds) > 0 {
+ now := time.Now()
+ addInfos := make(videocollectionmediamod.VideoCollectionMediaModelList, 0, len(addIds))
+ for _, id := range addIds {
+ addInfos = append(addInfos, &videocollectionmediamod.VideoCollectionMediaModel{
+ ID: primitive.NewObjectID(),
+ CollectionID: req.CollectionID,
+ VideoId: id,
+ Status: pubdef.InformationStatusVerifyOK,
+ Rank: 0,
+ CreatedAt: now,
+ UpdatedAt: now,
+ })
+ }
+ err = videocollectionmediamod.InsertManyVideoCollectionMedia(ctx, addInfos)
+ if err != nil {
+ return err
+ }
+ }
+ } else {
+ // 移除视频
+ _, err = videocollectionmediamod.DelManyVideoCollectionMediaModel(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
+ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: bson.M{"$in": req.VideoIds},
+ })
+ if err != nil {
+ return err
+ }
+ }
+ count, err := videocollectionmediamod.CountVideoCollectionMediaModels(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
+ })
+ if err == nil {
+ _ = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.CollectionID,
+ }, bson.M{"$set": bson.M{
+ videoinformationmod.FN.VideoInformation.VideoCount: count,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
+ }})
+ }
+ return nil
+}
+
+func UpdateCollectionMediaRank(ctx context.Context, req *UpdateCollectionMediaRankReq) *srvdef.SrvErr {
+ err := videocollectionmediamod.UpdateOneVideoCollectionMediaModel(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
+ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: req.VideoId,
+ }, bson.M{"$set": bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.Rank: req.Rank,
+ }})
+ return err
+}
+
+func GetCollectionMediaList(ctx context.Context, req *GetCollectionMediaListReq) (*GetCollectionMediaListResp, *srvdef.SrvErr) {
+ _, err := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.CollectionID,
+ videoinformationmod.FN.VideoInformation.MediaType: videoinformationmod.MediaType_Collection,
+ })
+ if err != nil {
+ return nil, err
+ }
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.M{videocollectionmediamod.FN.VideoCollectionMediaModel.Rank: 1})
+ opts.SetLimit(req.PageSize)
+ opts.SetSkip((req.PageNum - 1) * req.PageSize)
+ total, err := videocollectionmediamod.CountVideoCollectionMediaModels(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
+ })
+ if err != nil {
+ return nil, err
+ }
+ if total == 0 {
+ return &GetCollectionMediaListResp{
+ Total: 0,
+ List: make([]CollectionMediaInfo, 0),
+ }, nil
+ }
+ videos, err := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.CollectionID: req.CollectionID,
+ }, opts)
+ if err != nil {
+ return nil, err
+ }
+ videoIds := make([]int64, 0, len(videos))
+ for _, video := range videos {
+ videoIds = append(videoIds, video.VideoId)
+ }
+ videoInfos, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
+ })
+ if err != nil {
+ return nil, err
+ }
+ videoInfoMap := make(map[int64]*videoinformationmod.VideoInformation, len(videoInfos))
+ for _, info := range videoInfos {
+ videoInfoMap[info.Id] = info
+ }
+ infos := make([]CollectionMediaInfo, 0, len(videos))
+ for _, video := range videos {
+ info, ok := videoInfoMap[video.VideoId]
+ if !ok {
+ continue
+ }
+ infos = append(infos, CollectionMediaInfo{
+ Id: video.VideoId,
+ Title: info.Title,
+ CoverImg: info.CoverImg,
+ Desc: info.Desc,
+ Remark: info.Remark,
+ Nodes: info.Nodes,
+ Status: info.Status,
+ Video: info.Video,
+ Rank: video.Rank,
+ UpdatedAt: video.UpdatedAt,
+ CreatedAt: video.CreatedAt,
+ })
+ }
+ resp := &GetCollectionMediaListResp{
+ Total: total,
+ List: infos,
+ }
+ return resp, nil
+}
diff --git a/web/service/srv_videocreater/creater.go b/web/service/srv_videocreater/creater.go
new file mode 100644
index 0000000..a6c16c8
--- /dev/null
+++ b/web/service/srv_videocreater/creater.go
@@ -0,0 +1,126 @@
+package srv_videocreater
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetVideoCreaters 按照参数搜索视频标签
+func GetVideoCreaters(ctx context.Context, param *VideoCreaterReq) (*VideoCreaterResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+ resp := &VideoCreaterResp{}
+
+ opt.SetSort(bson.M{videocreatermod.FN.VideoCreaterInfo.Id: 1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := videocreatermod.CountVideoCreaterInfos(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := videocreatermod.FindManyVideoCreaterInfos(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddVideoCreater 添加一视频标签
+func AddVideoCreater(ctx context.Context,
+ ac *videocreatermod.VideoCreaterInfo) *srvdef.SrvErr {
+
+ ac.Id = mongos.Lid.VideoCreator.Get()
+ exists := func(s string) bool {
+ doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
+ videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, 0))
+ return doc != nil
+ }
+ if serr := slugx.Ensure(&ac.Slug, utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name), exists); serr != nil {
+ return serr
+ }
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ log.Info("InsertOneVideoCreaterInfo", log.Any("", *ac))
+ serr := videocreatermod.InsertOneVideoCreaterInfo(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+// DelVideoCreater 删除指定视频标签
+func DelVideoCreater(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{videocreatermod.FN.VideoCreaterInfo.Id: id}
+ //删除
+ count, serr := videocreatermod.DelOneVideoCreaterInfo(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateVideoCreater 更新指定视频标签
+func UpdateVideoCreater(ctx context.Context, id int64, param VideoCreaterUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{videocreatermod.FN.VideoCreaterInfo.Id: id}
+ current, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ name := current.Name
+ if param.Name != nil {
+ name = *param.Name
+ }
+ keywords := current.SeoInfo.Keyworld
+ if param.SeoInfo != nil {
+ keywords = param.SeoInfo.Keyworld
+ }
+ exists := func(s string) bool {
+ doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
+ videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, id))
+ return doc != nil
+ }
+ slug, changed, serr := slugx.PrepareOnUpdate(current.Slug, param.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keywords, name), exists)
+ if serr != nil {
+ return serr
+ }
+ if changed {
+ param.Slug = &slug
+ } else {
+ param.Slug = nil
+ }
+ param.IndexNow = nil
+ if param.IsReIndexNow != nil && *param.IsReIndexNow {
+ indexNow := int64(0)
+ param.IndexNow = &indexNow
+ }
+ updateDoc := bson.M{"$set": param}
+ serr = videocreatermod.UpdateOneVideoCreaterInfo(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
diff --git a/web/service/srv_videocreater/define.go b/web/service/srv_videocreater/define.go
new file mode 100644
index 0000000..0d50ae6
--- /dev/null
+++ b/web/service/srv_videocreater/define.go
@@ -0,0 +1,40 @@
+package srv_videocreater
+
+import (
+ "time"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoCreaterReq struct {
+ pubdef.Page
+}
+type VideoCreaterResp struct {
+ Total int64 `json:"total"`
+ List videocreatermod.VideoCreaterInfoList `json:"list"`
+}
+type VideoCreaterUpdate struct {
+ AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
+ Name *string `json:"name" bson:"name,omitempty"` // 名称
+ Slug *string `json:"slug" bson:"slug,omitempty"` // slug
+ Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
+ Sort *int64 `json:"sort" bson:"sort,omitempty"` // 排序
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
+ MediaTypes *[]int64 `json:"mediaTypes" bson:"mediaTypes"` // 作者关联的资源类型 1视频
+ IndexNow *int64 `bson:"indexNow,omitempty" json:"-"`
+ Works *int64 `json:"works" bson:"works,omitempty"` // 作品数量
+ Image *string `json:"image" bson:"image,omitempty"` // 头像
+ Background *string `json:"background" bson:"background,omitempty"` // 背景
+ CreateType *int64 `json:"createType" bson:"createType,omitempty"` // 创作者类型 0=普通 1=官方
+ Desc *string `json:"desc" bson:"desc,omitempty"` // 简介
+}
+type VideoCreaterUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param VideoCreaterUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type VideoCreaterDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_videoinfo/define.go b/web/service/srv_videoinfo/define.go
new file mode 100644
index 0000000..f77099a
--- /dev/null
+++ b/web/service/srv_videoinfo/define.go
@@ -0,0 +1,166 @@
+package srv_videoinfo
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoInfoReq struct {
+ Id *int64 `json:"id"` //
+ ActressIds *[]int64 `json:"actressIds" bson:"actressIds"` // 女优ID
+ Title *string `json:"title"` // 标题
+ Bango *string `json:"bango"` // 番号,
+ CreatorId *int64 `json:"creatorId"` // 作者Id
+ Status *pubdef.InformationStatus `json:"status"` // 帖子上架状态
+ Tag *int64 `json:"tag" ` // 标签
+ IsHot *bool `json:"isHot"` // 是否热门
+ IsRecommend *bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
+ MediaType *videoinformationmod.MediaType `json:"mediaType"` // 资源类型
+ CreatorName *string `json:"creatorName"` // 作者名称
+ Keyworld *string `json:"keyworld"` // 关键字
+ CategoryId *int64 `json:"categoryId"` // 分类ID
+ VideoTaskId *string `json:"videoTaskId"` // 视频任务id
+ IsTop *bool `json:"isTop" bson:"isTop"` // 是否置顶
+ VideoFileId *string `json:"videoFileId"` // 切片id(切片列表的ID)
+ Sort int64 `json:"sort"` // 排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数。按分类查询且排序方式为排序值时,使用该分类的 categoryRanks 排序
+ pubdef.Page
+ StartTime *string `json:"startTime"` // 创建时间-开始
+ EndTime *string `json:"endTime"` // 创建时间-截止
+ OptionAccount *string `json:"optionAccount"` // 最后操作人
+}
+
+type VideoInfoResp struct {
+ Total int64 `json:"total"`
+ List videoinformationmod.VideoInformationList `json:"list"`
+}
+
+type AddVideoReq struct {
+ FileIds []string `json:"fileId"` // 资源id
+}
+
+type SyncMediaFileReq struct {
+ FileIds []string `json:"fileId"` // 媒资库影片 ID
+ SourceVersion string `json:"sourceVersion,omitempty"` // v1 / v2
+ FirstTagMap map[string]string `json:"firstTagMap,omitempty"` // 覆盖一级标签
+ MinVideoSeconds int32 `json:"minVideoSeconds,omitempty"` // 最短时长,0 表示不限制
+ VideoList []SyncMediaFileItem `json:"videoList,omitempty"` // 媒资库搜索结果,优先使用
+}
+
+type SyncMediaFileItem struct {
+ Id string `json:"id"`
+ Title string `json:"title"`
+ FieldNameFs string `json:"fieldNameFs"`
+ FirstTag string `json:"firstTag"`
+ SecondTags []string `json:"secondTags"`
+ VideoTime int32 `json:"videoTime"`
+ CoverImg string `json:"coverImg"`
+ Publish string `json:"publish"`
+ Bango string `json:"bango"`
+ TaskID string `json:"taskId"`
+ Keyworld []string `json:"keyworld"`
+ Width int64 `json:"width"`
+ Height int64 `json:"height"`
+}
+
+type SyncMediaFileResp struct {
+ Synced int `json:"synced"`
+ Failed int `json:"failed"`
+}
+type VideoInfoUpdate struct {
+ Title *string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
+ Slug *string `json:"slug,omitempty" bson:"slug,omitempty"` // slug
+ ActressIds *[]int64 `json:"actressIds" bson:"actressIds,omitempty"` // 女优ID
+ AddedTime *time.Time `json:"addedTime,omitempty" bson:"addedTime,omitempty"`
+ CreatorId *int64 `json:"creatorId,omitempty" bson:"creatorId,omitempty"` // 作者Id
+ Keyworld *[]string `bson:"keyworld,omitempty" json:"keyworld,omitempty"` //
+ MediaType *videoinformationmod.MediaType `json:"mediaType,omitempty" bson:"mediaType,omitempty"` // 资源类型
+ FirstTag *string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
+ SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
+ CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
+ UnencryptedImage *string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
+ Remark *string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
+ Nodes *[]videoinformationmod.Node `json:"nodes,omitempty" bson:"nodes,omitempty"` // 节点
+ Status *pubdef.InformationStatus `json:"status,omitempty" bson:"status,omitempty"` // 帖子上架状态
+ Sort *int64 `json:"sort,omitempty" bson:"sort,omitempty"` // 排序
+ CategoryId *int64 `json:"categoryId" bson:"categoryId,omitempty"` // 分类ID
+ CategoryRanks *[]videoinformationmod.VideoCategoryRank `json:"categoryRanks" bson:"categoryRanks,omitempty"` // 各分类下的排序,传空数组表示清空
+
+ Cover *string `json:"cover,omitempty" bson:"cover,omitempty"` // 视频封面
+ Height *int64 `json:"height,omitempty" bson:"height,omitempty"` // 分辨率高(视频才会存在)
+ Width *int64 `json:"width,omitempty" bson:"width,omitempty"` // 分辨率宽(视频才会存在)
+ VideoTime *int32 `bson:"videoTime,omitempty" json:"videoTime,omitempty"` // 影片长度
+
+ Tags *[]int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
+ IsHot *bool `json:"isHot,omitempty" bson:"isHot,omitempty"` // 是否热门
+ IsRecommend *bool `json:"isRecommend,omitempty" bson:"isRecommend,omitempty"` // 是否推荐 true-推荐 false-不推荐
+ UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` // 更新时间
+
+ CreatorName *string `bson:"creatorName,omitempty"` // 作者名
+ IsTop *bool `json:"isTop,omitempty" bson:"isTop,omitempty"` // 是否置顶
+ Video *videoinformationmod.VideoInfo `bson:"video,omitempty"` // 视频
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
+ IndexNow *int64 `bson:"indexNow,omitempty"`
+ IssuedAt *time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
+ IssuedBrand *string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
+ Bango *string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+ SecondUpdatedAt *time.Time `json:"secondUpdatedAt,omitempty" bson:"secondUpdatedAt,omitempty"` // 二次修改时间
+}
+type VideoInfoUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param VideoInfoUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type VideoInfoDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+type AddVideoCollectionReq struct {
+ Title string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld,omitempty"` // 关键字
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
+ Desc string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
+ Tags []int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
+}
+
+type VideoAddTagsReq struct {
+ VideoIds []int64 `json:"videoIds" binding:"required"` // 视频ID集合
+ Tags []int64 `json:"tags" binding:"required"` // 统一标签ID集合
+}
+
+type VideoSetTagsReq struct {
+ Ids []int64 `json:"ids" binding:"required"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
+ TagIds *[]int64 `json:"tagIds"` // 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
+}
+
+type SetFakeLikesReq struct {
+ Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
+ LikesMin int64 `json:"likesMin"` // 假点赞数随机最小值,包含该值
+ LikesMax int64 `json:"likesMax"` // 假点赞数随机最大值,包含该值
+}
+
+type SetFakeWatchTimesReq struct {
+ Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
+ WatchTimesMin int64 `json:"watchTimesMin"` // 假观看次数随机最小值,包含该值
+ WatchTimesMax int64 `json:"watchTimesMax"` // 假观看次数随机最大值,包含该值
+}
+
+// SetVideoHeatReq 单/批量设置视频热度
+type SetVideoHeatReq struct {
+ Ids []int64 `json:"ids"` // 视频ID集合
+ HeatMin int64 `json:"heatMin"` // 热度最小值
+ HeatMax int64 `json:"heatMax"` // 热度最大值
+}
+
+type SetRecommendReq struct {
+ Ids []int64 `json:"ids"` // 视频ID集合,传1个表示单条设置,传多个表示批量设置
+ IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
+}
+
+type SetFakeCountResp struct {
+ MatchedCount int64 `json:"matchedCount"` // 匹配到的视频数量
+ ModifiedCount int64 `json:"modifiedCount"` // 实际修改的视频数量
+}
diff --git a/web/service/srv_videoinfo/externallist.go b/web/service/srv_videoinfo/externallist.go
new file mode 100644
index 0000000..4539f5d
--- /dev/null
+++ b/web/service/srv_videoinfo/externallist.go
@@ -0,0 +1,152 @@
+package srv_videoinfo
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type ExternalVideoListReq struct {
+ pubdef.Page
+ Date string `form:"date"` // 日期筛选,格式:2006-01-02
+ Publisher string `form:"publisher"` // 发布人
+}
+
+type ExternalVideoListItem struct {
+ Date string `json:"date"`
+ Publisher string `json:"publisher"`
+ SiteName string `json:"siteName"`
+ Title string `json:"title"`
+ Keyworld []string `json:"keyworld"`
+ Tags []string `json:"tags"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"updatedAt"`
+ Url string `json:"url"`
+ Content []string `json:"content"`
+ UploadUserName string `json:"uploadUserName"`
+}
+
+type ExternalVideoListResp struct {
+ Total int64 `json:"total"`
+ List []ExternalVideoListItem `json:"list"`
+}
+
+// SearchExternalVideoList 按日期查询已上架视频,供外部数据接口使用。
+func SearchExternalVideoList(ctx context.Context, req *ExternalVideoListReq) (*ExternalVideoListResp, *srvdef.SrvErr) {
+ cst := time.FixedZone("CST", 8*3600)
+
+ var dayStart, dayEnd time.Time
+ if req.Date != "" {
+ t, err := time.ParseInLocation("2006-01-02", req.Date, cst)
+ if err == nil {
+ dayStart = t
+ } else {
+ dayStart = time.Now().In(cst).Truncate(24 * time.Hour).Add(-24 * time.Hour)
+ }
+ } else {
+ now := time.Now().In(cst)
+ today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, cst)
+ dayStart = today.Add(-24 * time.Hour)
+ }
+ dayEnd = dayStart.Add(24 * time.Hour)
+
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ "$or": bson.A{
+ bson.M{
+ videoinformationmod.FN.VideoInformation.AddedTime: bson.M{
+ "$gte": dayStart.UTC(),
+ "$lt": dayEnd.UTC(),
+ },
+ },
+ bson.M{
+ videoinformationmod.FN.VideoInformation.SecondUpdatedAt: bson.M{
+ "$gte": dayStart.UTC(),
+ "$lt": dayEnd.UTC(),
+ },
+ },
+ },
+ }
+ if req.Publisher != "" {
+ filter[videoinformationmod.FN.VideoInformation.UploadUserName] = req.Publisher
+ }
+
+ total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
+ if serr != nil {
+ log.Warn("SearchExternalVideoList CountVideoInformations failed", log.E(serr))
+ return nil, serr
+ }
+
+ pageSize := req.Page.PageSize
+ if pageSize <= 0 {
+ pageSize = 1000
+ }
+ pageNum := req.Page.PageNum
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ opts := &options.FindOptions{}
+ opts.SetLimit(pageSize)
+ opts.SetSkip((pageNum - 1) * pageSize)
+ opts.SetSort(bson.D{{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1}})
+ list, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if serr != nil {
+ log.Warn("SearchExternalVideoList FindManyVideoInformations failed", log.E(serr))
+ return nil, serr
+ }
+
+ tagIdSet := make(map[int64]struct{})
+ for _, v := range list {
+ for _, tid := range v.Tags {
+ tagIdSet[tid] = struct{}{}
+ }
+ }
+ tagTitleMap := make(map[int64]string)
+ if len(tagIdSet) > 0 {
+ tagIds := make([]int64, 0, len(tagIdSet))
+ for tid := range tagIdSet {
+ tagIds = append(tagIds, tid)
+ }
+ tags, _ := contenttagmod.GetMapTags(ctx, tagIds)
+ for _, t := range tags {
+ tagTitleMap[t.Id] = t.Name
+ }
+ }
+
+ items := make([]ExternalVideoListItem, 0, len(list))
+ for _, v := range list {
+ tagTitles := make([]string, 0, len(v.Tags))
+ for _, tid := range v.Tags {
+ if title, ok := tagTitleMap[tid]; ok {
+ tagTitles = append(tagTitles, title)
+ }
+ }
+ var content []string
+ for _, node := range v.Nodes {
+ content = append(content, node.Text)
+ }
+ items = append(items, ExternalVideoListItem{
+ Date: utils.GetDateString(v.AddedTime),
+ CreatedAt: utils.GetTimeString(v.CreatedAt),
+ UpdatedAt: utils.GetTimeString(v.SecondUpdatedAt),
+ Publisher: v.OptionAccount,
+ SiteName: pubdef.AppName,
+ Title: v.Title,
+ Keyworld: v.Keyworld,
+ Tags: tagTitles,
+ Url: fmt.Sprintf("/video/detail?id=%d", v.Id),
+ Content: content,
+ UploadUserName: v.UploadUserName,
+ })
+ }
+ return &ExternalVideoListResp{Total: total, List: items}, nil
+}
diff --git a/web/service/srv_videoinfo/hevc_transcode.go b/web/service/srv_videoinfo/hevc_transcode.go
new file mode 100644
index 0000000..32a71eb
--- /dev/null
+++ b/web/service/srv_videoinfo/hevc_transcode.go
@@ -0,0 +1,160 @@
+package srv_videoinfo
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "videoweb/pub/common/laosiji"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/web/webg"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// TranscodeVideo 手动提交主视频 H265 转码。
+func TranscodeVideo(ctx context.Context, videoId int64) *srvdef.SrvErr {
+ info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: videoId,
+ })
+ if serr != nil {
+ return serr
+ }
+ src := strings.TrimSpace(info.Video.Url)
+ if src == "" {
+ return srvdef.New(srvdef.Err, "", "该视频没有可转码的源地址")
+ }
+ if laosiji.LaosijiClient == nil || laosiji.LaosijiDomains == nil {
+ return srvdef.New(srvdef.Err, "", "转码服务未就绪")
+ }
+ if info.HevcStatus == videoinformationmod.HevcStatusPending {
+ return srvdef.New(srvdef.Err, "", "转码请求已提交,正在转码中,请勿重复提交")
+ }
+ if err := SubmitTranscodeVideo(ctx, videoId, src, info.Video.FileId); err != nil {
+ return srvdef.New(srvdef.Err, "", "提交转码失败:"+err.Error())
+ }
+ return nil
+}
+
+func buildTranscodeSourceURL(videoUrl string) string {
+ name := strings.TrimSpace(videoUrl)
+ if name == "" {
+ return ""
+ }
+ if !strings.Contains(name, ".m3u8") {
+ name += ".m3u8"
+ }
+ host := strings.TrimSpace(webg.Conf.WebM3u8Host)
+ if host == "" {
+ return ""
+ }
+ return utils.JoinUrlPath(host, "/api/web/m3u8/"+name)
+}
+
+// SubmitTranscodeVideo 提交主视频转码任务。
+func SubmitTranscodeVideo(ctx context.Context, videoId int64, src, fileId string) error {
+ fileURL := buildTranscodeSourceURL(src)
+ if fileURL == "" {
+ return fmt.Errorf("empty transcode source url, videoUrl:%s host:%s", src, webg.Conf.WebM3u8Host)
+ }
+ sd, err := laosiji.LaosijiDomains.Get(ctx)
+ if err != nil {
+ return err
+ }
+ result, err := laosiji.LaosijiClient.SubmitTranscoding(ctx, fileURL, fileId, *sd, videoId)
+ if err != nil {
+ return err
+ }
+ return applyTranscodeResult(ctx, videoId, result)
+}
+
+// RefreshTranscodeVideo 查询转码任务状态并回写。
+func RefreshTranscodeVideo(ctx context.Context, videoId int64, src, fileId string) error {
+ fileURL := buildTranscodeSourceURL(src)
+ if fileURL == "" {
+ return fmt.Errorf("empty transcode source url, videoUrl:%s host:%s", src, webg.Conf.WebM3u8Host)
+ }
+ sd, err := laosiji.LaosijiDomains.Get(ctx)
+ if err != nil {
+ return err
+ }
+ result, found, err := laosiji.LaosijiClient.QueryTranscoding(ctx, fileURL, fileId, *sd)
+ if err != nil {
+ return err
+ }
+ if !found {
+ return nil
+ }
+ return applyTranscodeResult(ctx, videoId, result)
+}
+
+func applyTranscodeResult(ctx context.Context, videoId int64, result laosiji.TranscodeResult) error {
+ log.Infof("==applyTranscodeResult===> result", log.Any("videoId", videoId), log.Any("result", result))
+ var update bson.M
+ switch {
+ case result.Done:
+ meta := toVideoMetaInfo(result.HevcMeta)
+ if meta == nil {
+ meta = &videoinformationmod.VideoMetaInfo{}
+ }
+ h265Url := normalizeH265UrlForSave(result.HevcURL)
+ meta.M3u8URL = h265Url
+ set := bson.M{
+ videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusSuccess,
+ videoinformationmod.FN.VideoInformation.HevcMetaInfo: meta,
+ videoinformationmod.FN.VideoInformation.Video.H265Url: h265Url,
+ }
+ if result.PreviewVtt != "" {
+ set[videoinformationmod.FN.VideoInformation.PreviewVtt] = result.PreviewVtt
+ }
+ if result.AudioFile != "" {
+ set[videoinformationmod.FN.VideoInformation.AudioFile] = result.AudioFile
+ }
+ if result.SmartCover != "" {
+ set[videoinformationmod.FN.VideoInformation.SmartCover] = result.SmartCover
+ }
+ update = bson.M{"$set": set}
+ case result.Failed:
+ update = bson.M{
+ "$set": bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusFailed},
+ "$inc": bson.M{videoinformationmod.FN.VideoInformation.HevcFailCount: 1},
+ }
+ default:
+ update = bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusPending}}
+ }
+ if serr := videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: videoId,
+ }, update); serr != nil {
+ return fmt.Errorf("update video transcode status: %v", serr)
+ }
+ return nil
+}
+
+func normalizeH265UrlForSave(url string) string {
+ url = strings.TrimSpace(url)
+ return strings.TrimPrefix(url, "/")
+}
+
+func toVideoMetaInfo(m *laosiji.MetaInfo) *videoinformationmod.VideoMetaInfo {
+ if m == nil {
+ return nil
+ }
+ return &videoinformationmod.VideoMetaInfo{
+ Codec: m.Codec,
+ Profile: m.Profile,
+ Width: m.Width,
+ Height: m.Height,
+ Fps: m.Fps,
+ VideoBitrate: m.VideoBitrate,
+ PixFmt: m.PixFmt,
+ AudioCodec: m.AudioCodec,
+ AudioBitrate: m.AudioBitrate,
+ AudioSampleRate: m.AudioSampleRate,
+ AudioChannels: m.AudioChannels,
+ Bitrate: m.Bitrate,
+ Size: m.Size,
+ Duration: m.Duration,
+ }
+}
diff --git a/web/service/srv_videoinfo/sync.go b/web/service/srv_videoinfo/sync.go
new file mode 100644
index 0000000..50a3179
--- /dev/null
+++ b/web/service/srv_videoinfo/sync.go
@@ -0,0 +1,294 @@
+package srv_videoinfo
+
+import (
+ "context"
+ "math/rand"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func videoSyncKey(media proxy.MediaResp) string {
+ if !media.MediaSourceId.IsZero() {
+ return media.MediaSourceId.Hex()
+ }
+ return media.Id.Hex()
+}
+
+func buildVideoSyncFilter(media proxy.MediaResp) bson.M {
+ keys := []string{media.Id.Hex()}
+ if !media.MediaSourceId.IsZero() {
+ msid := media.MediaSourceId.Hex()
+ if msid != keys[0] {
+ keys = append(keys, msid)
+ }
+ }
+ or := make([]bson.M, 0, len(keys)*2)
+ for _, key := range keys {
+ or = append(or,
+ bson.M{videoinformationmod.FN.VideoInformation.Video.FileId: key},
+ bson.M{videoinformationmod.FN.VideoInformation.SyncId: key},
+ )
+ }
+ return bson.M{"$or": or}
+}
+
+func IsPlayletMedia(ctx context.Context, media proxy.MediaResp) bool {
+ return playletchaptermod.IsLinkedMedia(ctx, media.IsPlaylet, media.PlayletKeys()...)
+}
+
+func SyncOneVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
+ if IsPlayletMedia(ctx, media) {
+ log.Info("VideoSync skip playlet media", log.Any("mediaId", media.Id.Hex()), log.Any("mediaSourceId", media.MediaSourceId.Hex()))
+ return nil
+ }
+ // 同步时不再自动写 video_taginfo,标签由后台统一标签绑定维护
+ syncKey := videoSyncKey(media)
+ filter := buildVideoSyncFilter(media)
+ videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, filter)
+ if serr != nil {
+ // 新加入的视频
+ if serr.Code == srvdef.ErrNoData {
+ videoInfo = media.ToVideoInfo()
+ if videoInfo == nil {
+ log.Error("VideoSync FindOneVideoInformation", log.Any("MediaId", media.Id))
+ return serr
+ }
+ if len(media.Publish) > 0 {
+ creater, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Name: media.Publish,
+ })
+ if serr != nil && serr.Code != srvdef.ErrNoData {
+ log.Error("VideoSync.FindOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
+ return serr
+ }
+ if creater == nil {
+ createrId := mongos.Lid.VideoCreator.Get()
+ creater = &videocreatermod.VideoCreaterInfo{
+ Id: createrId,
+ Name: media.Publish,
+ MediaTypes: []int64{1},
+ Status: true,
+ Sort: 0,
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ }
+ createrExists := func(s string) bool {
+ doc, _ := videocreatermod.FindOneVideoCreaterInfo(ctx, slugx.Filter(
+ videocreatermod.FN.VideoCreaterInfo.Slug, videocreatermod.FN.VideoCreaterInfo.Id, s, 0))
+ return doc != nil
+ }
+ if serr = slugx.Ensure(&creater.Slug, utils.SlugSourceFromKeywordsOrTitle(nil, creater.Name), createrExists); serr != nil {
+ return serr
+ }
+ serr = videocreatermod.InsertOneVideoCreaterInfo(ctx, creater)
+ if serr != nil {
+ log.Error("VideoSync.InsertOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
+ return serr
+ }
+ } else {
+ isFind := false
+ for _, v := range creater.MediaTypes {
+ if v == 1 {
+ isFind = true
+ }
+ }
+ if !isFind {
+ mediaTypes := append(creater.MediaTypes, 1)
+ _ = videocreatermod.UpdateOneVideoCreaterInfo(ctx, bson.M{videocreatermod.FN.VideoCreaterInfo.Id: creater.Id}, bson.M{
+ "$set": bson.M{videocreatermod.FN.VideoCreaterInfo.MediaTypes: mediaTypes},
+ })
+ }
+ }
+ videoInfo.CreatorId = creater.Id
+ videoInfo.CreatorName = creater.Name
+ }
+ videoInfo.IndexNow = 0
+ applyDefaultFakeFlow(&videoInfo.Fake, rand.New(rand.NewSource(time.Now().UnixNano())))
+ exists := func(s string) bool {
+ doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
+ videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, 0))
+ return doc != nil
+ }
+ if serr = slugx.Ensure(&videoInfo.Slug, utils.SlugSourceFromKeywordsOrTitle(videoInfo.Keyworld, videoInfo.Title), exists); serr != nil {
+ return serr
+ }
+ serr = videoinformationmod.InsertOneVideoInformation(ctx, videoInfo)
+ if serr != nil {
+ log.Error("VideoSync InsertOneVideoInformation", log.Any("videoInfo", videoInfo), log.E(serr))
+ return serr
+ }
+ } else {
+ log.Error("VideoSync FindOneVideoInformation", log.Any("filter", filter), log.E(serr))
+ return serr
+ }
+ } else {
+ // 更新资源
+ nodes := make([]videoinformationmod.Node, 0)
+ if videoInfo != nil {
+ mediaNodeMap := make(map[string]videoinformationmod.Node, len(media.Nodes))
+ for _, node := range media.Nodes {
+ if node.NodeId != "" {
+ mediaNodeMap[node.NodeId] = node
+ }
+ }
+ // 更新节点里面图片水印 以本地视频列表的节点为主
+ for _, node := range videoInfo.Nodes {
+ if mediaNode, ok := mediaNodeMap[node.NodeId]; ok {
+ node.Images = mediaNode.Images
+ }
+ nodes = append(nodes, node)
+ }
+ } else {
+ nodes = media.Nodes
+ }
+ setDoc := bson.M{
+ videoinformationmod.FN.VideoInformation.Nodes: nodes,
+ videoinformationmod.FN.VideoInformation.Keyworld: media.Keyworld,
+ videoinformationmod.FN.VideoInformation.FirstTag: media.FirstTag,
+ videoinformationmod.FN.VideoInformation.SecondTags: media.SecondTags,
+ videoinformationmod.FN.VideoInformation.CoverImg: media.CoverImg,
+ videoinformationmod.FN.VideoInformation.IssuedBrand: media.IssuedBrand,
+ videoinformationmod.FN.VideoInformation.IssuedAt: media.IssuedAt,
+ }
+ for k, v := range defaultFakeFlowSetDoc(rand.New(rand.NewSource(time.Now().UnixNano()))) {
+ setDoc[k] = v
+ }
+ setDoc[videoinformationmod.FN.VideoInformation.SyncId] = syncKey
+ if len(media.FieldNameFs) > 0 {
+ video := videoinformationmod.VideoInfo{
+ TaskId: media.TaskID,
+ FileId: syncKey,
+ Url: media.FieldNameFs,
+ Cover: media.CoverImg,
+ Height: media.Height,
+ Width: media.Width,
+ VideoTime: media.VideoTime,
+ }
+ if !strings.HasSuffix(video.Url, ".m3u8") {
+ video.Url += ".m3u8"
+ }
+ setDoc[videoinformationmod.FN.VideoInformation.Video.TaskId] = video.TaskId
+ setDoc[videoinformationmod.FN.VideoInformation.Video.FileId] = video.FileId
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Url] = video.Url
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Cover] = video.Cover
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Height] = video.Height
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Width] = video.Width
+ setDoc[videoinformationmod.FN.VideoInformation.Video.VideoTime] = video.VideoTime
+ }
+ if preMp4 := strings.TrimSpace(media.PreMp4FileNameFs); preMp4 != "" {
+ setDoc[videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs] = preMp4
+ }
+ if unencryptedPreMp4 := strings.TrimSpace(media.UnencryptedPreMp4); unencryptedPreMp4 != "" {
+ setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = unencryptedPreMp4
+ }
+ if videoInfo.Slug == "" {
+ exists := func(s string) bool {
+ doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
+ videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, videoInfo.Id))
+ return doc != nil
+ }
+ slug := ""
+ if serr = slugx.Ensure(&slug, utils.SlugSourceFromKeywordsOrTitle(media.Keyworld, videoInfo.Title), exists); serr != nil {
+ return serr
+ }
+ setDoc[videoinformationmod.FN.VideoInformation.Slug] = slug
+ }
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id}, bson.M{"$set": setDoc})
+ if serr != nil {
+ return serr
+ }
+ }
+ playletcache.ClearHomeCache()
+ _ = syncNodeVideo(ctx, media)
+ return nil
+}
+
+// syncNodeVideo 同步节点里的视频
+func syncNodeVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
+ mediaID := videoSyncKey(media)
+ nodeIDs := []string{mediaID}
+ if !media.MediaSourceId.IsZero() {
+ msid := media.MediaSourceId.Hex()
+ if msid != mediaID {
+ nodeIDs = append(nodeIDs, msid)
+ }
+ }
+ if media.Id.Hex() != mediaID && media.Id.Hex() != "" {
+ nodeIDs = append(nodeIDs, media.Id.Hex())
+ }
+ filter := bson.M{"nodes.nodeId": bson.M{"$in": nodeIDs}}
+ opts := &options.FindOptions{}
+ opts.SetLimit(100)
+ videoInfo, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if serr != nil {
+ log.Warn("syncNodeVideo", log.Any("filter", filter), log.E(serr))
+ return serr
+ }
+ if !strings.HasSuffix(media.FieldNameFs, ".m3u8") {
+ media.FieldNameFs += ".m3u8"
+ }
+ if len(videoInfo) > 0 {
+ writeModels := make([]mongo.WriteModel, 0, len(videoInfo))
+ now := time.Now()
+ // 更新视频路径
+ for _, video := range videoInfo {
+ if video.Nodes != nil && len(video.Nodes) > 0 {
+ isFind := false
+ for i, node := range video.Nodes {
+ if node.Videos != nil && len(node.Videos) > 0 {
+ for j, v := range node.Videos {
+ if v.FileId == mediaID || containsString(nodeIDs, v.FileId) {
+ node.Videos[j].Url = media.FieldNameFs
+ isFind = true
+ break
+ }
+ }
+ }
+ if isFind {
+ video.Nodes[i] = node
+ break
+ }
+ }
+ if isFind {
+ writeModels = append(writeModels, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: video.Id}).
+ SetUpdate(bson.M{"$set": bson.M{
+ videoinformationmod.FN.VideoInformation.UpdatedAt: now,
+ videoinformationmod.FN.VideoInformation.Nodes: video.Nodes,
+ }}))
+ }
+ }
+ }
+ if len(writeModels) > 0 {
+ _, serr = videoinformationmod.BulkWriteVideoInformation(ctx, writeModels, options.BulkWrite().SetOrdered(false))
+ if serr != nil {
+ log.Warn("syncNodeVideo BulkWriteVideoInformation failed", log.Any("filter", filter), log.E(serr))
+ return serr
+ }
+ }
+ }
+ return nil
+}
+
+func containsString(list []string, target string) bool {
+ for _, item := range list {
+ if item == target {
+ return true
+ }
+ }
+ return false
+}
diff --git a/web/service/srv_videoinfo/update_json.go b/web/service/srv_videoinfo/update_json.go
new file mode 100644
index 0000000..ca45ba9
--- /dev/null
+++ b/web/service/srv_videoinfo/update_json.go
@@ -0,0 +1,341 @@
+package srv_videoinfo
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// UnmarshalJSON 兼容 id 使用数字字符串,以及 param 里的表单写法。
+func (r *VideoInfoUpdateReq) UnmarshalJSON(data []byte) error {
+ data = bytes.TrimSpace(data)
+ if len(data) == 0 || bytes.Equal(data, []byte("null")) {
+ return nil
+ }
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+ if value, ok := raw["id"]; ok {
+ next, keep, err := normalizeIntRaw(value)
+ if err != nil {
+ return fmt.Errorf("id %w", err)
+ }
+ if keep && next != nil && !bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
+ if err := json.Unmarshal(next, &r.Id); err != nil {
+ return err
+ }
+ }
+ }
+ if value, ok := raw["param"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
+ if err := json.Unmarshal(value, &r.Param); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// UnmarshalJSON 兼容后台表单里的空字符串、数字字符串,以及分类排序的多种写法。
+func (v *VideoInfoUpdate) UnmarshalJSON(data []byte) error {
+ data = bytes.TrimSpace(data)
+ if len(data) == 0 || bytes.Equal(data, []byte("null")) {
+ return nil
+ }
+ normalized, err := normalizeVideoUpdateJSON(data)
+ if err != nil {
+ return err
+ }
+ type alias VideoInfoUpdate
+ var out alias
+ if err := json.Unmarshal(normalized, &out); err != nil {
+ return err
+ }
+ *v = VideoInfoUpdate(out)
+ return nil
+}
+
+func normalizeVideoUpdateJSON(data []byte) ([]byte, error) {
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return nil, err
+ }
+ for _, key := range []string{"addedTime", "updatedAt", "issuedAt", "secondUpdatedAt"} {
+ value, ok := raw[key]
+ if !ok {
+ continue
+ }
+ next, err := normalizeTimeRaw(value)
+ if err != nil {
+ return nil, err
+ }
+ if next == nil {
+ delete(raw, key)
+ continue
+ }
+ raw[key] = next
+ }
+ for _, key := range []string{"categoryId", "sort", "creatorId", "height", "width", "videoTime", "indexNow", "mediaType", "status"} {
+ value, ok := raw[key]
+ if !ok {
+ continue
+ }
+ next, keep, err := normalizeIntRaw(value)
+ if err != nil {
+ return nil, fmt.Errorf("%s %w", key, err)
+ }
+ if !keep {
+ continue
+ }
+ if next == nil {
+ delete(raw, key)
+ continue
+ }
+ raw[key] = next
+ }
+ for _, key := range []string{"actressIds", "tags"} {
+ value, ok := raw[key]
+ if !ok {
+ continue
+ }
+ next, keep, err := normalizeIntSliceRaw(value)
+ if err != nil {
+ return nil, fmt.Errorf("%s %w", key, err)
+ }
+ if !keep {
+ continue
+ }
+ if next == nil {
+ delete(raw, key)
+ continue
+ }
+ raw[key] = next
+ }
+ if value, ok := raw["categoryRanks"]; ok {
+ next, keep, err := normalizeCategoryRanksRaw(value, raw["categoryId"])
+ if err != nil {
+ return nil, err
+ }
+ if !keep {
+ delete(raw, "categoryRanks")
+ } else {
+ raw["categoryRanks"] = next
+ }
+ }
+ return json.Marshal(raw)
+}
+
+func normalizeTimeRaw(raw json.RawMessage) (json.RawMessage, error) {
+ raw = bytes.TrimSpace(raw)
+ if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
+ return raw, nil
+ }
+ var text string
+ if err := json.Unmarshal(raw, &text); err != nil {
+ return nil, fmt.Errorf("时间格式错误")
+ }
+ text = strings.TrimSpace(text)
+ if text == "" {
+ return nil, nil
+ }
+ parsed, err := parseFlexTime(text)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(parsed)
+}
+
+func parseFlexTime(text string) (time.Time, error) {
+ if parsed, err := time.Parse(time.RFC3339Nano, text); err == nil {
+ return parsed, nil
+ }
+ if parsed, err := time.Parse(time.RFC3339, text); err == nil {
+ return parsed, nil
+ }
+ for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02"} {
+ if parsed, err := time.ParseInLocation(layout, text, time.Local); err == nil {
+ return parsed, nil
+ }
+ }
+ return time.Time{}, fmt.Errorf("时间格式错误: %s", text)
+}
+
+// normalizeIntRaw 把数字或数字字符串整理成 JSON 数字。空字符串表示删除该字段。
+func normalizeIntRaw(raw json.RawMessage) (json.RawMessage, bool, error) {
+ raw = bytes.TrimSpace(raw)
+ if len(raw) == 0 {
+ return nil, false, nil
+ }
+ if bytes.Equal(raw, []byte("null")) {
+ return raw, true, nil
+ }
+ if raw[0] != '"' {
+ var number json.Number
+ if err := json.Unmarshal(raw, &number); err != nil {
+ return nil, false, fmt.Errorf("数字格式错误")
+ }
+ if _, err := number.Int64(); err != nil {
+ return nil, false, fmt.Errorf("数字格式错误")
+ }
+ return raw, true, nil
+ }
+ var text string
+ if err := json.Unmarshal(raw, &text); err != nil {
+ return nil, false, fmt.Errorf("数字格式错误")
+ }
+ text = strings.TrimSpace(text)
+ if text == "" {
+ return nil, true, nil
+ }
+ value, err := strconv.ParseInt(text, 10, 64)
+ if err != nil {
+ return nil, false, fmt.Errorf("数字格式错误: %s", text)
+ }
+ encoded, err := json.Marshal(value)
+ return encoded, true, err
+}
+
+func normalizeIntSliceRaw(raw json.RawMessage) (json.RawMessage, bool, error) {
+ raw = bytes.TrimSpace(raw)
+ if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
+ return raw, true, nil
+ }
+ if bytes.Equal(raw, []byte(`""`)) {
+ return nil, true, nil
+ }
+ if raw[0] != '[' {
+ return nil, false, fmt.Errorf("数组格式错误")
+ }
+ var items []json.RawMessage
+ if err := json.Unmarshal(raw, &items); err != nil {
+ return nil, false, err
+ }
+ values := make([]int64, 0, len(items))
+ for _, item := range items {
+ next, keep, err := normalizeIntRaw(item)
+ if err != nil {
+ return nil, false, err
+ }
+ if !keep || next == nil || bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
+ return nil, false, fmt.Errorf("数字格式错误")
+ }
+ var value int64
+ if err := json.Unmarshal(next, &value); err != nil {
+ return nil, false, err
+ }
+ values = append(values, value)
+ }
+ encoded, err := json.Marshal(values)
+ return encoded, true, err
+}
+
+func normalizeCategoryRanksRaw(raw, categoryIDRaw json.RawMessage) (json.RawMessage, bool, error) {
+ raw = bytes.TrimSpace(raw)
+ if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || bytes.Equal(raw, []byte(`""`)) {
+ return nil, false, nil
+ }
+ fallbackID := int64FromRaw(categoryIDRaw)
+ if raw[0] != '[' {
+ item, ok, err := normalizeCategoryRankItem(raw, fallbackID)
+ if err != nil {
+ return nil, false, err
+ }
+ if !ok {
+ return nil, false, nil
+ }
+ encoded, err := json.Marshal([]categoryRankJSON{item})
+ return encoded, true, err
+ }
+ var items []json.RawMessage
+ if err := json.Unmarshal(raw, &items); err != nil {
+ return nil, false, fmt.Errorf("分类排序格式错误")
+ }
+ if len(items) == 0 {
+ encoded, err := json.Marshal([]categoryRankJSON{})
+ return encoded, true, err
+ }
+ cleaned := make([]categoryRankJSON, 0, len(items))
+ for _, item := range items {
+ rank, ok, err := normalizeCategoryRankItem(item, fallbackID)
+ if err != nil {
+ return nil, false, err
+ }
+ if !ok {
+ continue
+ }
+ cleaned = append(cleaned, rank)
+ }
+ if len(cleaned) == 0 {
+ return nil, false, nil
+ }
+ encoded, err := json.Marshal(cleaned)
+ return encoded, true, err
+}
+
+type categoryRankJSON struct {
+ Id int64 `json:"id"`
+ Rank int64 `json:"rank"`
+}
+
+func normalizeCategoryRankItem(raw json.RawMessage, fallbackID int64) (categoryRankJSON, bool, error) {
+ raw = bytes.TrimSpace(raw)
+ if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
+ return categoryRankJSON{}, false, nil
+ }
+ if raw[0] != '{' {
+ if bytes.Equal(raw, []byte("null")) || bytes.Equal(raw, []byte(`""`)) || rawIsZero(raw) {
+ return categoryRankJSON{}, false, nil
+ }
+ rank := int64FromRaw(raw)
+ if rank == 0 {
+ return categoryRankJSON{}, false, fmt.Errorf("分类排序格式错误")
+ }
+ if fallbackID <= 0 {
+ return categoryRankJSON{}, false, fmt.Errorf("分类排序缺少分类ID")
+ }
+ return categoryRankJSON{Id: fallbackID, Rank: rank}, true, nil
+ }
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(raw, &fields); err != nil {
+ return categoryRankJSON{}, false, fmt.Errorf("分类排序格式错误")
+ }
+ id := int64FromRaw(fields["id"])
+ if id <= 0 {
+ id = int64FromRaw(fields["categoryId"])
+ }
+ rank := int64FromRaw(fields["rank"])
+ if id <= 0 && !rawHasValue(fields["rank"]) {
+ return categoryRankJSON{}, false, nil
+ }
+ if id <= 0 {
+ id = fallbackID
+ }
+ if id <= 0 {
+ return categoryRankJSON{}, false, fmt.Errorf("分类排序缺少分类ID")
+ }
+ return categoryRankJSON{Id: id, Rank: rank}, true, nil
+}
+
+func rawHasValue(raw json.RawMessage) bool {
+ raw = bytes.TrimSpace(raw)
+ return len(raw) > 0 && !bytes.Equal(raw, []byte("null")) && !bytes.Equal(raw, []byte(`""`))
+}
+
+func int64FromRaw(raw json.RawMessage) int64 {
+ next, keep, err := normalizeIntRaw(raw)
+ if err != nil || !keep || next == nil || bytes.Equal(bytes.TrimSpace(next), []byte("null")) {
+ return 0
+ }
+ var value int64
+ if err := json.Unmarshal(next, &value); err != nil {
+ return 0
+ }
+ return value
+}
+
+func rawIsZero(raw json.RawMessage) bool {
+ next, keep, err := normalizeIntRaw(raw)
+ return err == nil && keep && bytes.Equal(bytes.TrimSpace(next), []byte("0"))
+}
diff --git a/web/service/srv_videoinfo/videoinfo.go b/web/service/srv_videoinfo/videoinfo.go
new file mode 100644
index 0000000..f9e38e8
--- /dev/null
+++ b/web/service/srv_videoinfo/videoinfo.go
@@ -0,0 +1,992 @@
+package srv_videoinfo
+
+import (
+ "context"
+ "math/rand"
+ "net/http"
+ "time"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/slugx"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/actress"
+ "videoweb/pub/models/contenttagmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videocollectionmediamod"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage"
+ "videoweb/pub/storage/proxy"
+ "videoweb/web/service/srv_contenttag"
+ "videoweb/web/service/srv_report"
+ "videoweb/web/service/srv_seopush"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetVideoInfos 按照参数搜索APP
+func GetVideoInfos(ctx context.Context, param *VideoInfoReq) (*VideoInfoResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if param.MediaType != nil {
+ filter[videoinformationmod.FN.VideoInformation.MediaType] = *param.MediaType
+ }
+ if param.Id != nil && *param.Id > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Id] = *param.Id
+ }
+ if param.Title != nil && len(*param.Title) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Title] = bson.M{"$regex": *param.Title}
+ }
+ if param.Bango != nil && *param.Bango != "" {
+ filter[videoinformationmod.FN.VideoInformation.Bango] = bson.M{"$regex": *param.Bango}
+ }
+ if param.CreatorId != nil && *param.CreatorId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CreatorId] = *param.CreatorId
+ }
+ if param.Status != nil {
+ filter[videoinformationmod.FN.VideoInformation.Status] = *param.Status
+ }
+ if param.Tag != nil && *param.Tag > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": []int64{*param.Tag}}
+ }
+ if param.IsHot != nil {
+ filter[videoinformationmod.FN.VideoInformation.IsHot] = *param.IsHot
+ }
+ if param.IsRecommend != nil {
+ filter[videoinformationmod.FN.VideoInformation.IsRecommend] = *param.IsRecommend
+ }
+ if param.CreatorName != nil && len(*param.CreatorName) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CreatorName] = bson.M{"$regex": *param.CreatorName}
+ }
+ if param.Keyworld != nil && len(*param.Keyworld) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Keyworld] = bson.M{"$in": []string{*param.Keyworld}}
+ }
+ if param.VideoTaskId != nil && len(*param.VideoTaskId) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Video.TaskId] = *param.VideoTaskId
+ }
+ if param.VideoFileId != nil && len(*param.VideoFileId) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Video.FileId] = *param.VideoFileId
+ }
+ if param.CategoryId != nil && *param.CategoryId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CategoryId] = *param.CategoryId
+ }
+ if param.IsTop != nil {
+ filter[videoinformationmod.FN.VideoInformation.IsTop] = *param.IsTop
+ }
+ if param.ActressIds != nil && len(*param.ActressIds) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.ActressIds] = bson.M{"$in": *param.ActressIds}
+ }
+ if param.StartTime != nil && param.EndTime != nil {
+ start := utils.GetDayBeginByTime(*param.StartTime)
+ end := utils.GetDayEndByTime(*param.EndTime)
+ if end.Unix() > start.Unix() {
+ filter[videoinformationmod.FN.VideoInformation.CreatedAt] = bson.M{
+ "$gte": start, "$lte": end,
+ }
+ }
+ }
+ if param.OptionAccount != nil {
+ filter[videoinformationmod.FN.VideoInformation.OptionAccount] = *param.OptionAccount
+ }
+ opt := &options.FindOptions{}
+ resp := &VideoInfoResp{
+ Total: 0,
+ List: make(videoinformationmod.VideoInformationList, 0),
+ }
+
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: -1})
+ switch param.Sort {
+ case 0:
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.CreatedAt: -1})
+ case 1:
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1})
+ case 2:
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1})
+ case 3:
+ opt.SetSort(bson.D{
+ {videoinformationmod.FN.VideoInformation.Sort, -1},
+ {videoinformationmod.FN.VideoInformation.AddedTime, -1},
+ })
+ case 4:
+ opt.SetSort(bson.D{
+ {videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1},
+ {videoinformationmod.FN.VideoInformation.AddedTime, -1},
+ })
+ }
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ var ads videoinformationmod.VideoInformationList
+ if param.Sort == 3 && param.CategoryId != nil && *param.CategoryId > 0 {
+ ads, serr = videoinformationmod.FindManyVideoByCategoryRank(ctx, filter, *param.CategoryId, (param.PageNum-1)*param.PageSize, param.PageSize, nil)
+ } else {
+ ads, serr = videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
+ }
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// SetVideoRecommend 单/批量设置视频是否推荐
+func SetVideoRecommend(ctx context.Context, req *SetRecommendReq) *srvdef.SrvErr {
+ if req == nil || len(req.Ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ ids, err := normalizeFakeFlowIds(req.Ids)
+ if err != nil {
+ return err
+ }
+ err = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
+ }, bson.M{
+ "$set": bson.M{
+ videoinformationmod.FN.VideoInformation.IsRecommend: req.IsRecommend,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
+ },
+ })
+ if err != nil {
+ return err
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
+
+// SetVideoFakeLikes 单/批量随机设置视频假点赞数
+func SetVideoFakeLikes(ctx context.Context, req *SetFakeLikesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
+ if req == nil || len(req.Ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ if req.LikesMin < 0 || req.LikesMax < req.LikesMin {
+ return nil, srvdef.New(srvdef.ErrParamError, "点赞数区间无效")
+ }
+ ids, serr := normalizeFakeFlowIds(req.Ids)
+ if serr != nil {
+ return nil, serr
+ }
+ return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.Likes, req.LikesMin, req.LikesMax)
+}
+
+// SetVideoFakeWatchTimes 单/批量随机设置视频假观看次数
+func SetVideoFakeWatchTimes(ctx context.Context, req *SetFakeWatchTimesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
+ if req == nil || len(req.Ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ if req.WatchTimesMin < 0 || req.WatchTimesMax < req.WatchTimesMin {
+ return nil, srvdef.New(srvdef.ErrParamError, "观看次数区间无效")
+ }
+ ids, err := normalizeFakeFlowIds(req.Ids)
+ if err != nil {
+ return nil, err
+ }
+ return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.WatchTimes, req.WatchTimesMin, req.WatchTimesMax)
+}
+
+// SetVideoHeat 单/批量随机设置视频热度
+func SetVideoHeat(ctx context.Context, req *SetVideoHeatReq) (*SetFakeCountResp, *srvdef.SrvErr) {
+ if req == nil || len(req.Ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ if req.HeatMin < 0 || req.HeatMax < req.HeatMin {
+ return nil, srvdef.New(srvdef.ErrParamError, "热度区间无效")
+ }
+ ids, err := normalizeFakeFlowIds(req.Ids)
+ if err != nil {
+ return nil, err
+ }
+ return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.HeatValue, req.HeatMin, req.HeatMax)
+}
+
+func setVideoFakeField(ctx context.Context, ids []int64, field string, min, max int64) (*SetFakeCountResp, *srvdef.SrvErr) {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ docs := make([]mongo.WriteModel, 0, len(ids))
+ for _, id := range ids {
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: id}).
+ SetUpdate(bson.M{"$set": bson.M{field: randomFakeFlowValue(r, min, max)}}))
+ }
+ result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
+ if err != nil {
+ return nil, err
+ }
+ if result.MatchedCount == 0 {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ playletcache.ClearHomeCache()
+ return &SetFakeCountResp{MatchedCount: result.MatchedCount, ModifiedCount: result.ModifiedCount}, nil
+}
+
+func normalizeFakeFlowIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
+ ids := make([]int64, 0, len(rawIds))
+ seen := make(map[int64]struct{}, len(rawIds))
+ for _, id := range rawIds {
+ if id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ return ids, nil
+}
+
+const (
+ defaultFakeFlowMin = int64(1000)
+ defaultFakeFlowMax = int64(10000)
+)
+
+func randomDefaultFakeFlowPair(r *rand.Rand) (watchTimes, likes int64) {
+ if r == nil {
+ r = rand.New(rand.NewSource(time.Now().UnixNano()))
+ }
+ likes = randomFakeFlowValue(r, defaultFakeFlowMin, defaultFakeFlowMax-1)
+ watchTimes = randomFakeFlowValue(r, likes+1, defaultFakeFlowMax)
+ return watchTimes, likes
+}
+
+func applyDefaultFakeFlow(fake *pubdef.FakeFlow, r *rand.Rand) {
+ if fake == nil {
+ return
+ }
+ fake.WatchTimes, fake.Likes = randomDefaultFakeFlowPair(r)
+}
+
+func defaultFakeFlowSetDoc(r *rand.Rand) bson.M {
+ watchTimes, likes := randomDefaultFakeFlowPair(r)
+ return bson.M{
+ videoinformationmod.FN.VideoInformation.Fake.WatchTimes: watchTimes,
+ videoinformationmod.FN.VideoInformation.Fake.Likes: likes,
+ }
+}
+
+const fillZeroFakeFlowBatchSize = 500
+
+func zeroFakeFlowFilter() bson.M {
+ return bson.M{
+ "$or": []bson.M{
+ {
+ "$expr": bson.M{
+ "$lt": []interface{}{
+ bson.M{"$add": []interface{}{"$real.watchTimes", "$fake.watchTimes"}},
+ defaultFakeFlowMin,
+ },
+ },
+ },
+ {
+ "$expr": bson.M{
+ "$lt": []interface{}{
+ bson.M{"$add": []interface{}{"$real.likes", "$fake.likes"}},
+ defaultFakeFlowMin,
+ },
+ },
+ },
+ },
+ }
+}
+
+// FillZeroVideoFakeFlow 为播放量或收藏总量小于 1000 的视频随机设置 fake 播放量和收藏数(1000-10000,播放量大于收藏量)。
+func FillZeroVideoFakeFlow(ctx context.Context) (*SetFakeCountResp, *srvdef.SrvErr) {
+ filter := zeroFakeFlowFilter()
+ opt := options.Find().
+ SetProjection(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}).
+ SetBatchSize(fillZeroFakeFlowBatchSize).
+ SetLimit(fillZeroFakeFlowBatchSize)
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ resp := &SetFakeCountResp{}
+ var lastID int64
+
+ for {
+ pageFilter := filter
+ if lastID > 0 {
+ pageFilter = bson.M{
+ "$and": []bson.M{
+ filter,
+ {videoinformationmod.FN.VideoInformation.Id: bson.M{"$gt": lastID}},
+ },
+ }
+ }
+ findOpt := opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1})
+ list, serr := videoinformationmod.FindManyVideoInformations(ctx, pageFilter, findOpt)
+ if serr != nil {
+ return nil, serr
+ }
+ if len(list) == 0 {
+ break
+ }
+
+ docs := make([]mongo.WriteModel, 0, len(list))
+ for _, item := range list {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ lastID = item.Id
+ resp.MatchedCount++
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
+ SetUpdate(bson.M{"$set": defaultFakeFlowSetDoc(r)}))
+ }
+ if len(docs) == 0 {
+ break
+ }
+ result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
+ if err != nil {
+ return nil, err
+ }
+ resp.ModifiedCount += result.ModifiedCount
+ if len(list) < fillZeroFakeFlowBatchSize {
+ break
+ }
+ }
+ if resp.ModifiedCount > 0 {
+ playletcache.ClearHomeCache()
+ }
+ return resp, nil
+}
+
+func randomFakeFlowValue(r *rand.Rand, min, max int64) int64 {
+ if min == max {
+ return min
+ }
+ return min + r.Int63n(max-min+1)
+}
+
+// SyncVideoInfo 同步视频
+func SyncVideoInfo(ctx context.Context,
+ req *AddVideoReq) *srvdef.SrvErr {
+
+ ids := make([]string, 0)
+ for _, id := range req.FileIds {
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ return nil
+ }
+
+ queryMediaReq := &proxy.MediaQueryesReq{
+ Ids: ids,
+ }
+ resp, err := storage.QueryMedias(ctx, queryMediaReq, "")
+ if err != nil || resp.Code != http.StatusOK {
+ if resp != nil {
+ log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err), log.Any("respCode", resp.Code))
+ } else {
+ log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err))
+ }
+ return srvdef.New(srvdef.ErrMediaNotExsit, err.Error(), err.Error())
+ }
+ if len(resp.Data) == 0 {
+ return nil
+ }
+ for _, media := range resp.Data {
+ se := SyncOneVideo(ctx, media)
+ if se != nil {
+ log.Warn("SyncVideoInfo SYncOneVideo failed", log.Any("filter", req.FileIds), log.E(se))
+ continue
+ }
+ }
+ return nil
+}
+
+func mediaFileDomain(sourceVersion string) string {
+ if sourceVersion == "v1" {
+ return proxy.BucketVideoAddressV1
+ }
+ return proxy.BucketVideoAddressV2
+}
+
+func buildMediaRespFromSyncItem(item SyncMediaFileItem) (proxy.MediaResp, error) {
+ oid, err := primitive.ObjectIDFromHex(item.Id)
+ if err != nil {
+ return proxy.MediaResp{}, err
+ }
+ return proxy.MediaResp{
+ Id: oid,
+ MediaSourceId: oid,
+ MediaType: 2,
+ Title: item.Title,
+ FieldNameFs: item.FieldNameFs,
+ FirstTag: item.FirstTag,
+ SecondTags: item.SecondTags,
+ Keyworld: item.Keyworld,
+ VideoTime: item.VideoTime,
+ CoverImg: item.CoverImg,
+ Publish: item.Publish,
+ Bango: item.Bango,
+ TaskID: item.TaskID,
+ Width: item.Width,
+ Height: item.Height,
+ }, nil
+}
+
+// SyncVideoFromMediaFile 从媒资库直同步到视频列表(不经过切片任务)。
+func SyncVideoFromMediaFile(ctx context.Context, req *SyncMediaFileReq) (*SyncMediaFileResp, *srvdef.SrvErr) {
+ result := &SyncMediaFileResp{}
+ if req == nil {
+ return result, nil
+ }
+ minVideoSeconds := req.MinVideoSeconds
+
+ syncOne := func(id string, media proxy.MediaResp) {
+ if media.Id.IsZero() {
+ result.Failed++
+ return
+ }
+ if minVideoSeconds > 0 && media.VideoTime > 0 && media.VideoTime <= minVideoSeconds {
+ log.Info("SyncVideoFromMediaFile skip short video",
+ log.Any("id", id),
+ log.Any("videoTime", media.VideoTime))
+ result.Failed++
+ return
+ }
+ if len(media.FieldNameFs) == 0 {
+ log.Warn("SyncVideoFromMediaFile empty fieldNameFs", log.Any("id", id))
+ result.Failed++
+ return
+ }
+ media.MediaSourceId = media.Id
+ media.MediaType = 2
+ if req.FirstTagMap != nil {
+ if firstTag, ok := req.FirstTagMap[id]; ok && len(firstTag) > 0 {
+ media.FirstTag = firstTag
+ }
+ }
+ if se := SyncOneVideo(ctx, media); se != nil {
+ log.Warn("SyncVideoFromMediaFile SyncOneVideo failed", log.Any("id", id), log.E(se))
+ result.Failed++
+ return
+ }
+ result.Synced++
+ }
+
+ if len(req.VideoList) > 0 {
+ for _, item := range req.VideoList {
+ media, err := buildMediaRespFromSyncItem(item)
+ if err != nil {
+ log.Warn("SyncVideoFromMediaFile invalid item", log.Any("id", item.Id), log.E(err))
+ result.Failed++
+ continue
+ }
+ syncOne(item.Id, media)
+ }
+ return result, nil
+ }
+
+ domain := mediaFileDomain(req.SourceVersion)
+ for _, id := range req.FileIds {
+ if len(id) == 0 {
+ continue
+ }
+ resp, err := storage.QueryMedia(ctx, &proxy.MediaQueryeReq{Id: id}, domain)
+ if err != nil || resp == nil || resp.Code != proxy.Success {
+ if resp != nil {
+ log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err), log.Any("respCode", resp.Code))
+ } else {
+ log.Warn("SyncVideoFromMediaFile QueryMedia failed", log.Any("id", id), log.E(err))
+ }
+ result.Failed++
+ continue
+ }
+ media := resp.Data
+ if media.Id.IsZero() {
+ oid, parseErr := primitive.ObjectIDFromHex(id)
+ if parseErr != nil {
+ log.Warn("SyncVideoFromMediaFile invalid id", log.Any("id", id), log.E(parseErr))
+ result.Failed++
+ continue
+ }
+ media.Id = oid
+ }
+ syncOne(id, media)
+ }
+ return result, nil
+}
+
+// DelVideoInfo 删除指定APP
+func DelVideoInfo(ctx context.Context, id int64) *srvdef.SrvErr {
+ info, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: id,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
+ }))
+ if err1 != nil {
+ return err1
+ }
+ adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
+ //删除
+ count, serr := videoinformationmod.DelOneVideoInformation(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ playletcache.ClearHomeCache()
+ // 删除后检查一下合集数据
+ collectionInfos, _ := videocollectionmediamod.FindManyVideoCollectionMediaModels(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id,
+ })
+ if len(collectionInfos) > 0 {
+ collectionIds := make([]int64, 0)
+ for _, collectionInfo := range collectionInfos {
+ collectionIds = append(collectionIds, collectionInfo.CollectionID)
+ }
+ if len(collectionIds) > 0 {
+ _ = videoinformationmod.UpdateManyVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": collectionInfos},
+ }, bson.M{"$inc": bson.M{
+ videoinformationmod.FN.VideoInformation.VideoCount: -1,
+ }})
+ }
+ }
+ _, _ = videocollectionmediamod.DelManyVideoCollectionMediaModel(ctx, bson.M{
+ videocollectionmediamod.FN.VideoCollectionMediaModel.VideoId: id,
+ })
+ if len(info.Tags) > 0 {
+ oldTagIds := append([]int64{}, info.Tags...)
+ godef.Go(func() {
+ _ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, nil, contenttagmod.ContentTagTypeVideo)
+ })
+ }
+ return nil
+}
+
+// UpdateVideoInfo 更新指定APP
+func UpdateVideoInfo(ctx context.Context, id, userId int64, param VideoInfoUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: id})
+ if serr != nil {
+ return serr
+ }
+ param.UpdatedAt = &now
+ param.SecondUpdatedAt = &now
+ if param.CreatorId != nil {
+ name := ""
+ if *param.CreatorId == 0 {
+ param.CreatorName = &name
+ } else {
+ // 查询作者
+ c, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Id: *param.CreatorId,
+ })
+ if serr != nil {
+ return serr
+ }
+ name = c.Name
+ param.CreatorName = &name
+ }
+ }
+ if param.Tags != nil {
+ tagIds, err1 := srv_contenttag.NormalizeContentTagIds(*param.Tags)
+ if err1 != nil {
+ return err1
+ }
+ if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil {
+ return err1
+ }
+ param.Tags = &tagIds
+ }
+ if param.CategoryRanks != nil {
+ seen := make(map[int64]bool, len(*param.CategoryRanks))
+ for _, rank := range *param.CategoryRanks {
+ if rank.Id <= 0 || seen[rank.Id] {
+ return srvdef.New(srvdef.ErrParamError, "分类ID无效或重复", "分类ID无效或重复")
+ }
+ seen[rank.Id] = true
+ if _, err := videocategorymod.FindOneVideoCategory(ctx, bson.M{videocategorymod.FN.VideoCategory.Id: rank.Id}); err != nil {
+ if err.Code == srvdef.ErrNoData {
+ return srvdef.New(srvdef.ErrParamError, "视频分类不存在", "视频分类不存在")
+ }
+ return err
+ }
+ }
+ }
+ isChangeActress := checkActressChange(*info, param)
+ isChangeTag := checkTagChange(*info, param)
+ if param.ActressIds != nil && len(*param.ActressIds) > 0 {
+ acts, serr := actress.FindManyActresss(ctx, bson.M{
+ actress.FN.Actress.Id: bson.M{"$in": *param.ActressIds},
+ })
+ if serr != nil {
+ log.Warn("UpdateVideoInfo FindManyActresss failed", log.Any("filter", param.ActressIds), log.E(serr))
+ return serr
+ }
+ acIds := make([]int64, 0)
+ for _, a := range acts {
+ acIds = append(acIds, a.Id)
+ }
+ param.ActressIds = &acIds
+ }
+
+ if param.Nodes != nil && len(*param.Nodes) > 0 {
+ nodes := *param.Nodes
+ for i, node := range nodes {
+ if len(node.NodeId) == 0 {
+ node.NodeId = primitive.NewObjectID().Hex()
+ nodes[i] = node
+ }
+ }
+ param.Nodes = &nodes
+ }
+ if param.Cover != nil && len(*param.Cover) > 0 {
+ if len(info.Video.Url) > 0 {
+ info.Video.Cover = *param.Cover
+ param.Video = &info.Video
+ }
+ }
+ if param.Status != nil && *param.Status != info.Status {
+ isChangeActress = true
+ tempActressIds := make([]int64, 0)
+ if param.ActressIds != nil {
+ tempActressIds = *param.ActressIds
+ }
+ go updateActressWorks(info.ActressIds, tempActressIds)
+ }
+ title := info.Title
+ if param.Title != nil {
+ title = *param.Title
+ }
+ keyworld := info.Keyworld
+ if param.Keyworld != nil {
+ keyworld = *param.Keyworld
+ }
+ exists := func(s string) bool {
+ doc, _ := videoinformationmod.FindOneVideoInformation(ctx, slugx.Filter(
+ videoinformationmod.FN.VideoInformation.Slug, videoinformationmod.FN.VideoInformation.Id, s, id))
+ return doc != nil
+ }
+ slug, changed, slugErr := slugx.PrepareOnUpdate(info.Slug, param.Slug,
+ utils.SlugSourceFromKeywordsOrTitle(keyworld, title), exists)
+ if slugErr != nil {
+ return slugErr
+ }
+ if changed {
+ param.Slug = &slug
+ } else {
+ param.Slug = nil
+ }
+ adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
+ param.IndexNow = nil
+ isPublished := param.Status != nil &&
+ info.Status != *param.Status &&
+ *param.Status == pubdef.InformationStatusVerifyOK
+ if isPublished || (param.IsReIndexNow != nil && *param.IsReIndexNow) {
+ indexNow := int64(0)
+ param.IndexNow = &indexNow
+ }
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx, adFilter, bson.M{"$set": param})
+ if serr != nil {
+ return serr
+ }
+ playletcache.ClearHomeCache()
+ if isChangeActress {
+ tempActressIds := make([]int64, 0)
+ if param.ActressIds != nil {
+ tempActressIds = *param.ActressIds
+ }
+ go updateActressWorks(info.ActressIds, tempActressIds)
+ }
+ if isChangeTag {
+ tempIds := make([]int64, 0)
+ if param.Tags != nil {
+ tempIds = *param.Tags
+ }
+ oldTagIds := append([]int64{}, info.Tags...)
+ newTagIds := append([]int64{}, tempIds...)
+ godef.Go(func() {
+ _ = srv_contenttag.SyncContentTagBindCount(context.Background(), oldTagIds, newTagIds, contenttagmod.ContentTagTypeVideo)
+ })
+ }
+ if param.Status != nil && info.Status != *param.Status {
+ if flag := srv_report.VideoStatusFlag(*param.Status); flag != 0 {
+ mediaCopy := *info
+ mediaCopy.Status = *param.Status
+ godef.Go(func() {
+ srv_report.ReportVideoStatusChangeEvent(&mediaCopy, flag, userId)
+ })
+ }
+ }
+ newStatus := info.Status
+ if param.Status != nil {
+ newStatus = *param.Status
+ }
+ srv_seopush.NotifyInfoAfterWrite(ctx, srv_seopush.KindVideo, id, info.Status, newStatus)
+ return nil
+}
+
+func checkActressChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool {
+ if updateParam.ActressIds != nil {
+ if len(info.ActressIds) != len(*updateParam.ActressIds) {
+ return true
+ }
+ actressIds := *updateParam.ActressIds
+ for i, v := range info.ActressIds {
+ if v != actressIds[i] {
+ return true
+ }
+ }
+ }
+ return false
+}
+func checkTagChange(info videoinformationmod.VideoInformation, updateParam VideoInfoUpdate) bool {
+ if updateParam.Tags != nil {
+ if len(info.Tags) != len(*updateParam.Tags) {
+ return true
+ }
+ tags := *updateParam.Tags
+ for i, v := range info.Tags {
+ if v != tags[i] {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func updateActressWorks(oldIds []int64, newIds []int64) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("updateActressWorks failed", log.Any("err", err))
+ }
+ }()
+ mapIds := make(map[int64]struct{})
+ if oldIds == nil {
+ oldIds = make([]int64, 0)
+ }
+ if newIds == nil {
+ newIds = make([]int64, 0)
+ }
+ for _, id := range newIds {
+ mapIds[id] = struct{}{}
+ }
+ for _, id := range oldIds {
+ mapIds[id] = struct{}{}
+ }
+ ids := make([]int64, 0)
+ for id := range mapIds {
+ ids = append(ids, id)
+ }
+ ctx := context.Background()
+ for _, id := range ids {
+ count, err := videoinformationmod.CountVideoInformations(ctx, bson.M{videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK, videoinformationmod.FN.VideoInformation.ActressIds: bson.M{"$in": []int64{id}}})
+ if err == nil {
+ _ = actress.UpdateOneActress(ctx, bson.M{
+ actress.FN.Actress.Id: id,
+ }, bson.M{
+ "$set": bson.M{
+ actress.FN.Actress.Works: count,
+ },
+ })
+ }
+ }
+}
+
+func AddVideoCollection(ctx context.Context, param *AddVideoCollectionReq) *srvdef.SrvErr {
+ tagIds, err1 := srv_contenttag.NormalizeContentTagIds(param.Tags)
+ if err1 != nil {
+ return err1
+ }
+ if err1 := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err1 != nil {
+ return err1
+ }
+ info := videoinformationmod.VideoInformation{
+ Id: mongos.Lid.VideoInfo.Get(),
+ Title: param.Title,
+ MediaType: videoinformationmod.MediaType_Collection,
+ AddedTime: time.Now(),
+ Keyworld: param.Keyworld,
+ FirstTag: param.FirstTag,
+ SecondTags: param.SecondTags,
+ CoverImg: param.CoverImg,
+ Desc: param.Desc,
+ Remark: param.Remark,
+ Nodes: make([]videoinformationmod.Node, 0),
+ Status: pubdef.InformationStatusWaiting,
+ Video: videoinformationmod.VideoInfo{},
+ Tags: tagIds,
+ Real: pubdef.Flow{},
+ VideoCount: 0,
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ }
+ if err1 := videoinformationmod.InsertOneVideoInformation(ctx, &info); err1 != nil {
+ return err1
+ }
+ return srv_contenttag.SyncContentTagBindCount(ctx, nil, tagIds, contenttagmod.ContentTagTypeVideo)
+}
+
+// AddVideoTags 批量追加视频统一标签
+func AddVideoTags(ctx context.Context, req *VideoAddTagsReq) *srvdef.SrvErr {
+ if req == nil || len(req.VideoIds) == 0 || len(req.Tags) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "视频ID和标签ID不能为空", "")
+ }
+ videoIds, err := normalizeFakeFlowIds(req.VideoIds)
+ if err != nil {
+ return err
+ }
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(req.Tags)
+ if err != nil {
+ return err
+ }
+ if len(tagIds) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
+ return err
+ }
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
+ }))
+ if err != nil {
+ return err
+ }
+ if len(list) == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ now := time.Now()
+ docs := make([]mongo.WriteModel, 0, len(list))
+ changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ newTagIds, changed := appendMissingVideoTagIds(item.Tags, tagIds)
+ if !changed {
+ continue
+ }
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
+ SetUpdate(bson.M{"$set": bson.M{
+ videoinformationmod.FN.VideoInformation.Tags: newTagIds,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: now,
+ }}))
+ changes = append(changes, srv_contenttag.ContentTagBindChange{
+ OldTagIds: append([]int64{}, item.Tags...),
+ NewTagIds: newTagIds,
+ })
+ }
+ if len(docs) == 0 {
+ return nil
+ }
+ if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil {
+ return err
+ }
+ playletcache.ClearHomeCache()
+ return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo)
+}
+
+// SetVideoTags 单/批量覆盖设置视频统一标签,并同步新标签表视频绑定数量
+func SetVideoTags(ctx context.Context, req *VideoSetTagsReq) *srvdef.SrvErr {
+ if req == nil || len(req.Ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "视频ID不能为空", "")
+ }
+ if req.TagIds == nil {
+ return srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
+ }
+ videoIds, err := normalizeFakeFlowIds(req.Ids)
+ if err != nil {
+ return err
+ }
+ tagIds, err := srv_contenttag.NormalizeContentTagIds(*req.TagIds)
+ if err != nil {
+ return err
+ }
+ if err := srv_contenttag.ValidateContentTags(ctx, tagIds, contenttagmod.ContentTagTypeVideo); err != nil {
+ return err
+ }
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": videoIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
+ }))
+ if err != nil {
+ return err
+ }
+ if len(list) == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ now := time.Now()
+ docs := make([]mongo.WriteModel, 0, len(list))
+ changes := make([]srv_contenttag.ContentTagBindChange, 0, len(list))
+ for _, item := range list {
+ if item == nil || sameVideoTagIds(item.Tags, tagIds) {
+ continue
+ }
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: item.Id}).
+ SetUpdate(bson.M{"$set": bson.M{
+ videoinformationmod.FN.VideoInformation.Tags: tagIds,
+ videoinformationmod.FN.VideoInformation.UpdatedAt: now,
+ }}))
+ changes = append(changes, srv_contenttag.ContentTagBindChange{
+ OldTagIds: append([]int64{}, item.Tags...),
+ NewTagIds: append([]int64{}, tagIds...),
+ })
+ }
+ if len(docs) == 0 {
+ return nil
+ }
+ if _, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false)); err != nil {
+ return err
+ }
+ playletcache.ClearHomeCache()
+ return srv_contenttag.SyncContentTagBindCountBatch(ctx, changes, contenttagmod.ContentTagTypeVideo)
+}
+
+func sameVideoTagIds(current, target []int64) bool {
+ if len(current) != len(target) {
+ return false
+ }
+ for i := range current {
+ if current[i] != target[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func appendMissingVideoTagIds(current, addIds []int64) ([]int64, bool) {
+ result := append([]int64{}, current...)
+ tagMap := make(map[int64]struct{}, len(current)+len(addIds))
+ for _, tagId := range current {
+ if tagId > 0 {
+ tagMap[tagId] = struct{}{}
+ }
+ }
+ changed := false
+ for _, tagId := range addIds {
+ if _, ok := tagMap[tagId]; ok {
+ continue
+ }
+ tagMap[tagId] = struct{}{}
+ result = append(result, tagId)
+ changed = true
+ }
+ return result, changed
+}
diff --git a/web/service/srv_videotag/define.go b/web/service/srv_videotag/define.go
new file mode 100644
index 0000000..82d8524
--- /dev/null
+++ b/web/service/srv_videotag/define.go
@@ -0,0 +1,38 @@
+package srv_videotag
+
+import (
+ "time"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoTagInfoReq struct {
+ pubdef.Page
+ Id *int64 `json:"id"`
+ Title *string `json:"title"`
+}
+type VideoTagInfoResp struct {
+ Total int64 `json:"total"`
+ List videotagmod.VideoTagInfoList `json:"list"`
+}
+type VideoTagInfoUpdate struct {
+ AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
+ Title *string `json:"title" bson:"title,omitempty"` // 帖子标题
+ Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
+ Sort *int64 `json:"sort" bson:"sort,omitempty"` // 排序
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
+ Works *int64 `json:"works" bson:"works,omitempty"` // 作品数量
+ MediaTypes *[]int64 `json:"mediaTypes" bson:"mediaTypes"` // 作者关联的资源类型 1视频
+ Images *string `json:"images" bson:"images,omitempty"` // 背景
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
+ IndexNow *int64 `bson:"indexNow,omitempty"`
+}
+type VideoTagInfoUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param VideoTagInfoUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type VideoTagInfoDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_videotag/hottag.go b/web/service/srv_videotag/hottag.go
new file mode 100644
index 0000000..451898f
--- /dev/null
+++ b/web/service/srv_videotag/hottag.go
@@ -0,0 +1,103 @@
+package srv_videotag
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/ipdailystatmod"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// HotTagReq 热门标签请求参数
+type HotTagReq struct {
+ Limit int64 `form:"limit" json:"limit"` // 返回条数,默认 10,最大 1000
+}
+
+// HotTagItem 热门标签结果
+type HotTagItem struct {
+ TagId int64 `json:"tagId"`
+ Title string `json:"title"`
+ WatchTimes int64 `json:"watchTimes"`
+}
+
+// HotTagResp 热门标签返回
+type HotTagResp struct {
+ List []*HotTagItem `json:"list"`
+}
+
+// GetHotTagsByPlayData 按预计算的 watchTimes 直接取 TopN(O(logN),依赖 {status:1, watchTimes:-1} 索引)
+func GetHotTagsByPlayData(ctx context.Context, req *HotTagReq) (*HotTagResp, *srvdef.SrvErr) {
+ limit := req.Limit
+ if limit <= 0 {
+ limit = 10
+ }
+ if limit > 1000 {
+ limit = 1000
+ }
+
+ resp := &HotTagResp{List: []*HotTagItem{}}
+
+ filter := bson.M{
+ videotagmod.FN.VideoTagInfo.Status: true,
+ videotagmod.FN.VideoTagInfo.WatchTimes: bson.M{"$gt": 0},
+ }
+ opts := options.Find().
+ SetSort(bson.D{
+ {Key: videotagmod.FN.VideoTagInfo.WatchTimes, Value: -1},
+ {Key: videotagmod.FN.VideoTagInfo.Id, Value: 1},
+ }).
+ SetLimit(limit)
+
+ tags, serr := videotagmod.FindManyVideoTagInfos(ctx, filter, opts)
+ if serr != nil {
+ return resp, serr
+ }
+
+ for _, t := range tags {
+ resp.List = append(resp.List, &HotTagItem{
+ TagId: t.Id,
+ Title: t.Title,
+ WatchTimes: t.WatchTimes,
+ })
+ }
+ return resp, nil
+}
+
+type IpStatReq struct {
+ TodayIP int64 `json:"todayIP"`
+ YesterdayIP int64 `json:"yesterdayIP"`
+}
+
+// FetchIpStat 从 MongoDB ip_daily_stat 集合读取 app 端同步过来的 IP 去重数。
+// app 端中间件收集 IP 存入本地 Redis,定时任务将 SCard 写入共享 MongoDB,web 在此读取。
+// 日期统一按北京时间(Asia/Shanghai)计算。
+func FetchIpStat(ctx context.Context) *IpStatReq {
+ ipStatReq := &IpStatReq{
+ TodayIP: 0,
+ YesterdayIP: 0,
+ }
+ now := time.Now()
+ bjNow := now.In(utils.LocBJ)
+ today := bjNow.Format("20060102")
+ yesterday := bjNow.AddDate(0, 0, -1).Format("20060102")
+
+ if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, today); err == nil {
+ ipStatReq.TodayIP = cnt
+ } else {
+ log.Warn("FetchIpStat: FindCount today failed",
+ log.Any("appId", pubdef.AppId), log.E(err))
+ }
+ if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, yesterday); err == nil {
+ ipStatReq.YesterdayIP = cnt
+ } else {
+ log.Warn("FetchIpStat: FindCount yesterday failed",
+ log.Any("appId", pubdef.AppId), log.E(err))
+ }
+ return ipStatReq
+}
diff --git a/web/service/srv_videotag/taginfo.go b/web/service/srv_videotag/taginfo.go
new file mode 100644
index 0000000..e192cba
--- /dev/null
+++ b/web/service/srv_videotag/taginfo.go
@@ -0,0 +1,167 @@
+package srv_videotag
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ MediaTypeVideo int64 = 1
+ MediaTypePost int64 = 2
+)
+
+var firstTagMu sync.Mutex
+
+// FindOrCreateByTitle 查询或创建同步数据中的一级标签。
+func FindOrCreateByTitle(ctx context.Context, title string, mediaType int64) (int64, *srvdef.SrvErr) {
+ title = strings.TrimSpace(title)
+ if title == "" {
+ return 0, nil
+ }
+
+ firstTagMu.Lock()
+ defer firstTagMu.Unlock()
+
+ filter := bson.M{videotagmod.FN.VideoTagInfo.Title: title}
+ tag, serr := videotagmod.FindOneVideoTagInfo(ctx, filter)
+ if serr == nil {
+ if mediaType > 0 && !containsMediaType(tag.MediaTypes, mediaType) {
+ if serr = videotagmod.UpdateOneVideoTagInfo(ctx, filter, bson.M{
+ "$addToSet": bson.M{videotagmod.FN.VideoTagInfo.MediaTypes: mediaType},
+ "$set": bson.M{videotagmod.FN.VideoTagInfo.UpdatedAt: time.Now()},
+ }); serr != nil {
+ return 0, serr
+ }
+ }
+ return tag.Id, nil
+ }
+ if serr.Code != srvdef.ErrNoData {
+ return 0, serr
+ }
+
+ now := time.Now()
+ tag = &videotagmod.VideoTagInfo{
+ Id: mongos.Lid.VideoTag.Get(),
+ Title: title,
+ Status: true,
+ MediaTypes: []int64{mediaType},
+ IndexNow: 0,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if serr = videotagmod.InsertOneVideoTagInfo(ctx, tag); serr != nil {
+ return 0, serr
+ }
+ return tag.Id, nil
+}
+
+// AppendTagID 向标签列表追加 ID,并避免重复。
+func AppendTagID(tags []int64, tagID int64) []int64 {
+ if tagID <= 0 {
+ return tags
+ }
+ for _, id := range tags {
+ if id == tagID {
+ return tags
+ }
+ }
+ return append(tags, tagID)
+}
+
+func containsMediaType(mediaTypes []int64, mediaType int64) bool {
+ for _, value := range mediaTypes {
+ if value == mediaType {
+ return true
+ }
+ }
+ return false
+}
+
+// GetVideoTagInfos 按照参数搜索视频标签
+func GetVideoTagInfos(ctx context.Context, param *VideoTagInfoReq) (*VideoTagInfoResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+ resp := &VideoTagInfoResp{}
+ if param.Id != nil {
+ filter[videotagmod.FN.VideoTagInfo.Id] = *param.Id
+ }
+ if param.Title != nil {
+ filter[videotagmod.FN.VideoTagInfo.Title] = bson.M{"$regex": *param.Title}
+ }
+
+ opt.SetSort(bson.M{videotagmod.FN.VideoTagInfo.Works: -1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := videotagmod.CountVideoTagInfos(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := videotagmod.FindManyVideoTagInfos(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddVideoTagInfo 添加一视频标签
+func AddVideoTagInfo(ctx context.Context,
+ ac *videotagmod.VideoTagInfo) *srvdef.SrvErr {
+
+ ac.Id = mongos.Lid.VideoTag.Get()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ serr := videotagmod.InsertOneVideoTagInfo(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+// DelVideoTagInfo 删除指定视频标签
+func DelVideoTagInfo(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{videotagmod.FN.VideoTagInfo.Id: id}
+ //删除
+ count, serr := videotagmod.DelOneVideoTagInfo(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateVideoTagInfo 更新指定视频标签
+func UpdateVideoTagInfo(ctx context.Context, id int64, param VideoTagInfoUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{videotagmod.FN.VideoTagInfo.Id: id}
+ param.IndexNow = nil
+ if param.IsReIndexNow != nil && *param.IsReIndexNow {
+ indexNow := int64(0)
+ param.IndexNow = &indexNow
+ }
+ updateDoc := bson.M{"$set": param}
+ serr := videotagmod.UpdateOneVideoTagInfo(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
diff --git a/web/skd/googleanalytics/analytics.go b/web/skd/googleanalytics/analytics.go
new file mode 100644
index 0000000..a741d8d
--- /dev/null
+++ b/web/skd/googleanalytics/analytics.go
@@ -0,0 +1,305 @@
+package googleanalytics
+
+import (
+ "context"
+ "os"
+ "strconv"
+ "time"
+ "videoweb/pub/common/httputil"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/analyticsdailystat"
+ "videoweb/pub/models/analyticsstat"
+ "videoweb/web/webg"
+
+ "go.mongodb.org/mongo-driver/bson"
+ analyticsdata "google.golang.org/api/analyticsdata/v1beta"
+ "google.golang.org/api/option"
+)
+
+// HourlyData 用于存储和排序每小时的数据
+type HourlyData struct {
+ Hour int
+ Users int64
+}
+type ChannelAnalyzeReq struct {
+ Channel string `from:"channel" json:"channel" binding:"required"` // 渠道
+ Begin time.Time `json:"begin" form:"begin"` // 开始时间
+ End time.Time `json:"end" form:"end"` // 结束时间
+ AppId *int32 `from:"appId" json:"appId" ` // 应用id
+}
+
+type AnalyzeItem struct {
+ Channel string `json:"channel" form:"channel"` // 渠道
+ AppId int32 `json:"appId" form:"appId"` // 应用ID
+ AppName string `json:"appName" form:"appName"` // 应用名称
+ Begin time.Time `json:"begin" form:"begin"` // 开始时间
+ End time.Time `json:"end" form:"end"` // 结束时间
+ RegisterCount int64 `json:"registerCount" form:"registerCount"` // 总注册IP数
+ RegisterIpCount int64 `json:"registerIpCount" form:"registerIpCount"` // 总注册IP数
+ VisitCount int64 `json:"visitCount" form:"visitCount"` // 总访问数
+ AdvertiseClick int64 `json:"advertiseClick" form:"advertiseClick"` // 广告点击数
+ RechargeCount int64 `json:"rechargeCount" form:"rechargeCount"` // 充值次数
+ RechargeAmount int64 `json:"rechargeAmount" form:"rechargeAmount"` // 充值金额
+ RechargeUsers int64 `json:"rechargeUsers" form:"rechargeUsers"` // 充值用户数
+ YesTodayRegisterCount int64 `json:"yesTodayRegisterCount" form:"yesTodayRegisterCount"` // 昨日注册IP数
+ YesTodayVisitCount int64 `json:"yesTodayVisitCount" form:"yesTodayVisitCount"` // 昨日访问数
+ YesTodayRechargeAmount int64 `json:"yesTodayRechargeAmount" form:"yesTodayRechargeAmount"` // 昨日充值
+ YesTodayRetainRate int64 `json:"yesTodayRetainRate" form:"yesTodayRetainRate"` // 昨日次流
+}
+type ChannelAnalyzeResp struct {
+ List []*AnalyzeItem `json:"list" form:"list"`
+}
+type AnalyzeResp struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data ChannelAnalyzeResp `json:"data"`
+}
+
+func AnalyticsData(appId int64, appName string, propertyId string) {
+ ctx := context.Background()
+ now := time.Now()
+ // 1. 认证授权 - 使用服务账号的 JSON 密钥文件
+ credentialsFile := "composed-anvil-470512-r8-fd674ac0d7dc.json"
+ b, err := os.ReadFile(credentialsFile)
+ if err != nil {
+ log.Error("AnalyticsData 读取凭证文件失败", log.E(err))
+ return
+ }
+
+ curStatTime := utils.RecentMinute(now.Add(time.Minute), 30) // 按20分钟对齐
+
+ data := analyticsstat.AnalyticsInfo{
+ AppId: appId,
+ SumDate: curStatTime,
+ AppName: appName,
+ DaySumDate: utils.BeginOfTime(curStatTime),
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ // 2. 创建 API 客户端
+ client, err := analyticsdata.NewService(ctx, option.WithCredentialsJSON(b))
+ if err != nil {
+ log.Error("AnalyticsData 无法创建 Analytics Data 客户端", log.E(err))
+ return
+ }
+ if client == nil {
+ log.Error("AnalyticsData client 为空")
+ return
+ }
+
+ req := &analyticsdata.RunRealtimeReportRequest{
+ // 虽然实时报告默认就是最近30分钟,但你可以通过 dateRanges 进行限制(如果需要的话)
+ // DateRanges: []*analyticsdata.DateRange{
+ // {StartDate: "30 minutes ago", EndDate: "now"}, // 某些客户端库可能支持相对时间
+ // },
+ Dimensions: []*analyticsdata.Dimension{
+ // 添加你关心的维度,例如国家或地区
+ {Name: "country"},
+ // 实时报告支持的维度有限,请参考官方文档:
+ // https://developers.google.com/analytics/devguides/reporting/data/v1/realtime-basics?hl=zh-cn
+ },
+ Metrics: []*analyticsdata.Metric{
+ {Name: "activeUsers"}, // 获取活跃用户数
+ },
+ }
+
+ // 4. 调用 RealTime API
+ resp, err := client.Properties.RunRealtimeReport(propertyId, req).Do()
+ if err != nil {
+ log.Error("AnalyticsData RunRealtimeReport 请求报告出错", log.E(err))
+ return
+ }
+ if resp == nil {
+ log.Error("AnalyticsData RunRealtimeReport resp 为空")
+ return
+ }
+
+ for _, row := range resp.Rows {
+ if row == nil || len(row.MetricValues) == 0 || row.MetricValues[0] == nil {
+ continue
+ }
+ activeUsers, _ := strconv.ParseInt(row.MetricValues[0].Value, 10, 64)
+ data.ActiveUsers += activeUsers
+ }
+
+ yestodayReq := &analyticsdata.RunReportRequest{
+ DateRanges: []*analyticsdata.DateRange{
+ {StartDate: "yesterday", EndDate: "yesterday"}, // 查询昨天
+ },
+ Dimensions: []*analyticsdata.Dimension{
+ {Name: "date"}, // 核心:添加日期维度,按天分组
+ },
+ Metrics: []*analyticsdata.Metric{
+ {Name: "screenPageViews"},
+ {Name: "totalUsers"},
+ {Name: "activeUsers"}, // 获取活跃用户数
+ {Name: "sessions"}, // 会话数
+ {Name: "newUsers"}, // 新增数量
+ },
+ }
+
+ yestodayResp, err := client.Properties.RunReport(propertyId, yestodayReq).Do()
+ if err != nil {
+ log.Error("AnalyticsData RunReport 请求报告出错", log.E(err))
+ return
+ }
+ if yestodayResp == nil {
+ log.Error("AnalyticsData RunReport yestodayResp 为空")
+ return
+ }
+ for _, row := range yestodayResp.Rows {
+ if row == nil || len(row.MetricValues) < 5 {
+ continue
+ }
+ data.YestodayPV, _ = strconv.ParseInt(row.MetricValues[0].Value, 10, 64)
+ data.YestodayUV, _ = strconv.ParseInt(row.MetricValues[1].Value, 10, 64)
+ data.YestodayActiveUsers, _ = strconv.ParseInt(row.MetricValues[2].Value, 10, 64)
+ data.YestodaySessions, _ = strconv.ParseInt(row.MetricValues[3].Value, 10, 64)
+ data.YestodayNewUsers, _ = strconv.ParseInt(row.MetricValues[4].Value, 10, 64)
+ }
+
+ analyticsstat.InsertOneAnalyticsInfo(ctx, &data)
+}
+
+func AnalyticsDailyData(appId int64, appName string, channel string, propertyId string) {
+ ctx := context.Background()
+ now := time.Now()
+ // 1. 认证授权 - 使用服务账号的 JSON 密钥文件
+ credentialsFile := "composed-anvil-470512-r8-fd674ac0d7dc.json"
+ b, err := os.ReadFile(credentialsFile)
+ if err != nil {
+ log.Error("AnalyticsDailyData 读取凭证文件失败", log.E(err))
+ return
+ }
+
+ // 2. 创建 API 客户端
+ client, err := analyticsdata.NewService(ctx, option.WithCredentialsJSON(b))
+ if err != nil {
+ log.Error("AnalyticsDailyData 无法创建 Analytics Data 客户端", log.E(err))
+ return
+ }
+ if client == nil {
+ log.Error("AnalyticsDailyData client 为空")
+ return
+ }
+
+ data := analyticsdailystat.AnalyticsDailyInfo{
+ AppId: appId,
+ SumDate: utils.BeginOfTime(now),
+ AppName: appName,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ yestodayReq := &analyticsdata.RunReportRequest{
+ DateRanges: []*analyticsdata.DateRange{
+ {StartDate: "7daysAgo", EndDate: "today"}, // 查询昨天
+ },
+ Dimensions: []*analyticsdata.Dimension{
+ {Name: "date"}, // 核心:添加日期维度,按天分组
+ },
+ Metrics: []*analyticsdata.Metric{
+ {Name: "screenPageViews"},
+ {Name: "totalUsers"},
+ {Name: "activeUsers"}, // 获取活跃用户数
+ {Name: "sessions"}, // 会话数
+ {Name: "newUsers"}, // 新增数量
+ },
+ }
+
+ yestodayResp, err := client.Properties.RunReport(propertyId, yestodayReq).Do()
+ if err != nil {
+ log.Error("AnalyticsDailyData RunReport 请求报告出错", log.E(err))
+ return
+ }
+ if yestodayResp == nil {
+ log.Error("AnalyticsDailyData RunReport yestodayResp 为空")
+ return
+ }
+ for _, row := range yestodayResp.Rows {
+ if row == nil || len(row.DimensionValues) == 0 || row.DimensionValues[0] == nil || len(row.MetricValues) < 5 {
+ continue
+ }
+ // data.SumDate
+ layout := "20060102" // Go 的特定格式:2006=年,01=月,02=日
+
+ // 解析字符串为时间对象
+ dayTime, err := time.Parse(layout, row.DimensionValues[0].Value)
+ if err != nil {
+ log.Error("AnalyticsDailyData time.Parse 失败", log.E(err))
+ continue
+ }
+ data.SumDate = dayTime.Add(-8 * time.Hour)
+ data.YestodayPV, _ = strconv.ParseInt(row.MetricValues[0].Value, 10, 64)
+ data.YestodayUV, _ = strconv.ParseInt(row.MetricValues[1].Value, 10, 64)
+ data.YestodayActiveUsers, _ = strconv.ParseInt(row.MetricValues[2].Value, 10, 64)
+ data.YestodaySessions, _ = strconv.ParseInt(row.MetricValues[3].Value, 10, 64)
+ data.YestodayNewUsers, _ = strconv.ParseInt(row.MetricValues[4].Value, 10, 64)
+
+ LastFilter := bson.M{
+ analyticsdailystat.FN.AnalyticsDailyInfo.SumDate: data.SumDate,
+ analyticsdailystat.FN.AnalyticsDailyInfo.AppId: appId,
+ }
+
+ _, serr := analyticsdailystat.FindOneAnalyticsDailyInfo(ctx, LastFilter)
+ if serr != nil && serr.Code == srvdef.ErrNoData {
+ analyticsdailystat.InsertOneAnalyticsDailyInfo(ctx, &data)
+ } else {
+ LastUpdate := bson.M{
+ "$set": bson.M{
+ analyticsdailystat.FN.AnalyticsDailyInfo.YestodayPV: data.YestodayPV,
+ analyticsdailystat.FN.AnalyticsDailyInfo.YestodayUV: data.YestodayUV,
+ analyticsdailystat.FN.AnalyticsDailyInfo.YestodayActiveUsers: data.YestodayActiveUsers,
+ analyticsdailystat.FN.AnalyticsDailyInfo.YestodaySessions: data.YestodaySessions,
+ analyticsdailystat.FN.AnalyticsDailyInfo.YestodayNewUsers: data.YestodayNewUsers,
+ analyticsdailystat.FN.AnalyticsDailyInfo.UpdatedAt: now,
+ },
+ }
+ analyticsdailystat.UpdateOneAnalyticsDailyInfo(ctx, LastFilter, LastUpdate)
+ }
+ }
+
+ analyzeList := ChannelAnalyzeResp{}
+ req := ChannelAnalyzeReq{
+ Channel: channel,
+ Begin: utils.BeginOfTime(now.AddDate(0, 0, -1)),
+ End: utils.EndOfTime(now.AddDate(0, 0, -1)),
+ }
+
+ param, _ := utils.ToJsonM(req)
+ innerStatUrl := webg.Conf.NewInnerStatUrl + "/api/stat/dailyStat/channel/analyze"
+ resp := AnalyzeResp{}
+ code, err := httputil.Client().POSTJsonWithJResp(&resp, innerStatUrl, nil, param)
+ if err != nil {
+ log.Error("ChannelAnalyze httputil innerStatUrl failed!", log.E(err))
+ }
+ if code != 200 {
+ log.Error("ChannelAnalyze httputil innerStatUrl code1!=200", log.Any("code", code))
+ }
+ log.Info("ChannelAnalyze", log.Any("req", req), log.Any("innerStatUrl", innerStatUrl), log.Any("analyzeList", resp.Data))
+ analyzeList = resp.Data
+
+ for _, channelAnalyze := range analyzeList.List {
+ data.LeadNewCount += float64(channelAnalyze.RegisterCount)
+ data.LeadAmount += float64(channelAnalyze.RechargeAmount)
+ }
+ LastFilter := bson.M{
+ analyticsdailystat.FN.AnalyticsDailyInfo.SumDate: utils.BeginOfTime(now),
+ analyticsdailystat.FN.AnalyticsDailyInfo.AppId: appId,
+ }
+ _, serr := analyticsdailystat.FindOneAnalyticsDailyInfo(ctx, LastFilter)
+ if serr != nil && serr.Code == srvdef.ErrNoData {
+ analyticsdailystat.InsertOneAnalyticsDailyInfo(ctx, &data)
+ } else {
+ LastUpdate := bson.M{
+ "$set": bson.M{
+ analyticsdailystat.FN.AnalyticsDailyInfo.LeadNewCount: data.LeadNewCount,
+ analyticsdailystat.FN.AnalyticsDailyInfo.LeadAmount: data.LeadAmount,
+ },
+ }
+
+ analyticsdailystat.UpdateOneAnalyticsDailyInfo(ctx, LastFilter, LastUpdate)
+ }
+}
diff --git a/web/skd/job/reportdata/event.go b/web/skd/job/reportdata/event.go
new file mode 100644
index 0000000..241a9c3
--- /dev/null
+++ b/web/skd/job/reportdata/event.go
@@ -0,0 +1,124 @@
+package reportdata
+
+import (
+ "context"
+ "encoding/json"
+ "videoweb/pub/common/log"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/eventmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var isRuining = false
+
+const (
+ maxRetryCount = 10 // 最大重试次数
+ maxReportNum = 100 // 最大上报数
+)
+
+func Report() {
+ //防止重入
+ if isRuining {
+ return
+ }
+ isRuining = true
+ defer func() {
+ isRuining = false
+ }()
+ ctx := context.Background()
+ opts := options.Find()
+ opts = opts.SetSort(bson.D{{Key: "_id", Value: 1}}).SetLimit(1000)
+ // 查询出一千条记录批量上传
+ list, stdErr := eventmod.FindManyEvent(ctx, bson.M{
+ "$or": []bson.M{
+ bson.M{
+ "status": eventmod.Default,
+ },
+ bson.M{
+ "status": eventmod.Failure,
+ "retryCount": bson.M{"$lte": maxRetryCount},
+ },
+ },
+ }, opts)
+ if stdErr != nil {
+ log.Error("Get event list error", log.E(stdErr))
+ return
+ }
+ if len(list) == 0 {
+ return
+ }
+ var eventList []*dataReport.CommonField
+ for _, v := range list {
+ event := &dataReport.CommonField{}
+ err := json.Unmarshal([]byte(v.Data), &event)
+ if err != nil {
+ log.Error("event report json.Unmarshal fail", log.Any("eventData", v.Data), log.E(err))
+ continue
+ }
+ eventList = append(eventList, event)
+ if len(eventList) < maxReportNum {
+ continue
+ }
+ // 批量上报
+ err = dataReport.BatchReport(ctx, eventList)
+ if err != nil {
+ log.Error("Batch report error", log.E(err))
+ // 将这些设置为失败
+ onReportEventFail(ctx, eventList)
+ } else {
+ onReportEventSuccess(ctx, eventList)
+ }
+ eventList = []*dataReport.CommonField{} // 重新变成0个
+ }
+
+ // 将剩余的批量上报
+ err := dataReport.BatchReport(ctx, eventList)
+ if err != nil {
+ log.Error("Batch report error", log.E(err))
+ // 将这些设置为失败
+ onReportEventFail(ctx, eventList)
+ } else {
+ onReportEventSuccess(ctx, eventList)
+ }
+}
+
+func onReportEventSuccess(ctx context.Context, eventList []*dataReport.CommonField) {
+ eventIds := []string{}
+ for _, v := range eventList {
+ eventIds = append(eventIds, v.EventID)
+ }
+ if len(eventIds) == 0 {
+ return
+ }
+ err := eventmod.UpdateManyEvent(ctx, bson.M{
+ "eventId": bson.M{"$in": eventIds},
+ }, bson.M{
+ "$set": bson.M{"status": eventmod.Success},
+ })
+ if err != nil {
+ log.Error("Update event fail", log.E(err))
+ return
+ }
+}
+
+func onReportEventFail(ctx context.Context, eventList []*dataReport.CommonField) {
+ eventIds := []string{}
+ for _, v := range eventList {
+ eventIds = append(eventIds, v.EventID)
+ }
+ if len(eventIds) == 0 {
+ return
+ }
+ err := eventmod.UpdateManyEvent(ctx, bson.M{
+ "eventId": bson.M{"$in": eventIds},
+ }, bson.M{
+ "$set": bson.M{"status": eventmod.Failure},
+ "$inc": bson.M{"retryCount": 1},
+ })
+ if err != nil {
+ log.Error("Update event fail", log.E(err))
+ return
+ }
+}
diff --git a/web/skd/job/seopush/seopush.go b/web/skd/job/seopush/seopush.go
new file mode 100644
index 0000000..2078e3c
--- /dev/null
+++ b/web/skd/job/seopush/seopush.go
@@ -0,0 +1,230 @@
+// Package seopush 为搜索引擎推送做兜底:补推已上架内容,并重试失败的任务。
+package seopush
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/seopushmod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+ "videoweb/web/service/srv_seopush"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ backfillLimit = 100
+ retryLimit = 50
+ taskKeepDays = 30
+)
+
+var jobMu sync.Mutex
+
+// Run 定时兜底任务
+func Run() {
+ if !jobMu.TryLock() {
+ log.Warn("seopush: skip job, previous run still in progress")
+ return
+ }
+ defer jobMu.Unlock()
+
+ ctx := context.Background()
+ if !srv_seopush.Enabled() {
+ discardPendingTasks(ctx)
+ return
+ }
+ webAddr := srv_seopush.SEOBaseURL(ctx)
+ if webAddr == "" {
+ return
+ }
+ backfillVideos(ctx)
+ backfillPlaylets(ctx)
+ retryPendingTasks(ctx, webAddr)
+ cleanupExpiredTasks(ctx)
+}
+
+func discardPendingTasks(ctx context.Context) {
+ filter := bson.M{"$or": []bson.M{
+ {seopushmod.FN.SeoPushTask.GoogleStatus: seopushmod.PushStatusPending},
+ {seopushmod.FN.SeoPushTask.IndexNowStatus: seopushmod.PushStatusPending},
+ }}
+ list, serr := seopushmod.FindManySeoPushTasks(ctx, filter)
+ if serr != nil {
+ log.Warn("seopush: find pending tasks to discard failed", log.E(serr))
+ return
+ }
+ if len(list) == 0 {
+ return
+ }
+ byKind := map[string][]int64{}
+ seen := map[string]map[int64]struct{}{}
+ for _, task := range list {
+ if task == nil || task.VideoId <= 0 {
+ continue
+ }
+ kind := kindFromTaskURL(task.Url)
+ if kind == "" {
+ continue
+ }
+ if seen[kind] == nil {
+ seen[kind] = make(map[int64]struct{})
+ }
+ if _, ok := seen[kind][task.VideoId]; ok {
+ continue
+ }
+ seen[kind][task.VideoId] = struct{}{}
+ byKind[kind] = append(byKind[kind], task.VideoId)
+ }
+ count, serr := seopushmod.DelManySeoPushTask(ctx, filter)
+ if serr != nil {
+ log.Warn("seopush: discard pending tasks failed", log.E(serr))
+ return
+ }
+ resetIndexNow(ctx, srv_seopush.KindVideo, byKind[srv_seopush.KindVideo])
+ resetIndexNow(ctx, srv_seopush.KindPlaylet, byKind[srv_seopush.KindPlaylet])
+ log.Info("seopush=skip=disabled", log.Any("discarded", count))
+}
+
+func kindFromTaskURL(pageURL string) string {
+ switch {
+ case strings.Contains(pageURL, "/video-detail"):
+ return srv_seopush.KindVideo
+ case strings.Contains(pageURL, "/drama-detail"):
+ return srv_seopush.KindPlaylet
+ default:
+ return ""
+ }
+}
+
+func resetIndexNow(ctx context.Context, kind string, ids []int64) {
+ if len(ids) == 0 {
+ return
+ }
+ in := bson.M{"$in": ids}
+ setZero := bson.M{"$set": bson.M{}}
+ var serr error
+ switch kind {
+ case srv_seopush.KindVideo:
+ setZero["$set"] = bson.M{videoinformationmod.FN.VideoInformation.IndexNow: int64(0)}
+ serr = videoinformationmod.UpdateManyVideoInformation(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Id: in}, setZero)
+ case srv_seopush.KindPlaylet:
+ setZero["$set"] = bson.M{playletmod.FN.Playlet.IndexNow: int64(0)}
+ serr = playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: in}, setZero)
+ }
+ if serr != nil {
+ log.Warn("seopush: reset indexNow failed", log.Any("kind", kind), log.E(serr))
+ }
+}
+
+func indexNowPendingFilter(statusField, indexNowField string, statusVal interface{}) bson.M {
+ return bson.M{
+ statusField: statusVal,
+ "$or": []bson.M{
+ {indexNowField: 0},
+ {indexNowField: bson.M{"$exists": false}},
+ },
+ }
+}
+
+func backfillVideos(ctx context.Context) {
+ opt := options.Find().
+ SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1}).
+ SetLimit(backfillLimit)
+ list, serr := videoinformationmod.FindManyVideoInformations(ctx, indexNowPendingFilter(
+ videoinformationmod.FN.VideoInformation.Status,
+ videoinformationmod.FN.VideoInformation.IndexNow,
+ pubdef.InformationStatusVerifyOK,
+ ), opt)
+ if serr != nil {
+ log.Warn("seopush: backfill find videos failed", log.E(serr))
+ return
+ }
+ if len(list) == 0 {
+ return
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ srv_seopush.RegisterUpdated(ctx, srv_seopush.KindVideo, item.Id, srv_seopush.PageVisibleInfo(item.Status))
+ }
+ log.Info("seopush: backfill videos registered", log.Any("count", len(list)))
+}
+
+func backfillPlaylets(ctx context.Context) {
+ opt := options.Find().
+ SetSort(bson.M{playletmod.FN.Playlet.OnlineAt: -1}).
+ SetLimit(backfillLimit)
+ list, serr := playletmod.FindManyPlaylet(ctx, indexNowPendingFilter(
+ playletmod.FN.Playlet.Status,
+ playletmod.FN.Playlet.IndexNow,
+ playletmod.StatusUp,
+ ), opt)
+ if serr != nil {
+ log.Warn("seopush: backfill find playlets failed", log.E(serr))
+ return
+ }
+ if len(list) == 0 {
+ return
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ srv_seopush.RegisterUpdated(ctx, srv_seopush.KindPlaylet, item.Id,
+ srv_seopush.PageVisiblePlaylet(item.Status, item.OnlineAt))
+ }
+ log.Info("seopush: backfill playlets registered", log.Any("count", len(list)))
+}
+
+func retryPendingTasks(ctx context.Context, webAddr string) {
+ conds := []bson.M{
+ {
+ seopushmod.FN.SeoPushTask.GoogleStatus: seopushmod.PushStatusPending,
+ seopushmod.FN.SeoPushTask.GoogleRetry: bson.M{"$lt": seopushmod.MaxPushRetry},
+ },
+ {
+ seopushmod.FN.SeoPushTask.IndexNowStatus: seopushmod.PushStatusPending,
+ seopushmod.FN.SeoPushTask.IndexNowRetry: bson.M{"$lt": seopushmod.MaxPushRetry},
+ },
+ }
+ opt := options.Find().
+ SetSort(bson.M{seopushmod.FN.SeoPushTask.CreatedAt: 1}).
+ SetLimit(retryLimit)
+ taskList, serr := seopushmod.FindManySeoPushTasks(ctx, bson.M{"$or": conds}, opt)
+ if serr != nil {
+ log.Warn("seopush: find pending tasks failed", log.E(serr))
+ return
+ }
+ if len(taskList) == 0 {
+ return
+ }
+ for _, task := range taskList {
+ if task == nil || strings.Contains(task.Url, "?") {
+ continue
+ }
+ srv_seopush.PushTask(ctx, webAddr, task)
+ }
+ log.Info("seopush: pending tasks processed", log.Any("count", len(taskList)))
+}
+
+func cleanupExpiredTasks(ctx context.Context) {
+ deadline := time.Now().AddDate(0, 0, -taskKeepDays)
+ count, serr := seopushmod.DelManySeoPushTask(ctx, bson.M{
+ seopushmod.FN.SeoPushTask.UpdatedAt: bson.M{"$lt": deadline},
+ })
+ if serr != nil {
+ log.Warn("seopush: cleanup expired tasks failed", log.E(serr))
+ return
+ }
+ if count > 0 {
+ log.Info("seopush: expired tasks cleaned", log.Any("count", count))
+ }
+}
diff --git a/web/skd/job/transcode_video.go b/web/skd/job/transcode_video.go
new file mode 100644
index 0000000..8ba5755
--- /dev/null
+++ b/web/skd/job/transcode_video.go
@@ -0,0 +1,108 @@
+package job
+
+import (
+ "context"
+ "strings"
+ "sync/atomic"
+ "videoweb/pub/common/laosiji"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/web/service/srv_videoinfo"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const transcodeBatchSize = 5
+const transcodeMaxFailCount = 3
+
+var transcodeRunning int32
+var transcodeCheckRunning int32
+
+const transcodeCheckBatchSize = 100
+
+// TranscodeVideoByWatchTimes 按观看次数挑选尚未转码成功的视频,批量提交 H265 转码。
+func TranscodeVideoByWatchTimes() {
+ if !atomic.CompareAndSwapInt32(&transcodeRunning, 0, 1) {
+ log.Info("TranscodeVideoByWatchTimes skip: previous run still running")
+ return
+ }
+ defer atomic.StoreInt32(&transcodeRunning, 0)
+
+ ctx := context.Background()
+ if laosiji.LaosijiClient == nil || laosiji.LaosijiDomains == nil {
+ log.Warn("TranscodeVideoByWatchTimes skip: laosiji not ready")
+ return
+ }
+ if _, err := laosiji.LaosijiDomains.Get(ctx); err != nil {
+ log.Warn("TranscodeVideoByWatchTimes skip: domains not ready", log.E(err))
+ return
+ }
+
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.HevcStatus: bson.M{
+ "$in": bson.A{videoinformationmod.HevcStatusNone, videoinformationmod.HevcStatusFailed},
+ },
+ videoinformationmod.FN.VideoInformation.HevcFailCount: bson.M{"$lt": transcodeMaxFailCount},
+ videoinformationmod.FN.VideoInformation.Video.Url: bson.M{"$ne": ""},
+ }
+ opts := options.Find().
+ SetSort(bson.D{{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: -1}}).
+ SetLimit(transcodeBatchSize)
+
+ videoList, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if serr != nil {
+ log.Error("TranscodeVideoByWatchTimes find video failed", log.Any("serr", serr))
+ return
+ }
+ if len(videoList) == 0 {
+ return
+ }
+
+ log.Info("TranscodeVideoByWatchTimes start", log.Any("count", len(videoList)))
+ for _, v := range videoList {
+ src := strings.TrimSpace(v.Video.Url)
+ if src == "" {
+ continue
+ }
+ if err := srv_videoinfo.SubmitTranscodeVideo(ctx, v.Id, src, v.Video.FileId); err != nil {
+ log.Warn("TranscodeVideoByWatchTimes submit failed",
+ log.Any("videoId", v.Id), log.Any("watchTimes", v.Real.WatchTimes), log.E(err))
+ }
+ }
+ log.Info("TranscodeVideoByWatchTimes done", log.Any("count", len(videoList)))
+}
+
+// CheckTranscodingResults 查询转码中任务并回写结果。
+func CheckTranscodingResults() {
+ if !atomic.CompareAndSwapInt32(&transcodeCheckRunning, 0, 1) {
+ log.Info("CheckTranscodingResults skip: previous run still running")
+ return
+ }
+ defer atomic.StoreInt32(&transcodeCheckRunning, 0)
+
+ ctx := context.Background()
+ if laosiji.LaosijiClient == nil || laosiji.LaosijiDomains == nil {
+ return
+ }
+
+ filter := bson.M{videoinformationmod.FN.VideoInformation.HevcStatus: videoinformationmod.HevcStatusPending}
+ opts := options.Find().SetLimit(transcodeCheckBatchSize)
+ videoList, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if serr != nil {
+ log.Error("CheckTranscodingResults find video failed", log.Any("serr", serr))
+ return
+ }
+ if len(videoList) == 0 {
+ return
+ }
+ for _, v := range videoList {
+ src := strings.TrimSpace(v.Video.Url)
+ if src == "" {
+ continue
+ }
+ if err := srv_videoinfo.RefreshTranscodeVideo(ctx, v.Id, src, v.Video.FileId); err != nil {
+ log.Warn("CheckTranscodingResults refresh failed", log.Any("videoId", v.Id), log.E(err))
+ }
+ }
+}
diff --git a/web/skd/playletwatermark/expire.go b/web/skd/playletwatermark/expire.go
new file mode 100644
index 0000000..dc1a965
--- /dev/null
+++ b/web/skd/playletwatermark/expire.go
@@ -0,0 +1,38 @@
+package playletwatermark
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletchaptermod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+const watermarkStuckTimeout = 48 * time.Hour
+
+// MarkStuckChaptersFailed 兜底:已登记/处理中超过 2 天仍未完成的剧集标记为处理失败。
+// media 原片已 CompletedFail 时由 submit 立即回写,不必等这 2 天。
+func MarkStuckChaptersFailed() {
+ ctx := context.Background()
+ now := time.Now()
+ cutoff := now.Add(-watermarkStuckTimeout)
+ err := playletchaptermod.UpdateManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: bson.M{
+ "$in": []playletchaptermod.WatermarkStatus{
+ playletchaptermod.WatermarkStatusRegistered,
+ playletchaptermod.WatermarkStatusProcessing,
+ },
+ },
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: bson.M{"$lt": cutoff},
+ }, bson.M{"$set": bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusFailed,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: now,
+ }})
+ if err != nil && err.Code != srvdef.ErrNoData {
+ log.Error("playletwatermark:MarkStuckChaptersFailed 更新失败", log.E(err))
+ return
+ }
+ log.Info("playletwatermark:MarkStuckChaptersFailed end", log.Any("cutoff", cutoff))
+}
diff --git a/web/skd/playletwatermark/submit.go b/web/skd/playletwatermark/submit.go
new file mode 100644
index 0000000..3ad57d2
--- /dev/null
+++ b/web/skd/playletwatermark/submit.go
@@ -0,0 +1,367 @@
+package playletwatermark
+
+import (
+ "context"
+ "errors"
+ "net/url"
+ "path"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ submitBatchSize = 10
+ taskAddRetryBatch = 20
+ mediaFileVersionV1 = "v1"
+ mediaFileVersionV2 = "v2"
+)
+
+var (
+ submitRunning bool
+ submitLock sync.Mutex
+)
+
+// SubmitPendingChapters 定时提交未加水印剧集到 media(每10分钟最多10条)
+func SubmitPendingChapters() {
+ submitLock.Lock()
+ if submitRunning {
+ submitLock.Unlock()
+ return
+ }
+ submitRunning = true
+ submitLock.Unlock()
+ defer func() {
+ submitLock.Lock()
+ submitRunning = false
+ submitLock.Unlock()
+ }()
+
+ ctx := context.Background()
+ log.Info("playletwatermark:SubmitPendingChapters begin")
+ //MarkStuckChaptersFailed()
+
+ sysCfg, err := cfg.FindOneSysCfg(ctx, bson.M{cfg.FN.SysCfg.Id: 1})
+ if err != nil || sysCfg == nil {
+ log.Error("playletwatermark:SubmitPendingChapters 读取系统配置失败", log.E(err))
+ return
+ }
+ if !hasVideoWatermark(sysCfg) {
+ log.Warn("playletwatermark:SubmitPendingChapters 水印素材未配置,跳过")
+ return
+ }
+ cfg.CacheAppKey(sysCfg.AppKey)
+
+ registerPendingSources(ctx)
+ submitWatermarkTasks(ctx, sysCfg)
+ log.Info("playletwatermark:SubmitPendingChapters end")
+}
+
+// registerPendingSources 将未处理剧集的相对 m3u8 登记到 media
+func registerPendingSources(ctx context.Context) {
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: bson.M{
+ "$in": []playletchaptermod.WatermarkStatus{
+ playletchaptermod.WatermarkStatusPending,
+ playletchaptermod.WatermarkStatusRegistered,
+ },
+ },
+ playletchaptermod.FN.PlayletChapter.VideoUrl: bson.M{"$gt": ""},
+ "$or": []bson.M{
+ {playletchaptermod.FN.PlayletChapter.MediaSourceId: bson.M{"$exists": false}},
+ {playletchaptermod.FN.PlayletChapter.MediaSourceId: ""},
+ },
+ }, options.Find().
+ SetSort(bson.D{{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1}}).
+ SetLimit(submitBatchSize))
+ if err != nil {
+ log.Error("playletwatermark:registerPendingSources 查询失败", log.E(err))
+ return
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ sourceID, syncErr := RegisterChapterSource(item)
+ if syncErr != nil {
+ log.Error("playletwatermark:registerPendingSources SyncMediaFile 失败",
+ log.Any("chapterId", item.Id), log.Any("videoUrl", item.VideoUrl), log.E(syncErr))
+ continue
+ }
+ now := time.Now()
+ if updateErr := playletchaptermod.UpdateOnePlayletChapter(ctx,
+ bson.M{playletchaptermod.FN.PlayletChapter.Id: item.Id},
+ bson.M{"$set": bson.M{
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: sourceID,
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusRegistered,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: now,
+ }}); updateErr != nil {
+ log.Error("playletwatermark:registerPendingSources 更新章节失败",
+ log.Any("chapterId", item.Id), log.E(updateErr))
+ }
+ }
+}
+
+// RegisterChapterSource 将剧集原片登记到 media 切片服,返回 mediaSourceId。
+func RegisterChapterSource(item *playletchaptermod.PlayletChapter) (string, error) {
+ if item == nil {
+ return "", errors.New("chapter is nil")
+ }
+ fieldPath, version := normalizeMediaFilePath(item.VideoUrl)
+ if fieldPath == "" {
+ return "", errors.New("invalid video path")
+ }
+ sourceID := primitive.NewObjectID()
+ req := &proxy.MediaFileSyncReq{
+ AppId: pubdef.AppId,
+ AppKey: cfg.CachedAppKey(context.Background()),
+ VideoList: []proxy.MediaFileSyncItem{
+ {
+ ID: sourceID.Hex(),
+ Title: mediaWatermarkTitle(playletForChapter(item), item),
+ TaskID: sourceID.Hex(),
+ FieldNameFs: fieldPath,
+ Width: item.Width,
+ Height: item.Height,
+ VideoTime: item.MediaTime,
+ Size: item.MediaSize,
+ MediaFileVersion: version,
+ IsPlaylet: true,
+ },
+ },
+ }
+ result, syncErr := proxy.SyncMediaFile(req, "")
+ if syncErr != nil {
+ return "", syncErr
+ }
+ if result == nil || result.Code != proxy.Success {
+ return "", errors.New("SyncMediaFile failed")
+ }
+ return sourceID.Hex(), nil
+}
+
+// submitWatermarkTasks 对已登记原片尝试 task/add,成功后进入处理中
+func submitWatermarkTasks(ctx context.Context, sysCfg *cfg.SysCfg) {
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusRegistered,
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: bson.M{"$gt": ""},
+ }, options.Find().
+ SetSort(bson.D{{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1}}).
+ SetLimit(taskAddRetryBatch))
+ if err != nil {
+ log.Error("playletwatermark:submitWatermarkTasks 查询失败", log.E(err))
+ return
+ }
+ for _, item := range list {
+ if item == nil || item.MediaSourceId == "" {
+ continue
+ }
+ if strings.TrimSpace(item.MediaTaskId) != "" {
+ if !restartWatermarkTask(ctx, item, sysCfg) {
+ continue
+ }
+ } else if !addWatermarkTask(ctx, item, sysCfg) {
+ continue
+ }
+ _ = playletchaptermod.UpdateOnePlayletChapter(ctx,
+ bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: item.Id,
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusRegistered,
+ },
+ bson.M{"$set": bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusProcessing,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now(),
+ }})
+ }
+}
+
+func addWatermarkTask(ctx context.Context, item *playletchaptermod.PlayletChapter, sysCfg *cfg.SysCfg) bool {
+ req := &proxy.MediaTaskAddReq{
+ MediaSourceId: item.MediaSourceId,
+ AppId: pubdef.AppId,
+ AppKey: strings.TrimSpace(sysCfg.AppKey),
+ Title: mediaWatermarkTitle(playletForChapter(item), item),
+ WatermarkImage: sysCfg.WatermarkImage,
+ WatermarkMedia: sysCfg.WatermarkMedia,
+ WatermarkMediaTop: sysCfg.WatermarkMediaTop,
+ WatermarkMediaTopRight: sysCfg.WatermarkMediaTopRight,
+ WatermarkMediaBottomLeft: sysCfg.WatermarkMediaBottomLeft,
+ }
+ result, addErr := proxy.AddMediaTask(req, "")
+ if addErr != nil || result == nil {
+ log.Info("playletwatermark:submitWatermarkTasks task/add 请求失败",
+ log.Any("chapterId", item.Id),
+ log.Any("mediaSourceId", item.MediaSourceId),
+ log.E(addErr))
+ return false
+ }
+ // 200=新建成功;6007=任务已存在。原片未处理完时保持已登记,下轮再试。
+ if result.Code != proxy.Success && result.Code != 6007 {
+ log.Info("playletwatermark:submitWatermarkTasks task/add 暂未成功",
+ log.Any("chapterId", item.Id),
+ log.Any("mediaSourceId", item.MediaSourceId),
+ log.Any("result", result))
+ markFailedIfMediaSourceFailed(ctx, item)
+ return false
+ }
+ return true
+}
+
+func restartWatermarkTask(ctx context.Context, item *playletchaptermod.PlayletChapter, sysCfg *cfg.SysCfg) bool {
+ watermarkImage := sysCfg.WatermarkImage
+ watermarkMedia := sysCfg.WatermarkMedia
+ watermarkMediaTop := sysCfg.WatermarkMediaTop
+ watermarkMediaTopRight := sysCfg.WatermarkMediaTopRight
+ watermarkMediaBottomLeft := sysCfg.WatermarkMediaBottomLeft
+ req := &proxy.MediaTaskUpdateWatermarkReq{
+ Id: item.MediaTaskId,
+ AppId: pubdef.AppId,
+ AppKey: strings.TrimSpace(sysCfg.AppKey),
+ WatermarkImage: &watermarkImage,
+ WatermarkMedia: &watermarkMedia,
+ WatermarkMediaTop: &watermarkMediaTop,
+ WatermarkMediaTopRight: &watermarkMediaTopRight,
+ WatermarkMediaBottomLeft: &watermarkMediaBottomLeft,
+ }
+ result, updateErr := proxy.UpdateMediaWatermark(req, "")
+ if updateErr != nil || result == nil {
+ log.Info("playletwatermark:submitWatermarkTasks update/watermark 请求失败",
+ log.Any("chapterId", item.Id),
+ log.Any("mediaTaskId", item.MediaTaskId),
+ log.E(updateErr))
+ return false
+ }
+ if result.Code != proxy.Success {
+ log.Info("playletwatermark:submitWatermarkTasks update/watermark 暂未成功",
+ log.Any("chapterId", item.Id),
+ log.Any("mediaTaskId", item.MediaTaskId),
+ log.Any("result", result))
+ markFailedIfMediaSourceFailed(ctx, item)
+ return false
+ }
+ return true
+}
+
+func markFailedIfMediaSourceFailed(ctx context.Context, item *playletchaptermod.PlayletChapter) {
+ if item == nil || item.MediaSourceId == "" {
+ return
+ }
+ source, err := proxy.QueryMediaSource(item.MediaSourceId, pubdef.AppId, "")
+ if err != nil {
+ log.Info("playletwatermark:markFailedIfMediaSourceFailed 查询原片失败",
+ log.Any("chapterId", item.Id),
+ log.Any("mediaSourceId", item.MediaSourceId),
+ log.E(err))
+ return
+ }
+ if source == nil || source.Status != proxy.MediaSourceCompletedFail {
+ return
+ }
+ now := time.Now()
+ if updateErr := playletchaptermod.UpdateOnePlayletChapter(ctx,
+ bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: item.Id,
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: bson.M{
+ "$in": []playletchaptermod.WatermarkStatus{
+ playletchaptermod.WatermarkStatusRegistered,
+ playletchaptermod.WatermarkStatusProcessing,
+ },
+ },
+ },
+ bson.M{"$set": bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusFailed,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: now,
+ }}); updateErr != nil {
+ log.Error("playletwatermark:markFailedIfMediaSourceFailed 更新章节失败",
+ log.Any("chapterId", item.Id),
+ log.E(updateErr))
+ return
+ }
+ log.Info("playletwatermark:markFailedIfMediaSourceFailed 原片已失败,章节标为处理失败",
+ log.Any("chapterId", item.Id),
+ log.Any("mediaSourceId", item.MediaSourceId),
+ log.Any("failReason", source.FailReason))
+}
+
+// normalizeMediaFilePath 将相对/绝对 m3u8 地址规范化为 media 可识别的相对路径(不含 .m3u8)
+func normalizeMediaFilePath(raw string) (fieldPath string, version string) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return "", ""
+ }
+ version = mediaFileVersionV2
+ if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
+ u, err := url.Parse(raw)
+ if err != nil {
+ return "", ""
+ }
+ raw = u.Path
+ }
+ raw = strings.TrimPrefix(raw, "/")
+ if strings.HasPrefix(raw, "v1/") {
+ version = mediaFileVersionV1
+ }
+ if strings.HasSuffix(strings.ToLower(raw), ".m3u8") {
+ raw = strings.TrimSuffix(raw, ".m3u8")
+ raw = strings.TrimSuffix(raw, ".M3U8")
+ }
+ raw = path.Clean(raw)
+ if raw == "." || raw == "" {
+ return "", ""
+ }
+ return raw, version
+}
+
+// mediaWatermarkTitle 传给 media 的标题:短剧标题 + "-" + 剧集标题
+func mediaWatermarkTitle(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) string {
+ playletName := ""
+ if playlet != nil {
+ playletName = strings.TrimSpace(playlet.Title)
+ }
+ episodeTitle := ""
+ if chapter != nil {
+ episodeTitle = strings.TrimSpace(chapter.Title)
+ }
+ if playletName != "" && episodeTitle != "" {
+ return playletName + "-" + episodeTitle
+ }
+ if playletName != "" {
+ return playletName
+ }
+ return episodeTitle
+}
+
+func playletForChapter(item *playletchaptermod.PlayletChapter) *playletmod.Playlet {
+ if item == nil || item.PlayletId <= 0 {
+ return nil
+ }
+ playlet, err := playletmod.FindOnePlaylet(context.Background(), bson.M{
+ playletmod.FN.Playlet.Id: item.PlayletId,
+ }, options.FindOne().SetProjection(bson.M{
+ playletmod.FN.Playlet.Title: 1,
+ }))
+ if err != nil {
+ return nil
+ }
+ return playlet
+}
+
+func hasVideoWatermark(sysCfg *cfg.SysCfg) bool {
+ if sysCfg == nil {
+ return false
+ }
+ return strings.TrimSpace(sysCfg.WatermarkMedia) != "" ||
+ strings.TrimSpace(sysCfg.WatermarkMediaTop) != "" ||
+ strings.TrimSpace(sysCfg.WatermarkMediaTopRight) != "" ||
+ strings.TrimSpace(sysCfg.WatermarkMediaBottomLeft) != ""
+}
diff --git a/web/skd/playletwatermark/sync.go b/web/skd/playletwatermark/sync.go
new file mode 100644
index 0000000..cf22f07
--- /dev/null
+++ b/web/skd/playletwatermark/sync.go
@@ -0,0 +1,284 @@
+package playletwatermark
+
+import (
+ "context"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/syncfilemod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ syncPageSize int64 = 1000
+ syncOverlapWindow = 10 * time.Minute
+ reconcileBatchSize int64 = 100
+)
+
+var (
+ syncRunning bool
+ syncLock sync.Mutex
+)
+
+// SyncCompletedChapters 定时从 media 拉取已完成水印任务,回写剧集地址与状态
+func SyncCompletedChapters() {
+ syncLock.Lock()
+ if syncRunning {
+ syncLock.Unlock()
+ return
+ }
+ syncRunning = true
+ syncLock.Unlock()
+ defer func() {
+ syncLock.Lock()
+ syncRunning = false
+ syncLock.Unlock()
+ }()
+
+ ctx := context.Background()
+ log.Info("playletwatermark:SyncCompletedChapters begin")
+
+ record, err := ensureSyncRecord(ctx)
+ if err != nil {
+ log.Error("playletwatermark:SyncCompletedChapters 获取同步水印失败", log.E(err))
+ return
+ }
+
+ recordTime, syncOK := syncIncrementalCompletedChapters(ctx, record.RecordTime)
+ if syncOK {
+ if updateErr := syncfilemod.UpdateOneSyncRecord(ctx, bson.M{
+ syncfilemod.FN.SyncRecord.Job: syncfilemod.PlayletWatermarkJob,
+ }, bson.M{"$set": bson.M{
+ syncfilemod.FN.SyncRecord.RecordTime: recordTime,
+ syncfilemod.FN.SyncRecord.SuccessAt: time.Now(),
+ syncfilemod.FN.SyncRecord.UpdatedAt: time.Now(),
+ }}); updateErr != nil {
+ log.Error("playletwatermark:SyncCompletedChapters 更新同步水印失败", log.E(updateErr))
+ }
+ }
+
+ reconcileProcessingChapters(ctx)
+ log.Info("playletwatermark:SyncCompletedChapters end",
+ log.Any("recordTime", recordTime),
+ log.Any("syncOK", syncOK))
+}
+
+// syncIncrementalCompletedChapters 按更新时间水印增量拉取。
+// 每次回退一段时间重复扫描,避免临界时间及同一时间戳分页造成漏数据;章节回写是幂等的。
+func syncIncrementalCompletedChapters(ctx context.Context, recordTime time.Time) (time.Time, bool) {
+ cursor := recordTime
+ if !cursor.IsZero() {
+ cursor = cursor.Add(-syncOverlapWindow)
+ }
+ maxRecordTime := recordTime
+ for {
+ req := proxy.MediaTaskSyncReq{
+ AppId: pubdef.AppId,
+ LastUpdateTime: cursor,
+ PageSize: syncPageSize,
+ }
+ result, syncErr := proxy.SyncMedia(&req, "")
+ if syncErr != nil || result == nil || result.Code != http.StatusOK {
+ log.Error("playletwatermark:SyncCompletedChapters SyncMedia 失败", log.Any("req", req), log.E(syncErr))
+ return recordTime, false
+ }
+ pageMaxTime := cursor
+ for _, media := range result.Data {
+ if media.UpdatedAt.After(pageMaxTime) {
+ pageMaxTime = media.UpdatedAt
+ }
+ if media.UpdatedAt.After(maxRecordTime) {
+ maxRecordTime = media.UpdatedAt
+ }
+ applyCompletedMedia(ctx, media, "incremental")
+ }
+ if int64(len(result.Data)) < syncPageSize {
+ break
+ }
+ if !pageMaxTime.After(cursor) {
+ log.Error("playletwatermark:SyncCompletedChapters 增量分页水印未推进",
+ log.Any("cursor", cursor),
+ log.Any("count", len(result.Data)))
+ return recordTime, false
+ }
+ cursor = pageMaxTime
+ }
+ return maxRecordTime, true
+}
+
+// reconcileProcessingChapters 反向对账所有处理中章节。
+// 它不依赖增量水印,可补偿本地更新失败、时间戳临界和历史水印越过造成的漏回写。
+func reconcileProcessingChapters(ctx context.Context) {
+ var lastID int64
+ for {
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: bson.M{"$gt": lastID},
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusProcessing,
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: bson.M{"$gt": ""},
+ }, options.Find().
+ SetSort(bson.D{{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1}}).
+ SetLimit(reconcileBatchSize))
+ if err != nil {
+ log.Error("playletwatermark:reconcileProcessingChapters 查询章节失败", log.E(err))
+ return
+ }
+ if len(list) == 0 {
+ return
+ }
+
+ sourceIDs := make([]string, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ sourceIDs = append(sourceIDs, item.MediaSourceId)
+ if item.Id > lastID {
+ lastID = item.Id
+ }
+ }
+ if len(sourceIDs) > 0 {
+ result, queryErr := proxy.QueryMedias(&proxy.MediaQueryesReq{
+ AppId: pubdef.AppId,
+ MediaSourceIds: sourceIDs,
+ }, "")
+ if queryErr != nil || result == nil || result.Code != http.StatusOK {
+ log.Error("playletwatermark:reconcileProcessingChapters 查询 media 任务失败",
+ log.Any("sourceCount", len(sourceIDs)),
+ log.Any("result", result),
+ log.E(queryErr))
+ return
+ }
+ for _, media := range result.Data {
+ applyCompletedMedia(ctx, media, "reconcile")
+ }
+ }
+ if int64(len(list)) < reconcileBatchSize {
+ return
+ }
+ }
+}
+
+func applyCompletedMedia(ctx context.Context, media proxy.MediaResp, source string) bool {
+ sourceID := media.MediaSourceId.Hex()
+ if sourceID == "" || sourceID == "000000000000000000000000" {
+ log.Warn("playletwatermark:applyCompletedMedia mediaSourceId 无效",
+ log.Any("mediaTaskId", media.Id.Hex()),
+ log.Any("source", source))
+ return false
+ }
+ fieldPath := strings.TrimSpace(media.FieldNameFs)
+ if fieldPath == "" {
+ log.Warn("playletwatermark:applyCompletedMedia 视频路径为空",
+ log.Any("mediaSourceId", sourceID),
+ log.Any("mediaTaskId", media.Id.Hex()),
+ log.Any("source", source))
+ return false
+ }
+ if !strings.HasSuffix(strings.ToLower(fieldPath), ".m3u8") {
+ fieldPath += ".m3u8"
+ }
+ taskID := media.Id.Hex()
+ setDoc := bson.M{
+ playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl: fieldPath,
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusDone,
+ playletchaptermod.FN.PlayletChapter.MediaTaskId: taskID,
+ playletchaptermod.FN.PlayletChapter.UpdatedAt: time.Now(),
+ }
+ if sourceName := strings.TrimSpace(media.SourceNameFs); sourceName != "" &&
+ isOriginalPlayletBySourceID(ctx, sourceID) {
+ // 原创才回填原片地址,供换水印/重置复用;非原创保留媒资原始 VideoUrl。
+ setDoc[playletchaptermod.FN.PlayletChapter.VideoUrl] = sourceName
+ }
+ updateErr := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: sourceID,
+ playletchaptermod.FN.PlayletChapter.WatermarkStatus: playletchaptermod.WatermarkStatusProcessing,
+ }, bson.M{"$set": setDoc})
+ if updateErr == nil {
+ log.Info("playletwatermark:applyCompletedMedia 回写成功",
+ log.Any("mediaSourceId", sourceID),
+ log.Any("mediaTaskId", taskID),
+ log.Any("watermarkVideoUrl", fieldPath),
+ log.Any("source", source))
+ return true
+ }
+ if updateErr.Code != srvdef.ErrNoData {
+ log.Error("playletwatermark:applyCompletedMedia 更新章节失败",
+ log.Any("mediaSourceId", sourceID),
+ log.Any("source", source),
+ log.E(updateErr))
+ return false
+ }
+
+ chapter, findErr := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: sourceID,
+ })
+ if findErr != nil {
+ if findErr.Code != srvdef.ErrNoData {
+ log.Error("playletwatermark:applyCompletedMedia 查询未匹配章节失败",
+ log.Any("mediaSourceId", sourceID),
+ log.E(findErr))
+ } else {
+ log.Warn("playletwatermark:applyCompletedMedia 未找到对应章节",
+ log.Any("mediaSourceId", sourceID),
+ log.Any("mediaTaskId", taskID),
+ log.Any("source", source))
+ }
+ return false
+ }
+ if chapter.WatermarkStatus != playletchaptermod.WatermarkStatusDone {
+ log.Warn("playletwatermark:applyCompletedMedia 章节状态不允许回写",
+ log.Any("chapterId", chapter.Id),
+ log.Any("mediaSourceId", sourceID),
+ log.Any("watermarkStatus", chapter.WatermarkStatus),
+ log.Any("source", source))
+ }
+ return false
+}
+
+func isOriginalPlayletBySourceID(ctx context.Context, sourceID string) bool {
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.MediaSourceId: sourceID,
+ }, options.FindOne().SetProjection(bson.M{
+ playletchaptermod.FN.PlayletChapter.PlayletId: 1,
+ }))
+ if err != nil || chapter == nil || chapter.PlayletId <= 0 {
+ return false
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ }, options.FindOne().SetProjection(bson.M{
+ playletmod.FN.Playlet.IsOriginal: 1,
+ }))
+ return err == nil && playlet != nil && playlet.IsOriginal
+}
+
+func ensureSyncRecord(ctx context.Context) (*syncfilemod.SyncRecord, *srvdef.SrvErr) {
+ filter := bson.M{syncfilemod.FN.SyncRecord.Job: syncfilemod.PlayletWatermarkJob}
+ record, err := syncfilemod.FindOneSyncRecord(ctx, filter)
+ if err == nil {
+ return record, nil
+ }
+ if err.Code != srvdef.ObjectDayStatDoesNotExist {
+ return nil, err
+ }
+ now := time.Now()
+ record = &syncfilemod.SyncRecord{
+ Job: syncfilemod.PlayletWatermarkJob,
+ RecordTime: time.Time{},
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if insertErr := syncfilemod.InsertOneSyncRecord(ctx, record); insertErr != nil {
+ return nil, insertErr
+ }
+ return record, nil
+}
diff --git a/web/skd/skd.go b/web/skd/skd.go
new file mode 100644
index 0000000..b58bb94
--- /dev/null
+++ b/web/skd/skd.go
@@ -0,0 +1,86 @@
+package skd
+
+import (
+ "context"
+ "math/rand"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/skd/job/reportdata"
+ seopushjob "videoweb/web/skd/job/seopush"
+ "videoweb/web/skd/playletwatermark"
+ "videoweb/web/skd/videosync"
+
+ "github.com/robfig/cron/v3"
+)
+
+var (
+ skdCron *cron.Cron
+)
+
+func init() {
+ skdCron = cron.New(cron.WithSeconds()) // 秒级定时任务
+}
+
+func Start() {
+ log.Info("skd start...")
+ rand.Seed(time.Now().UnixNano())
+
+ // 视频同步
+ _, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
+ videosync.VideoSync()
+ })
+
+ // 短剧水印:每5分钟提交最多10条未处理剧集到 media
+ _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ playletwatermark.SubmitPendingChapters()
+ })
+ // 短剧水印:每3分钟拉取 media 已完成任务并回写剧集
+ _, _ = skdCron.AddFunc("0 */3 * * * ?", func() {
+ playletwatermark.SyncCompletedChapters()
+ })
+ // 连载中短剧:每60分钟扫描一次并投递到 Redis 队列,由后台消费者逐条同步
+ _, _ = skdCron.AddFunc(srv_mediaresource.PlayletSerialSyncCronSpec, func() {
+ srv_mediaresource.EnqueueSerialPlayletSyncJobs()
+ })
+ // 历史短剧压缩封面:每5分钟扫描压缩封面为空的记录并投递队列
+ _, _ = skdCron.AddFunc(srv_mediaresource.PlayletCoverCompressCronSpec, func() {
+ srv_mediaresource.EnqueuePlayletCoverCompressJobs()
+ })
+ _, _ = skdCron.AddFunc(srv_mediaresource.AIContentBackflowCronSpec(), func() {
+ go srv_mediaresource.ReportAIContentBackflowYesterday()
+ })
+ _, _ = skdCron.AddFunc(srv_mediaresource.AIContentBackflowTodayStatCronSpec(), func() {
+ go srv_mediaresource.RefreshAIContentBackflowTodayStats()
+ })
+ srv_mediaresource.StartCGDJOriginalBackfillOnce(context.Background())
+ go srv_mediaresource.RefreshAIContentBackflowTodayStats()
+
+ _, _ = skdCron.AddFunc("0/1 * * * * ?", func() { // 每秒执行一次批量上报
+ reportdata.Report()
+ })
+
+ // 搜索引擎推送兜底:补推已上架内容并重试失败任务。
+ // 开关关闭或非正式环境不会真正对外提交,详见 srv_seopush.seoPushEnabled
+ _, _ = skdCron.AddFunc("0/30 * * * * ?", func() {
+ seopushjob.Run()
+ })
+
+ //if webg.Conf.LsjMediaConf.ApiUrl != "" {
+ // _, _ = skdCron.AddFunc("0 30 */2 * * ?", func() {
+ // job.TranscodeVideoByWatchTimes()
+ // })
+ // _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ // job.CheckTranscodingResults()
+ // })
+ //}
+
+ skdCron.Start()
+}
+
+func Stop() {
+ log.Info("srv_datinggirl:updater Stop...")
+ defer log.Info("srv_datinggirl: updater Stop!!!")
+ srv_mediaresource.CloseAIContentBackflowClient()
+ skdCron.Stop()
+}
diff --git a/web/skd/videosync/info.go b/web/skd/videosync/info.go
new file mode 100644
index 0000000..cf0e092
--- /dev/null
+++ b/web/skd/videosync/info.go
@@ -0,0 +1,74 @@
+package videosync
+
+import (
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+)
+
+type MediaTaskSyncReq struct {
+ AppId int64 `bson:"appId" json:"appId"` // APPID
+ LastUpdateTime time.Time `bson:"lastUpdateTime" json:"lastUpdateTime"` // 最后更新时间
+ PageSize int64 `bson:"pageSize" json:"pageSize"`
+}
+
+type MediaTaskSyncResp struct {
+ Code int `json:"code,omitempty"` //
+ Data []MediaTaskInfo `json:"data,omitempty"` //
+ Msg string `json:"msg,omitempty"` //
+}
+
+// @table:media
+type MediaTaskInfo struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"id"`
+ MediaSourceId primitive.ObjectID `bson:"mediaSourceId,omitempty" json:"mediaSourceId"` // 原始Id
+ VideoTime int32 `bson:"videoTime,omitempty" json:"videoTime"` // 影片长度
+ Size int64 `bson:"size,omitempty" json:"size"` // 文件size,单位byte
+ AppId int64 `bson:"appId,omitempty" json:"appId"` // 应用列表
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ CoverImgNew string `bson:"coverImgNew,omitempty" json:"coverImgNew"` // 动态封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ Title string `bson:"title,omitempty" json:"title"` // 标题
+ Publish string `bson:"publish,omitempty" json:"publish"` // 视频发布者
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+
+ Nodes []videoinformationmod.Node `bson:"nodes" json:"nodes"` // 节点
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ Status string `bson:"status,omitempty" json:"status"` // 当前状态
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ TaskStartTime time.Time `bson:"taskStartTime,omitempty" json:"taskStartTime"` // 任务开始时间
+ SourceNameFs string `bson:"sourceNameFs,omitempty" json:"sourceNameFs"` // 远程源文件
+ IsDelLocalDir bool `bson:"isDelLocalDir,omitempty" json:"isDelLocalDir"` // 是否删除本机文件
+ WatermarkImage string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
+ WatermarkMedia string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件
+ DownloadRetry int `bson:"downloadRetry,omitempty" json:"downloadRetry"` // 下载重试次数
+ WatermarkRetry int `bson:"watermarkRetry" json:"watermarkRetry"` // 水印重试次数
+ CompressRetry int `bson:"compressRetry" json:"compressRetry"` // 压缩重试次数
+ SegmentRetry int `bson:"segmentRetry" json:"segmentRetry"` // 切片重试次数
+ LinodeRetry int `bson:"linodeRetry" json:"linodeRetry"` // 上传重试次数
+
+ // 切片相关信息
+ Filename string `bson:"filename,omitempty" json:"filename"` // 文件名称
+ FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs"` // 文件名称
+ UnencryptedPreMp4 string `bson:"unencryptedPreMp4,omitempty" json:"unencryptedPreMp4"` // 不加密的预览MP4
+ TempFieldNameFs string `bson:"tempFieldNameFs,omitempty" json:"tempFieldNameFs"` // 临时存储
+ Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀
+ TaskID string `bson:"taskId,omitempty" json:"taskId"` // 项目组传来的唯一标示
+ Width int64 `bson:"width,omitempty" json:"width"` // 分辨率宽
+ Height int64 `bson:"height,omitempty" json:"height"` // 分辨率高
+ Ratio float64 `bson:"ratio,omitempty" json:"ratio"` // 宽高比
+ FrameRate string `bson:"frameRate,omitempty" json:"frameRate"` // 视频贞数
+ SyBitRate string `bson:"syBitRate,omitempty" json:"syBitRate"` // 音频bit率
+ VidBitRate string `bson:"vidBitRate,omitempty" json:"vidBitRate"` // 视频bit率
+ CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updatedAt"` // 更新时间
+ ProxyUpload *int32 `bson:"proxyUpload,omitempty" json:"proxyUpload"` // 代理上传
+ Retry int `bson:"retry" json:"retry"` // 重试次数 默认为零 最大重试3次
+ NotifySignal bool `bson:"notifySignal" json:"notifySignal"` //中止通知的信号 false 可以发送通知,true 中止发送通知
+
+ InfoTsName string `bson:"infoTsName" json:"infoTsName"` // 从该ts中获取视频分辨率等信息
+ SyncFlag int64 `bson:"syncFlag" json:"syncFlag"` // 同步状态
+ SrcKey string `bson:"srcKey,omitempty" json:"srcKey"` // 原始Key
+}
diff --git a/web/skd/videosync/videosync.go b/web/skd/videosync/videosync.go
new file mode 100644
index 0000000..62c1d87
--- /dev/null
+++ b/web/skd/videosync/videosync.go
@@ -0,0 +1,111 @@
+package videosync
+
+import (
+ "context"
+ nethttp "net/http"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/syncfilemod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+ "videoweb/web/service/srv_videoinfo"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+var (
+ isRunning = false
+ lock sync.Mutex
+)
+
+func getIsRunning() bool {
+ lock.Lock()
+ defer lock.Unlock()
+ return isRunning
+}
+func setRunning(b bool) {
+ lock.Lock()
+ defer lock.Unlock()
+ isRunning = b
+}
+func VideoSync() {
+ isRunning = getIsRunning()
+ if isRunning {
+ return
+ }
+ setRunning(true)
+ ctx := context.Background()
+ log.Info("VideoSync begin")
+ pageSize := int64(200)
+ recordFilter := bson.M{
+ syncfilemod.FN.SyncRecord.Job: syncfilemod.VideoJob,
+ }
+ recordInfo, serr := syncfilemod.FindOneSyncRecord(ctx, recordFilter)
+ if serr != nil {
+ if serr.Code == srvdef.ObjectDayStatDoesNotExist {
+ log.Info("VideoSync init record", log.Any("job", syncfilemod.VideoJob))
+ syncfilemod.InsertOneSyncRecord(ctx, &syncfilemod.SyncRecord{
+ ID: primitive.NewObjectID(),
+ Job: syncfilemod.VideoJob,
+ RecordId: 1,
+ RecordTime: time.Time{},
+ SuccessAt: time.Time{},
+ CreatedAt: time.Time{},
+ UpdatedAt: time.Time{},
+ })
+ } else {
+ log.Error("VideoSync FindOneSyncRecord failed", log.Any("recordFilter", recordFilter), log.E(serr))
+ }
+ setRunning(false)
+ return
+ }
+ defer func() {
+ setRunning(false)
+ // 更新同步记录
+ set := bson.M{
+ "$set": bson.M{
+ syncfilemod.FN.SyncRecord.RecordTime: recordInfo.RecordTime,
+ syncfilemod.FN.SyncRecord.SuccessAt: time.Now(),
+ },
+ }
+ _ = syncfilemod.UpdateOneSyncRecord(ctx, recordFilter, set)
+ }()
+ // 循环同步
+ for {
+ // 查询接口
+ req := proxy.MediaTaskSyncReq{
+ AppId: pubdef.AppId,
+ LastUpdateTime: recordInfo.RecordTime,
+ PageSize: pageSize,
+ }
+ result, err := proxy.SyncMedia(&req, "")
+ if err != nil || result.Code != nethttp.StatusOK {
+ log.Error("VideoSync SyncMedia", log.Any("req", req), log.E(err))
+ return
+ }
+
+ // 处理
+ for _, media := range result.Data {
+ if srv_videoinfo.IsPlayletMedia(ctx, media) {
+ log.Info("VideoSync skip playlet media", log.Any("mediaId", media.Id.Hex()), log.Any("mediaSourceId", media.MediaSourceId.Hex()))
+ }
+ se := srv_videoinfo.SyncOneVideo(ctx, media)
+ if se != nil {
+ log.Error("VideoSync SyncOneVideo", log.Any("media", media), log.E(se))
+ return
+ }
+ if media.UpdatedAt.After(recordInfo.RecordTime) {
+ recordInfo.RecordTime = media.UpdatedAt
+ }
+ }
+
+ if len(result.Data) < int(pageSize) {
+ log.Info("VideoSync success", log.Any("RecordTime", recordInfo.RecordTime))
+ break
+ }
+ }
+ log.Info("VideoSync success", log.Any("RecordTime", recordInfo.RecordTime))
+}
diff --git a/web/skd/videoupdater/videoupdater.go b/web/skd/videoupdater/videoupdater.go
new file mode 100644
index 0000000..5c419c6
--- /dev/null
+++ b/web/skd/videoupdater/videoupdater.go
@@ -0,0 +1,84 @@
+package videoupdater
+
+import (
+ "context"
+ "math/rand"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const _topN = 20
+
+// RefreshTopVideosUpdatedAt 遍历所有启用的分类,取排名靠前的20个资源,
+// 将 PageUpdatedAt 更新为前一个小时内的随机时间
+func RefreshTopVideosUpdatedAt() {
+ ctx := context.Background()
+ log.Info("videoupdater RefreshTopVideosUpdatedAt start")
+ defer log.Info("videoupdater RefreshTopVideosUpdatedAt end")
+
+ // 查询所有启用的分类
+ catFilter := bson.M{
+ videocategorymod.FN.VideoCategory.Status: true,
+ }
+ catOpts := &options.FindOptions{}
+ catOpts.SetLimit(500)
+ categories, serr := videocategorymod.FindManyVideoCategorys(ctx, catFilter, catOpts)
+ if serr != nil {
+ log.Error("videoupdater FindManyVideoCategorys failed", log.Any("err", serr))
+ return
+ }
+
+ now := time.Now()
+ oneHourAgo := now.Add(-1 * time.Hour)
+
+ for _, cat := range categories {
+ updateCategoryTopVideos(ctx, cat, oneHourAgo, now)
+ }
+}
+
+func updateCategoryTopVideos(ctx context.Context, cat *videocategorymod.VideoCategory, rangeStart, rangeEnd time.Time) {
+ // mdcom 的 VideoInformation.CategoryId 是单值,直接按等值匹配
+ videoFilter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.AddedTime: bson.M{"$lt": rangeEnd},
+ videoinformationmod.FN.VideoInformation.CategoryId: cat.Id,
+ }
+
+ videoOpt := &options.FindOptions{}
+ videoOpt.SetSort(bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Sort, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ })
+ videoOpt.SetLimit(_topN)
+
+ videos, serr := videoinformationmod.FindManyVideoInformations(ctx, videoFilter, videoOpt)
+ if serr != nil {
+ log.Warn("videoupdater FindManyVideoInformations failed",
+ log.Any("categoryId", cat.Id), log.Any("err", serr))
+ return
+ }
+
+ rangeDuration := rangeEnd.Sub(rangeStart)
+ for _, v := range videos {
+ randomOffset := time.Duration(rand.Int63n(int64(rangeDuration)))
+ randomTime := rangeStart.Add(randomOffset)
+
+ filter := bson.M{videoinformationmod.FN.VideoInformation.Id: v.Id}
+ update := bson.M{"$set": bson.M{videoinformationmod.FN.VideoInformation.PageUpdatedAt: randomTime}}
+ if err := videoinformationmod.UpdateOneVideoInformation(ctx, filter, update); err != nil {
+ log.Warn("videoupdater UpdateOneVideoInformation failed",
+ log.Any("videoId", v.Id), log.Any("err", err))
+ }
+ }
+
+ if len(videos) > 0 {
+ log.Info("videoupdater updated category top videos",
+ log.Any("categoryId", cat.Id), log.Any("categoryName", cat.Name), log.Any("count", len(videos)))
+ }
+}
diff --git a/web/webg/helper.go b/web/webg/helper.go
new file mode 100644
index 0000000..d96e306
--- /dev/null
+++ b/web/webg/helper.go
@@ -0,0 +1 @@
+package webg
diff --git a/web/webg/webg.go b/web/webg/webg.go
new file mode 100644
index 0000000..7bb6241
--- /dev/null
+++ b/web/webg/webg.go
@@ -0,0 +1,65 @@
+package webg
+
+const EsIndex = "sz"
+
+var (
+ Conf GlobalCfgEx
+)
+
+type GlobalCfgEx struct {
+ Redis struct {
+ Addr string `yaml:"addr"`
+ Db int `yaml:"db"`
+ Passwd string `yaml:"passwd"`
+ Subscribe bool `yaml:"subscribe"` // 是否有订阅功能
+ }
+ Mongodb struct {
+ Addr string `yaml:"addr"`
+ LogAddr string `yaml:"logaddr"`
+ }
+ Es struct {
+ Addr string `yaml:"addr"`
+ User string `yaml:"user"`
+ Pass string `yaml:"pass"`
+ } `yaml:"es"`
+ JwtKey string `yaml:"jwtKey"`
+ CdnKey string `yaml:"cdnKey"`
+ TgToken string `yaml:"tgToken"`
+ RunMod string `yaml:"runMod"`
+ Pprof bool `yaml:"pprof"`
+ Host string `yaml:"host"`
+ ListenAddr string `yaml:"listenAddr"`
+ VersionUrl string `yaml:"versionUrl"`
+ CdnDomain string `yaml:"cdnDomain"` // cdn域名
+ OrigDomain string `yaml:"origDomain"` // 源站域名
+ WebM3u8Host string `yaml:"webM3u8Host"` // web对外可访问域名(供老司机转码服务器拉取m3u8)
+ TsAuthKey string `yaml:"tsAuthKey"`
+ BucketVideoAddressV2 string `yaml:"bucketVideoAddressV2"`
+ BucketVideoAddress string `yaml:"bucketVideoAddress"`
+ LsjMediaConf struct {
+ AppId string `yaml:"appId"`
+ ApiKey string `yaml:"apiKey"`
+ ApiUrl string `yaml:"apiUrl"`
+ M3u8Yuan string `yaml:"m3u8Yuan"`
+ TsYuan string `yaml:"tsYuan"`
+ ImageYuan string `yaml:"imageYuan"`
+ DomainsRefreshHr int64 `yaml:"domainsRefreshHr"`
+ } `yaml:"lsjMediaConf"`
+ MediaResourceDBCfg struct {
+ BaseUrl string `yaml:"baseUrl"` // 域名
+ Merchat string `yaml:"merchat"` // 商户号
+ Secret string `yaml:"secret"` // 商户密钥
+ } `yaml:"mediaResourceDBCfg"`
+ NewInnerStatUrl string `yaml:"newInnerStatUrl"`
+ DataReport struct {
+ Url string `yaml:"url"`
+ } `yaml:"dataReport"`
+ DataCenterBackflow struct {
+ Key string `yaml:"key"`
+ SelfSourceBaseUrl string `yaml:"selfSourceBaseUrl"`
+ } `yaml:"dataCenterBackflow"`
+}
+
+func IsDev() bool {
+ return Conf.RunMod == "dev" || Conf.RunMod == "debug"
+}