commit 7974a423019e265b71a205aa5fcf24ed6bdfe373
Author: root
Date: Wed Sep 23 11:07:42 2026 +0800
Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b3a3b7f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+tool
+.DS_Store
+.project
+.idea
+.vscode
+/temp
+/run
+*.tar.gz
+*.tar.xz
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..a6eed0c
--- /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_91av_app
+ env GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_91av_app -v
+ tar czf web_91av_app.tar.gz web_91av_app
+ rm -f web_91av_app
+
+test:
+ @echo "test, GOPATH=$(GOPATH)"
+ GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_91av_app
+ tar czf web_91av_app.tar.gz web_91av_app docs/
+ rm -f web_91av_app
+localtest:
+ @echo "test, GOPATH=$(GOPATH)"
+ go build $(APP_BUILD_FLAG) -o app
\ No newline at end of file
diff --git a/app/api/appinfoapi/appinfo.go b/app/api/appinfoapi/appinfo.go
new file mode 100644
index 0000000..ac69bad
--- /dev/null
+++ b/app/api/appinfoapi/appinfo.go
@@ -0,0 +1,48 @@
+package appinfoapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/appser"
+ "videoweb/app/service/configser"
+ "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)
+ }
+
+ common.ServerJSON(c, srvdef.Success, proto.AppListResp{
+ Path: c.Request.URL.Path,
+ Config: configser.GetConfigInfo(),
+ Advitise: configser.GetAdvitise(),
+ CategoryList: categoryList,
+ Category: category,
+ })
+}
diff --git a/app/api/authapi/auth.go b/app/api/authapi/auth.go
new file mode 100644
index 0000000..9a2de17
--- /dev/null
+++ b/app/api/authapi/auth.go
@@ -0,0 +1,227 @@
+package authapi
+
+import (
+ "strings"
+ "videoweb/app/service/authser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+func requestUA(ctx *gin.Context) useragent.UA {
+ ua, _ := common.GetUA(ctx)
+ return ua
+}
+
+func applyRequestDevID(ua useragent.UA, devID string) useragent.UA {
+ if v := strings.TrimSpace(devID); v != "" {
+ ua.DevID = v
+ }
+ return ua
+}
+
+type registerReq struct {
+ AccountNo string `json:"accountNo" binding:"required"` // 账号
+ Email string `json:"email" binding:"required"` // 邮箱
+ Password string `json:"password" binding:"required"` // 明文密码
+ AgentInviteCode string `json:"agentInviteCode"` // 邀请人的邀请码(可选)
+ DevID string `json:"devID"` // 设备唯一标示(可选)
+ Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入)
+}
+
+type loginReq struct {
+ Account string `json:"account" binding:"required"` // 账号或邮箱
+ Password string `json:"password" binding:"required"` // 明文密码
+ DevID string `json:"devID"` // 设备唯一标示(可选)
+ Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入)
+}
+
+type resetPasswordReq struct {
+ AccountNo string `json:"accountNo" binding:"required"` // 账号
+ Email string `json:"email" binding:"required"` // 邮箱
+ NewPassword string `json:"newPassword" binding:"required"` // 新密码
+}
+
+type updateProfileReq struct {
+ NickName *string `json:"nickName"` // 新昵称(可选)
+ Avatar *string `json:"avatar"` // 新头像 URL(可选)
+ Introduction *string `json:"introduction"` // 简介(可选)
+ PrivacyMode *bool `json:"privacyMode"` // 隐私模式:true-开启,false-关闭(可选)
+ OldPassword *string `json:"oldPassword"` // 旧密码明文(修改密码时必填)
+ NewPassword *string `json:"newPassword"` // 新密码明文(修改密码时必填)
+}
+
+// GuestLogin 游客登录
+// @Tags 用户认证
+// @Summary 游客登录
+// @Description 按设备ID创建或找回游客账号;请求头可带 Authorization 复用已有游客会话。需传 X-User-Agent
+// @Accept json
+// @Produce json
+// @Param param body authser.GuestLoginReq true "游客登录参数"
+// @Success 200 {object} authser.LoginResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/guest [POST]
+func GuestLogin(ctx *gin.Context) {
+ var req authser.GuestLoginReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ ua, err := common.GetUA(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.NoUA, nil)
+ return
+ }
+ if req.DevID == "" {
+ common.ServerJSON(ctx, srvdef.ErrParamError, "devID不能为空")
+ return
+ }
+ ua.DevID = req.DevID
+ resp, serr := authser.GuestLogin(ctx, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), &req, ua)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Register 用户注册
+// @Tags 用户认证
+// @Summary 注册
+// @Description 传入账号、邮箱、明文密码。请求头带游客 Authorization 时升级该游客为普通用户,否则新建用户
+// @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
+ }
+ ua := applyRequestDevID(requestUA(ctx), req.DevID)
+ resp, serr := authser.Register(ctx, common.GetUserId(ctx), req.AccountNo, req.Email, req.Password, req.AgentInviteCode, ua, ipaddr.ResolveClientIP(ctx), req.Channel)
+ 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
+ }
+ ua := applyRequestDevID(requestUA(ctx), req.DevID)
+ resp, serr := authser.Login(ctx, req.Account, req.Password, ua, ipaddr.ResolveClientIP(ctx), req.Channel)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// UpdateProfile 修改用户资料
+// @Tags 用户认证
+// @Summary 修改用户资料(头像 / 简介 / 密码,可任意组合)
+// @Description 用户信息通过 token 获取,无需传账号;nickName、avatar、introduction、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) {
+ userId := common.GetUserId(ctx)
+ if userId <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, nil)
+ return
+ }
+ var req updateProfileReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := authser.UpdateProfile(ctx, &authser.UpdateProfileReq{
+ UserId: userId,
+ NickName: req.NickName,
+ Avatar: req.Avatar,
+ Introduction: req.Introduction,
+ PrivacyMode: req.PrivacyMode,
+ OldPassword: req.OldPassword,
+ NewPassword: req.NewPassword,
+ })
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// GetInfo 获取当前用户信息
+// @Tags 用户认证
+// @Summary 获取当前用户信息
+// @Description 通过 token 获取最新用户信息,包含金币余额、VIP状态等
+// @Produce json
+// @Success 200 {object} authser.LoginResp "用户信息"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/getInfo [GET]
+func GetInfo(ctx *gin.Context) {
+ userId := common.GetUserId(ctx)
+ if pubdef.IsCrawlerUser(userId) {
+ common.ServerJSON(ctx, srvdef.Success, authser.CrawlerLoginResp(""))
+ return
+ }
+ if userId <= 0 {
+ common.ServerJSON(ctx, srvdef.InvalidToken, nil)
+ return
+ }
+ resp, serr := authser.GetInfo(ctx, userId, requestUA(ctx))
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ResetPassword 找回密码
+// @Tags 用户认证
+// @Summary 找回密码
+// @Description 通过账号和邮箱双重校验后重置密码,两者必须同时匹配同一用户
+// @Accept json
+// @Produce json
+// @Param param body resetPasswordReq true "找回密码参数"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/auth/resetPassword [POST]
+func ResetPassword(ctx *gin.Context) {
+ var req resetPasswordReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ serr := authser.ResetPassword(ctx, req.AccountNo, req.Email, req.NewPassword)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/app/api/coinapi/coin.go b/app/api/coinapi/coin.go
new file mode 100644
index 0000000..86306d2
--- /dev/null
+++ b/app/api/coinapi/coin.go
@@ -0,0 +1,114 @@
+package coinapi
+
+import (
+ "videoweb/app/service/coinser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+type buyCoinReq struct {
+ PackageId int64 `json:"packageId" binding:"required"` // 购买套餐ID(金币充值)
+ PayMode string `json:"payMode" binding:"required"` // 支付方式:alipay/wechat
+}
+
+// BuyCoin 购买金币(发起第三方支付)
+// @Tags 金币
+// @Summary 购买金币
+// @Description 根据指定金币套餐发起第三方支付,返回支付链接
+// @Accept json
+// @Produce json
+// @Param param body buyCoinReq true "购买参数"
+// @Success 200 {object} coinser.BuyCoinResp "支付链接"
+// @Security ApiKeyAuth
+// @Router /api/app/coin/buy [POST]
+func BuyCoin(ctx *gin.Context) {
+ var req buyCoinReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ userId := common.GetUserId(ctx)
+ sysType := ctx.GetString(consts.CtxSysType)
+ devType := ctx.GetString(consts.CtxDevType)
+ ip := ipaddr.ResolveClientIP(ctx)
+
+ user, serr := coinser.GetUserInfo(ctx, userId)
+ if serr != nil || user == nil {
+ common.ServerJSON(ctx, srvdef.ErrUserNotExsit, nil)
+ return
+ }
+
+ resp, serr := coinser.CreateCoinOrder(ctx, userId, user.NickName, req.PackageId, req.PayMode, sysType, devType, ip, user.DistrictCode)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+type coinOrderHistoryReq struct {
+ Page int64 `json:"page" form:"page"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+}
+
+// GetCoinOrderHistory 查询当前用户金币购买记录
+// @Tags 金币
+// @Summary 查询金币购买记录
+// @Produce json
+// @Param page query int false "页码(默认1)"
+// @Param pageSize query int false "每页数量(默认10)"
+// @Success 200 {object} object "订单列表"
+// @Security ApiKeyAuth
+// @Router /api/app/coin/orders [GET]
+func GetCoinOrderHistory(ctx *gin.Context) {
+ var req coinOrderHistoryReq
+ _ = ctx.ShouldBindQuery(&req)
+ if req.Page <= 0 {
+ req.Page = 1
+ }
+ if req.PageSize <= 0 {
+ req.PageSize = 10
+ }
+
+ userId := common.GetUserId(ctx)
+ list, total, serr := coinser.GetCoinOrderHistory(ctx, userId, req.Page, req.PageSize)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, gin.H{"total": total, "list": list})
+}
+
+type coinTransListReq struct {
+ // 页码,默认1
+ Page int64 `json:"page" form:"page"`
+ // 每页数量,默认10,最大100
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+}
+
+// GetCoinTransList 查询当前用户金币余额明细
+// @Tags 金币
+// @Summary 查询金币余额明细 -- by 旺鸣
+// @Description 返回当前登录用户的金币收入、支出流水,按交易时间倒序
+// @Produce json
+// @Param page query int false "页码(默认1)"
+// @Param pageSize query int false "每页数量(默认10,最大100)"
+// @Success 200 {object} response.BasePageResp{list=[]proto.CoinTransListItemResp} "成功"
+// @Security ApiKeyAuth
+// @Router /api/app/coin/trans/list [GET]
+func GetCoinTransList(ctx *gin.Context) {
+ var req coinTransListReq
+ _ = ctx.ShouldBindQuery(&req)
+ userId := common.GetUserId(ctx)
+ resp, err := coinser.GetCoinTransList(ctx, userId, req.Page, req.PageSize)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/commentapi/comment.go b/app/api/commentapi/comment.go
new file mode 100644
index 0000000..f884637
--- /dev/null
+++ b/app/api/commentapi/comment.go
@@ -0,0 +1,86 @@
+package commentapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/commentser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SendComment 发布评论
+// @Tags 评论APP
+// @Summary 发布评论 --by 王鸣
+// @Description 发布短剧或短剧剧集评论,parentsId为0发布一级评论,大于0发布二级评论,replyCommentId表示实际回复的评论,objectType 10-短剧 11-短剧剧集
+// @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
+ }
+ ip := ipaddr.ResolveClientIP(ctx)
+ err := commentser.SendComment(ctx, common.GetUserId(ctx), ip, &in)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "success")
+}
+
+// List 评论列表
+// @Tags 评论APP
+// @Summary 评论列表 --by 王鸣
+// @Description 分页查询短剧或短剧剧集评论树,一级评论分页返回,二级评论挂在children中,objectType 10-短剧 11-短剧剧集
+// @Accept json
+// @Produce json
+// @Param params query proto.GetCommentListReq true "评论列表请求"
+// @Success 200 {object} proto.GetCommentListResp{list=[]proto.CommentInfo} "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Router /api/app/comment/list [get]
+func List(ctx *gin.Context) {
+ var in proto.GetCommentListReq
+ if err := ctx.ShouldBindQuery(&in); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := commentser.GetCommentPage(ctx, common.GetUserId(ctx), &in)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetLike 评论点赞/取消点赞
+// @Tags 评论APP
+// @Summary 评论点赞/取消点赞 --by 王鸣
+// @Description 设置当前用户对短剧评论或短剧剧集评论的点赞状态
+// @Accept json
+// @Produce json
+// @Param param body proto.CommentLikeSetReq true "评论点赞设置请求"
+// @Success 200 {object} proto.CommentLikeSetResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/app/comment/like/set [post]
+func SetLike(ctx *gin.Context) {
+ var in proto.CommentLikeSetReq
+ if err := ctx.ShouldBindJSON(&in); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := commentser.SetCommentLike(ctx, common.GetUserId(ctx), &in)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ 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..d6b3312
--- /dev/null
+++ b/app/api/config/config.go
@@ -0,0 +1,64 @@
+package config
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Info 查询APP公共配置
+// @Tags APP配置相关接口
+// @Summary 查询APP公共配置 --by 王鸣
+// @Description 返回最新地址信息、完整系统配置、广告配置和全站SEO配置
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.ConfigInfoResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/config/info [get]
+func Info(ctx *gin.Context) {
+ configInfo := configser.GetConfigInfo()
+ resp := &proto.ConfigInfoResp{
+ NewAddressInfo: configser.GetNewAddress(),
+ Config: configInfo,
+ Advitise: configser.GetAdvitise(),
+ SeoInfo: configser.GetSiteSeoInfo(),
+ SeoNavInfo: configser.GetNavSeoInfo(),
+ NavPageList: configser.GetNavPageList(),
+ ReportSDKConfig: configInfo.ReportSDKConfig,
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// 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) {
+ common.ServerJSON(ctx, srvdef.Success, configser.GetAppRichTextList(ctx.Query("ids"), ctx.Query("titles")))
+}
diff --git a/app/api/contactusapi/contactus.go b/app/api/contactusapi/contactus.go
new file mode 100644
index 0000000..50916ab
--- /dev/null
+++ b/app/api/contactusapi/contactus.go
@@ -0,0 +1,36 @@
+package contactusapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/contactusser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// AddContactUs 提交联系我们
+// @Tags 联系我们相关接口
+// @Summary 提交联系我们 -- by 旺鸣
+// @Description 用户提交联系我们表单,问题类型:1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+// @Accept json
+// @Produce json
+// @Param param body proto.ContactUsAddReq true "联系我们请求"
+// @Success 200 {object} proto.ContactUsAddResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/contactus/add [post]
+func AddContactUs(ctx *gin.Context) {
+ var req proto.ContactUsAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ userId := common.GetUserId(ctx)
+ resp, err := contactusser.AddContactUs(ctx, userId, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/feedbackapi/feedback.go b/app/api/feedbackapi/feedback.go
new file mode 100644
index 0000000..19f2098
--- /dev/null
+++ b/app/api/feedbackapi/feedback.go
@@ -0,0 +1,36 @@
+package feedbackapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/feedbackser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// AddFeedback 提交意见反馈
+// @Tags 意见反馈相关接口
+// @Summary 提交意见反馈 -- by 旺鸣
+// @Description 用户提交意见反馈,问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他
+// @Accept json
+// @Produce json
+// @Param param body proto.FeedbackReq true "意见反馈请求"
+// @Success 200 {object} proto.FeedbackResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/feedback/add [post]
+func AddFeedback(ctx *gin.Context) {
+ var req proto.FeedbackReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ userId := common.GetUserId(ctx)
+ resp, err := feedbackser.AddFeedback(ctx, userId, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
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..715fd4f
--- /dev/null
+++ b/app/api/home/home.go
@@ -0,0 +1,305 @@
+package home
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/commentser"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/translationser"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+ v10 "videoweb/pub/common/v10"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// VideoTagApi 标签搜索
+// @Tags 标签搜索
+// @Summary 标签搜索
+// @Description 标签搜索
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {object} proto.HomeHtmlData "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /richtext/:id [get]
+func RichtextApi(c *gin.Context) {
+ data := proto.RichTextHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetConfigInfo(),
+ Advitise: configser.GetAdvitise(),
+ }
+ richtextStr := c.Param("id")
+ richtextId, err := strconv.ParseInt(richtextStr, 10, 64)
+ if err != nil || richtextId < 1 {
+ common.ServerJSON(c, srvdef.NotFound, "")
+ return
+ }
+ richtextDate, serr := videoser.RichtextData(c, richtextId)
+ if serr != nil {
+ common.ServerJSON(c, serr.Code, "richtext param error")
+ return
+ }
+ data.CompletePath = fmt.Sprintf("/richtext/%d", richtextId)
+ data.RichTextInfo = richtextDate
+
+ common.JSONWriterCache(c, srvdef.Success, data)
+}
+
+// PageNotFound 404
+// @Tags 404
+// @Summary 404
+// @Description 404
+// @Accept json
+// @Produce json
+// @Param param body string true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+func PageNotFound(c *gin.Context) {
+ data := proto.NotFoundHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetConfigInfo(),
+ Advitise: configser.GetAdvitise(),
+ }
+ data.CompletePath = c.Request.URL.Path
+ c.JSON(http.StatusOK, data)
+ //common.ServerJSON(c, srvdef.Success, data)
+
+ //c.HTML(404, "404", gin.H{
+ // "data": data,
+ //})
+}
+
+func UrlRedirect(c *gin.Context) {
+ data := proto.UrlRedirectData{
+ Path: c.Request.URL.Path,
+ CompletePath: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetConfigInfo(),
+ Advitise: configser.GetAdvitise(),
+ }
+ url := c.Query("url")
+ data.UrlRedirect = url
+ common.ServerJSON(c, srvdef.Success, data)
+}
+
+func GetTagCategoryCache(ctx *gin.Context) {
+ data := configser.GetVideoTagCategoryDataList()
+ common.ServerJSON(ctx, srvdef.Success, data)
+}
+
+// VideoDetailInfo 新版视频详情页
+func VideoDetailInfo(c *gin.Context) {
+ data := proto.VideoDetailsHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetConfigInfo(),
+ Advitise: configser.GetAdvitise(),
+ }
+ ua := ""
+ if v, b := c.Get(consts.CtxUAH5); b {
+ ua = v.(string)
+ }
+ ip := ipaddr.ResolveClientIP(c)
+ idStr := c.Param("id")
+ videoId, err := strconv.ParseInt(idStr, 10, 64)
+ if err != nil || videoId < 1 {
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ return
+ }
+ renderVideoDetailPage(c, data, videoId, ip, ua)
+}
+
+func renderVideoDetailPage(c *gin.Context, data proto.VideoDetailsHtmlData, videoId int64, ip, ua string) {
+ videoDate, serr := videoser.VideoDetails(c, videoId, common.GetUserId(c), ip, ua)
+ if serr != nil {
+ if c.Query("format") == "json" {
+ common.ServerJSON(c, srvdef.ErrNoData, data)
+ return
+ }
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ return
+ }
+ data.Video = videoDate
+ data.Path = fmt.Sprintf("/videodetail/%d/", videoId)
+
+ data.Breadcrumbs = []proto.BreadcrumbItem{
+ {Name: "首页", Url: "/"},
+ {Name: "视频", Url: "/videoList"},
+ }
+ if len(videoDate.Categorys) > 0 {
+ cat := videoDate.Categorys[0]
+ data.Breadcrumbs = append(data.Breadcrumbs, proto.BreadcrumbItem{
+ Name: cat.Name,
+ Url: fmt.Sprintf("/category/%d/", cat.Id),
+ })
+ }
+ data.Breadcrumbs = append(data.Breadcrumbs, proto.BreadcrumbItem{
+ Name: videoDate.Title,
+ Url: "",
+ })
+
+ switch videoDate.Type {
+ case proto.VideoTypeVideo, proto.VideoTypeVideoV2:
+ comment, commentErr := commentser.GetCommentList(c, videoId, 0, 1)
+ data.CommentList = proto.GetCommentListResp{
+ ObjectId: videoId,
+ TotalCount: 0,
+ PageSize: 20,
+ PageNum: 1,
+ List: make([]proto.CommentInfo, 0),
+ }
+ if commentErr == nil {
+ data.CommentList = *comment
+ }
+ if c.Query("format") == "json" {
+ common.ServerJSON(c, srvdef.Success, data)
+ return
+ }
+ c.HTML(http.StatusOK, "videoDetail", gin.H{
+ "data": data,
+ })
+ default:
+ if c.Query("format") == "json" {
+ common.ServerJSON(c, srvdef.ErrNoData, data)
+ return
+ }
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ }
+}
+
+// VideoDetailPage 视频详情预览入口(不走模板缓存)
+func VideoDetailPage(c *gin.Context) {
+ id := c.DefaultQuery("id", "")
+ if id == "" {
+ c.HTML(http.StatusBadRequest, "404", gin.H{"data": proto.VideoDetailsHtmlData{}})
+ return
+ }
+ c.Params = gin.Params{{Key: "id", Value: id}}
+ c.Request.URL.Path = fmt.Sprintf("/videoDetail?id=%s", id)
+ VideoDetailInfo(c)
+}
+
+// VideoListPage 视频列表预览入口(不走模板缓存)
+func VideoListPage(c *gin.Context) {
+ id := c.DefaultQuery("id", "0")
+ sort := c.DefaultQuery("sort", "1")
+ page := c.DefaultQuery("page", "1")
+ c.Params = gin.Params{
+ {Key: "id", Value: id},
+ {Key: "sortType", Value: sort},
+ {Key: "page", Value: page},
+ }
+ c.Request.URL.Path = fmt.Sprintf("/videoList?id=%s&sort=%s&page=%s", id, sort, page)
+ VideoCategoryInfo(c)
+}
+
+// VideoCategoryInfo 新版视频列表页
+func VideoCategoryInfo(c *gin.Context) {
+ data := proto.VideoTopicHtmlData{
+ Path: c.Request.URL.Path,
+ Translations: translationser.GetTranslationData(),
+ BottonTranslations: translationser.GetTranslationBottomList(),
+ Config: configser.GetConfigInfo(),
+ Advitise: configser.GetAdvitise(),
+ }
+ topicStr := c.Param("id")
+ sortTypeStr := c.Param("sortType")
+ pageStr := c.Param("page")
+
+ pageNum, err := strconv.ParseInt(pageStr, 10, 64)
+ if err != nil || pageNum < 1 {
+ pageNum = 1
+ }
+ sortType, err := strconv.ParseInt(sortTypeStr, 10, 64)
+ if err != nil || sortType < 1 {
+ sortType = 0
+ }
+
+ if topicStr == "" {
+ renderVideoListPage(c, data, "0", 0, sortType, pageNum)
+ return
+ }
+
+ matche := v10.ParamIsNumber(topicStr)
+ if matche {
+ publish, err := strconv.ParseInt(topicStr, 10, 64)
+ if err != nil {
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ return
+ }
+ if publish == 0 {
+ renderVideoListPage(c, data, "0", 0, sortType, pageNum)
+ return
+ }
+ if publish < 1 {
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ return
+ }
+ sort := videoTopicSortType(sortType)
+ newPath := fmt.Sprintf("/videocategory/%d/%d/%d/", publish, sort, pageNum)
+ c.Redirect(http.StatusMovedPermanently, newPath)
+ return
+ }
+
+ topicId, err2 := strconv.ParseInt(topicStr, 10, 64)
+ if err2 != nil || topicId < 1 {
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ return
+ }
+ topicSegment := topicStr
+ if topicSegment == "" {
+ topicSegment = "0"
+ }
+ renderVideoListPage(c, data, topicSegment, topicId, sortType, pageNum)
+}
+
+func videoTopicSortType(sortType int64) int64 {
+ if sortType < 1 {
+ return int64(videoinformationmod.VideoSortType_New)
+ }
+ return sortType
+}
+
+func renderVideoListPage(c *gin.Context, data proto.VideoTopicHtmlData, topicSegment string, topicId, sortType, pageNum int64) {
+ sort := videoTopicSortType(sortType)
+ data.CompletePath = fmt.Sprintf("/videocategory/%s/%d/%d/", topicSegment, sort, pageNum)
+ totalSize, videoInfos, _, serr := videoser.VideoTopicData(c, false, topicId, sortType, pageNum)
+ data.VideoInfos = videoInfos
+ data.PageNum = pageNum
+ data.PageSize = pubdef.PageSize
+ data.TotalSize = pubdef.GetTotalPage(totalSize, pubdef.PageSize)
+ if data.VideoInfos == nil {
+ data.VideoInfos = []proto.VideoInfoData{}
+ }
+ if serr != nil {
+ if c.Query("format") == "json" {
+ common.ServerJSON(c, srvdef.ErrNoData, data)
+ return
+ }
+ c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
+ return
+ }
+ if c.Query("format") == "json" {
+ common.ServerJSON(c, srvdef.Success, data)
+ return
+ }
+ c.HTML(http.StatusOK, "videoList", gin.H{
+ "data": data,
+ })
+}
diff --git a/app/api/playletapi/playlet.go b/app/api/playletapi/playlet.go
new file mode 100644
index 0000000..a9b12f5
--- /dev/null
+++ b/app/api/playletapi/playlet.go
@@ -0,0 +1,547 @@
+package playletapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HomePlaylet app端首页短剧接口
+// @Tags app短剧相关接口
+// @Summary app端首页短剧接口 --by 王鸣
+// @Description 无请求参数,返回首页banner、最近更新和启用短剧分类数据;homeCategoryData按分类rank倒序返回全部启用分类,每个分类最多16条短剧
+// @Accept json
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// RecommendList app端分页查询分类短剧
+// @Tags app短剧相关接口
+// @Summary app端分页查询分类短剧(更多) --by 王鸣
+// @Description 根据短剧分类ID或分类slug分页查询已上架短剧;slug非空时优先按slug解析分类ID;pageSize默认10、最大100
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletRecommendListReq true "分类短剧分页请求"
+// @Success 200 {object} proto.PlayletRecommendListResp{list=[]proto.CommonData} "成功"
+// @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, nil)
+ return
+ }
+ resp, err := playletser.RecommendList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// List app端分页查询短剧
+// @Tags app短剧相关接口
+// @Summary 短剧 --by 王鸣
+// @Description 分页查询已上架短剧;可按题材ID或题材slug筛选,slug非空时优先按slug解析题材ID
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletVideoReq true "分页查询短剧请求"
+// @Success 200 {object} proto.PlayletVideoResp{list=[]proto.CommonData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/list [get]
+func List(ctx *gin.Context) {
+ var req proto.PlayletVideoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.List(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Rank app端分页查询短剧排行榜
+// @Tags app短剧相关接口
+// @Summary 短剧排行榜 --by 王鸣
+// @Description 分页查询已上架短剧排行榜;sortOrder=1按热度值倒序,2按上架时间倒序,3按收藏数倒序
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletRankReq true "分页查询短剧排行榜请求"
+// @Success 200 {object} proto.PlayletRankResp{list=[]proto.CommonData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/rank [get]
+func Rank(ctx *gin.Context) {
+ var req proto.PlayletRankReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.Rank(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// OperateList app端按运营类型分页查询短剧
+// @Tags app短剧相关接口
+// @Summary app端按运营类型分页查询短剧
+// @Description 按运营类型查询已上架短剧;operateType=1短剧 2漫剧;filterType=1最近更新(按上架时间) 2当前热播(按收藏数) 3独家原创(原创且按推荐排序)
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletOperateListReq true "按运营类型分页查询短剧请求"
+// @Success 200 {object} proto.PlayletOperateListResp{list=[]proto.CommonData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/operate/list [get]
+func OperateList(ctx *gin.Context) {
+ var req proto.PlayletOperateListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.OperateList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// TodayUpdate app端分页查询今日更新短剧
+// @Tags app短剧相关接口
+// @Summary app端分页查询今日更新短剧 --by 王鸣
+// @Description 分页查询已上架短剧
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletTodayUpdateReq true "分页查询今日更新短剧请求"
+// @Success 200 {object} proto.PlayletTodayUpdateResp{list=[]proto.CommonData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/today-update [get]
+func TodayUpdate(ctx *gin.Context) {
+ var req proto.PlayletTodayUpdateReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.TodayUpdate(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// TagVideoList app端根据标签ID分页查询短剧
+// @Tags app短剧相关接口
+// @Summary app端根据标签ID分页查询短剧 --by 王鸣
+// @Description 根据标签ID或标签slug分页查询已上架短剧;slug非空时优先按slug解析标签ID
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletTagVideoReq true "根据标签ID分页查询短剧请求"
+// @Success 200 {object} proto.PlayletTagVideoResp{list=[]proto.CommonData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/tag/list [get]
+func TagVideoList(ctx *gin.Context) {
+ var req proto.PlayletTagVideoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.TagVideoList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Search app端ES分页搜索短剧
+// @Tags app短剧相关接口
+// @Summary app端ES分页搜索短剧 --by 王鸣
+// @Description 使用ES分页搜索已上架短剧,支持剧名、题材名、简介和标签名检索
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletSearchReq true "短剧ES分页搜索请求"
+// @Success 200 {object} proto.PlayletSearchResp{list=[]proto.CommonData} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/search [get]
+func Search(ctx *gin.Context) {
+ var req proto.PlayletSearchReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.Search(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// HotLike app端热搜词和猜你喜欢
+// @Tags app短剧相关接口
+// @Summary app端热搜词和猜你喜欢
+// @Description 返回缓存的前10个热搜词,以及按收藏量最多的15部猜你喜欢短剧
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.PlayletHotLikeResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/hot-like [get]
+func HotLike(ctx *gin.Context) {
+ common.ServerJSON(ctx, srvdef.Success, playletser.HotLike(ctx))
+}
+
+// SetLike app端短剧点赞/取消点赞
+// @Tags app短剧相关接口
+// @Summary app端短剧点赞/取消点赞 --by 王鸣
+// @Description 设置当前用户短剧点赞状态,点赞数只在状态真实变化时增减
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletLikeSetReq true "短剧点赞设置请求"
+// @Success 200 {object} proto.PlayletLikeSetResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/like/set [post]
+func SetLike(ctx *gin.Context) {
+ var req proto.PlayletLikeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.SetLike(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetCollect app端短剧收藏/取消收藏
+// @Tags app短剧相关接口
+// @Summary app端短剧收藏/取消收藏 --by 王鸣
+// @Description 设置当前用户短剧收藏状态,收藏数只在状态真实变化时增减
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletCollectSetReq true "短剧收藏设置请求"
+// @Success 200 {object} proto.PlayletCollectSetResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/collect/set [post]
+func SetCollect(ctx *gin.Context) {
+ var req proto.PlayletCollectSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.SetCollect(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ForwardHeat app端短剧转发增加热度
+// @Tags app短剧相关接口
+// @Summary app端短剧转发增加热度 --by 王鸣
+// @Description 请求一次短剧热度值增加5,仅支持已上架短剧
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletForwardHeatReq true "短剧转发加热度请求"
+// @Success 200 {object} proto.PlayletForwardHeatResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/forward/heat [post]
+func ForwardHeat(ctx *gin.Context) {
+ var req proto.PlayletForwardHeatReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.ForwardHeat(ctx, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// CollectList app端我的短剧收藏列表
+// @Tags app短剧相关接口
+// @Summary app端我的短剧收藏列表 --by 王鸣
+// @Description 按当前用户收藏时间倒序分页返回短剧收藏列表,并返回已看/未看集数和最近观看位置
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletCollectListReq true "我的短剧收藏列表请求"
+// @Success 200 {object} proto.PlayletCollectListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/collect/list [get]
+func CollectList(ctx *gin.Context) {
+ var req proto.PlayletCollectListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.CollectList(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// HistoryList app端我的短剧观看历史列表
+// @Tags app短剧相关接口
+// @Summary app端我的短剧观看历史列表 --by 王鸣
+// @Description 按当前用户最近观看时间倒序分页返回短剧观看历史;pageSize默认20、最大100
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletHistoryListReq true "观看历史列表请求"
+// @Success 200 {object} proto.PlayletHistoryListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/history/list [get]
+func HistoryList(ctx *gin.Context) {
+ var req proto.PlayletHistoryListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.HistoryList(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ClearHistory app端清空我的短剧观看历史
+// @Tags app短剧相关接口
+// @Summary app端清空我的短剧观看历史 --by 王鸣
+// @Description 仅清空观看历史,不影响收藏列表和观看进度
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.PlayletHistoryClearResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/history/clear [post]
+func ClearHistory(ctx *gin.Context) {
+ resp, err := playletser.ClearHistory(ctx, common.GetUserId(ctx))
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetChapterLike app端短剧剧集点赞/取消点赞
+// @Tags app短剧相关接口
+// @Summary app端短剧剧集点赞/取消点赞 --by 王鸣
+// @Description 设置当前用户短剧剧集点赞状态,点赞数只在状态真实变化时增减
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletChapterLikeSetReq true "短剧剧集点赞设置请求"
+// @Success 200 {object} proto.PlayletChapterLikeSetResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/chapter/like/set [post]
+func SetChapterLike(ctx *gin.Context) {
+ var req proto.PlayletChapterLikeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.SetChapterLike(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetChapterCollect app端短剧剧集收藏/取消收藏
+// @Tags app短剧相关接口
+// @Summary app端短剧剧集收藏/取消收藏 --by 王鸣
+// @Description 设置当前用户短剧剧集收藏状态,收藏数只在状态真实变化时增减
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletChapterCollectSetReq true "短剧剧集收藏设置请求"
+// @Success 200 {object} proto.PlayletChapterCollectSetResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/chapter/collect/set [post]
+func SetChapterCollect(ctx *gin.Context) {
+ var req proto.PlayletChapterCollectSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.SetChapterCollect(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterCollectList app端我的短剧剧集收藏列表
+// @Tags app短剧相关接口
+// @Summary app端我的短剧剧集收藏列表 --by 王鸣
+// @Description 按当前用户收藏时间倒序分页返回短剧剧集收藏列表
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletChapterCollectListReq true "我的短剧剧集收藏列表请求"
+// @Success 200 {object} proto.PlayletChapterCollectListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/chapter/collect/list [get]
+func ChapterCollectList(ctx *gin.Context) {
+ var req proto.PlayletChapterCollectListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.ChapterCollectList(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ReportChapterWatch app端短剧剧集观看上报
+// @Tags app短剧相关接口
+// @Summary app端短剧剧集观看上报 --by 王鸣
+// @Description 上报当前用户短剧剧集播放进度,达到已看条件时用于我的收藏未看集数计算
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletChapterWatchReportReq true "短剧剧集观看上报请求"
+// @Success 200 {object} proto.PlayletChapterWatchReportResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/chapter/watch/report [post]
+func ReportChapterWatch(ctx *gin.Context) {
+ var req proto.PlayletChapterWatchReportReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.ReportChapterWatch(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// UnlockChapter app端使用金币逐集解锁短剧剧集
+// @Tags app短剧相关接口
+// @Summary app端使用金币逐集解锁短剧剧集 --by 王鸣
+// @Description 仅金币付费剧集可调用;接口具备幂等性,已解锁剧集重复调用不会重复扣金币
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletChapterUnlockReq true "短剧剧集金币解锁请求"
+// @Success 200 {object} proto.PlayletChapterUnlockResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/app/playlet/chapter/unlock [post]
+func UnlockChapter(ctx *gin.Context) {
+ var req proto.PlayletChapterUnlockReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err1 := playletser.UnlockChapter(ctx, common.GetUserId(ctx), &req)
+ if err1 != nil {
+ common.ServerJSON(ctx, err1.Code, err1.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Detail app端根据ID查询短剧详情
+// @Tags app短剧相关接口
+// @Summary app端根据ID查询短剧详情 --by 王鸣
+// @Description 根据短剧ID或slug查询已上架短剧详情;slug非空时优先按slug解析短剧ID
+// @Produce json
+// @Param params query proto.PlayletInfoReq true "短剧详情请求"
+// @Success 200 {object} proto.PlayletInfoResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/detail [get]
+func Detail(ctx *gin.Context) {
+ var req proto.PlayletInfoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.Detail(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterDetail app端根据ID查询短剧剧集详情
+// @Tags app短剧相关接口
+// @Summary app端根据ID查询短剧剧集详情 --by 王鸣
+// @Description 根据短剧剧集ID查询播放页当前剧集详情;可传playletId或playletSlug校验剧集归属,slug非空时优先解析短剧ID
+// @Produce json
+// @Param params query proto.PlayletChapterInfoReq true "短剧剧集详情请求"
+// @Success 200 {object} proto.PlayletChapterInfoResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/playlet/chapter/detail [get]
+func ChapterDetail(ctx *gin.Context) {
+ var req proto.PlayletChapterInfoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := playletser.ChapterDetail(ctx, common.GetUserId(ctx), &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterList app端根据短剧ID查询剧集列表
+// @Tags app短剧相关接口
+// @Summary app端根据短剧ID查询剧集列表 --by 王鸣
+// @Description 根据短剧ID或slug查询右侧选集列表;slug非空时优先按slug解析短剧ID
+// @Produce json
+// @Param params query proto.PlayletChapterListReq true "短剧剧集列表请求"
+// @Success 200 {object} proto.PlayletChapterListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/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 := playletser.ChapterList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/playletthemeapi/theme.go b/app/api/playletthemeapi/theme.go
new file mode 100644
index 0000000..9facaf1
--- /dev/null
+++ b/app/api/playletthemeapi/theme.go
@@ -0,0 +1,27 @@
+package playletthemeapi
+
+import (
+ "videoweb/app/service/playletthemeser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List app端短剧题材列表
+// @Tags app短剧题材相关接口
+// @Summary app端短剧题材列表 --by 王鸣
+// @Description 无请求参数,不分页,返回已上架题材列表
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.PlayletThemeListResponse "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/theme/list [get]
+func List(ctx *gin.Context) {
+ resp, err := playletthemeser.List(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/proto/about.go b/app/api/proto/about.go
new file mode 100644
index 0000000..2f04774
--- /dev/null
+++ b/app/api/proto/about.go
@@ -0,0 +1,28 @@
+package proto
+
+// AboutNodeItem 关于我们节点
+type AboutNodeItem struct {
+ NodeId int64 `json:"nodeId"`
+ Title string `json:"title"`
+ SubTitle string `json:"subTitle"`
+ Content string `json:"content"` // JSON字符串
+ //类型描述:
+ //文本:录入纯文本信息
+ //声明:图片、文本信息
+ //条目:可以录入多条,可删可增
+ //收录:收录类型(文章、网址、工具)、价格、单位、时效、描述
+ //图文:图片、文本信息
+ //注意:图片、文本信息
+ Type int `json:"type"`
+ ServiceDuration string `json:"serviceDuration"` // 服务时效
+ DurationIcon string `json:"durationIcon"` // 时效图标(CDN已处理)
+ Sort int `json:"sort"`
+}
+
+// AboutData 关于我们页面数据
+type AboutData struct {
+ Intro string `json:"intro"` // 简介
+ Nodes []*AboutNodeItem `json:"nodes"` // 节点列表
+ Categories []*ToolCategoryItem `json:"categories"` // 分类列表
+ Site SiteCommonData `json:"site"` // 通用站点数据
+}
diff --git a/app/api/proto/ads.go b/app/api/proto/ads.go
new file mode 100644
index 0000000..ce87080
--- /dev/null
+++ b/app/api/proto/ads.go
@@ -0,0 +1,34 @@
+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"` // 弹窗广告
+ BottomBanner []proxy.AdvitiseInfo `json:"bottomBanner" bson:"bottomBanner"` // 底部banner
+
+ VideoInfoList []proxy.AdvitiseInfo `json:"videoInfoList" bson:"videoInfoList"` // 视频详情
+ DownList []proxy.AdvitiseInfo `json:"downList" bson:"downList"` // 底部广告
+ FriendList []proxy.AdvitiseInfo `json:"friendList" bson:"friendList"` // 友链
+ TitleList []proxy.AdvitiseInfo `json:"titleList" bson:"titleList"` // 标题广告
+ PlayList []proxy.AdvitiseInfo `json:"playList" bson:"playList"` // 播放强制跳转
+ PauseyList []proxy.AdvitiseInfo `json:"pauseyList" bson:"pauseyList"` // 暂停广告
+}
diff --git a/app/api/proto/aitools.go b/app/api/proto/aitools.go
new file mode 100644
index 0000000..82029f4
--- /dev/null
+++ b/app/api/proto/aitools.go
@@ -0,0 +1,261 @@
+package proto
+
+import "videoweb/pub/pubdef"
+
+// -------- 公共站点数据 --------
+
+// FriendlyLinkItem 友情链接
+type FriendlyLinkItem struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Url string `json:"url"`
+ Sort int `json:"sort"`
+}
+
+// ContactInfoItem 联系方式
+type ContactInfoItem struct {
+ Id int64 `json:"id"`
+ Icon string `json:"icon"`
+ Type int `json:"type"` // 1=邮箱 2=URL 3=文字
+ Address string `json:"address"`
+ Sort int `json:"sort"`
+}
+
+// SiteCommonData 每个接口都会携带的通用站点信息
+type SiteCommonData struct {
+ Config ConfigInfo `json:"config"`
+ FriendlyLinks []*FriendlyLinkItem `json:"friendlyLinks"`
+ ContactInfos []*ContactInfoItem `json:"contactInfos"`
+ Advitise AdvitiseData `json:"advitise"`
+ ToolCount int64 `json:"toolCount"` // 审核通过的工具总数
+}
+
+// -------- 分类 --------
+
+// ToolSubCategoryItem 子分类简要信息
+type ToolSubCategoryItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Name string `json:"name"`
+ CategoryId int64 `json:"categoryId"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+}
+
+// ToolCategoryItem 主分类简要信息
+type ToolCategoryItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Name string `json:"name"`
+ Icon string `json:"icon"`
+ Sort int `json:"sort"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+ SubCategories []*ToolSubCategoryItem `json:"subCategories"`
+}
+
+// -------- 工具 --------
+
+// ToolBriefItem 工具简要信息(列表卡片)
+type ToolBriefItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Logo string `json:"logo"`
+ Snapshot string `json:"snapshot"`
+ Intro string `json:"intro"`
+ OfficialUrl string `json:"officialUrl"`
+ Hot int64 `json:"hot"`
+}
+
+// CategorySection 首页分类区块:分类标题 + 子分类列表 + 前10条工具
+type CategorySection struct {
+ CategoryId int64 `json:"categoryId"`
+ CategorySlug string `json:"categorySlug"`
+ CategoryName string `json:"categoryName"`
+ CategoryIcon string `json:"categoryIcon"`
+ CategorySort int `json:"categorySort"`
+ SubCategories []*ToolSubCategoryItem `json:"subCategories"`
+ Tools []*ToolBriefItem `json:"tools"`
+}
+
+// ToolNodeItem 工具节点
+type ToolNodeItem struct {
+ NodeId int64 `json:"nodeId"`
+ Title string `json:"title"`
+ Type int `json:"type"` // 1=文本 2=条目 3=QA 4=图片
+ Content string `json:"content"` // JSON字符串,格式因类型而异
+ Sort int `json:"sort"`
+ ReviewStatus int `json:"reviewStatus"` // 0=待审核 1=通过 2=下架
+}
+
+// ToolRecommendItem 推荐工具简要信息
+type ToolRecommendItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Intro string `json:"intro"`
+ Logo string `json:"logo"`
+}
+
+// ArticleRecommendItem 推荐文章简要信息
+type ArticleRecommendItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Intro string `json:"intro"`
+ Cover string `json:"cover"`
+ CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
+}
+
+// ToolDetailItem 工具详情
+type ToolDetailItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ CategoryId int64 `json:"categoryId"`
+ CategoryName string `json:"categoryName"`
+ SubCateId int64 `json:"subCateId"`
+ SubCateName string `json:"subCateName"`
+ Title string `json:"title"`
+ Logo string `json:"logo"`
+ Snapshot string `json:"snapshot"`
+ Intro string `json:"intro"`
+ Desc string `json:"desc"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+ OfficialUrl string `json:"officialUrl"`
+ Hot int64 `json:"hot"`
+ ViewCount int64 `json:"viewCount"`
+ Nodes []*ToolNodeItem `json:"nodes"`
+ RelatedTools []*ToolRecommendItem `json:"relatedTools"` // 相关推荐(优先同子分类,不足补其他分类)
+ RecommendTools []*ToolRecommendItem `json:"recommendTools"` // 工具推荐(随机6个)
+ RecommendArticles []*ArticleRecommendItem `json:"recommendArticles"` // 文章推荐(随机6个)
+ CommentList GetCommentListResp `json:"commentList"` // 评论
+ Categories []*ToolCategoryItem `json:"categories"` // 分类列表
+ Site SiteCommonData `json:"site"`
+}
+
+// -------- 文章 --------
+
+// ArticleBriefItem 文章简要信息(列表卡片)
+type ArticleBriefItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Intro string `json:"intro"`
+ Author string `json:"author"`
+ AuthorAvatar string `json:"authorAvatar"`
+ Cover string `json:"cover"`
+ ViewCount int64 `json:"viewCount"`
+ Sort int `json:"sort"`
+ CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
+}
+
+// ArticleNodeItem 文章节点
+type ArticleNodeItem struct {
+ NodeId int64 `json:"nodeId"`
+ Title string `json:"title"`
+ Type int `json:"type"` // 1=文本 2=条目 3=QA 4=图片
+ Content string `json:"content"` // JSON字符串,格式因类型而异
+ Sort int `json:"sort"`
+}
+
+// ArticleNavItem 文章导航(上一篇/下一篇)
+type ArticleNavItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+}
+
+// ArticleDetailItem 文章详情
+type ArticleDetailItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Intro string `json:"intro"`
+ Author string `json:"author"`
+ AuthorAvatar string `json:"authorAvatar"`
+ Cover string `json:"cover"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"`
+ ViewCount int64 `json:"viewCount"`
+ Sort int `json:"sort"`
+ ReviewStatus int `json:"reviewStatus"`
+ CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
+ Nodes []*ArticleNodeItem `json:"nodes"`
+ Prev *ArticleNavItem `json:"prev"` // 上一篇(无则为 null)
+ Next *ArticleNavItem `json:"next"` // 下一篇(无则为 null)
+ RecommendTools []*ToolRecommendItem `json:"recommendTools"` // 工具推荐(随机6个)
+ RecommendArticles []*ArticleRecommendItem `json:"recommendArticles"` // 文章推荐(随机6个)
+ CommentList GetCommentListResp `json:"commentList"` // 评论
+ Categories []*ToolCategoryItem `json:"categories"` // 分类列表
+ Site SiteCommonData `json:"site"`
+}
+
+// -------- 页面级响应 --------
+
+// AIToolsHomeData 工具首页数据
+type AIToolsHomeData struct {
+ Categories []*ToolCategoryItem `json:"categories"`
+ HotTools []*ToolBriefItem `json:"hotTools"` // 热门推荐工具(按设置时间倒序)
+ Sections []*CategorySection `json:"sections"`
+ Articles []*ArticleBriefItem `json:"articles"`
+ Site SiteCommonData `json:"site"`
+}
+
+// AIToolsListData 工具列表页数据
+type AIToolsListData struct {
+ Categories []*ToolCategoryItem `json:"categories"`
+ Total int64 `json:"total"`
+ List []*ToolBriefItem `json:"list"`
+ Site SiteCommonData `json:"site"`
+}
+
+// SearchArticleItem 搜索命中的文章
+type SearchArticleItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Intro string `json:"intro"`
+ Author string `json:"author"`
+ AuthorAvatar string `json:"authorAvatar"`
+ Cover string `json:"cover"`
+ CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
+ Score float32 `json:"score"` // 相关度分数
+}
+
+// SearchToolItem 搜索命中的工具
+type SearchToolItem struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ CategoryId int64 `json:"categoryId"`
+ SubCateId int64 `json:"subCateId"`
+ Title string `json:"title"`
+ Logo string `json:"logo"`
+ Snapshot string `json:"snapshot"`
+ Intro string `json:"intro"`
+ OfficialUrl string `json:"officialUrl"`
+ Hot int64 `json:"hot"`
+ Score float32 `json:"score"` // 相关度分数
+}
+
+// AISearchData 搜索结果
+type AISearchData struct {
+ Keyword string `json:"keyword"`
+ Articles []*SearchArticleItem `json:"articles"`
+ Tools []*SearchToolItem `json:"tools"`
+ Site SiteCommonData `json:"site"`
+}
+
+// AIRecommendData 推荐展示数据
+type AIRecommendData struct {
+ Articles []*ArticleBriefItem `json:"articles"` // 随机2篇文章
+ Tools []*ToolBriefItem `json:"tools"` // 随机20个工具
+ Site SiteCommonData `json:"site"`
+}
+
+// AIArticleListData 文章列表页数据
+type AIArticleListData struct {
+ Categories []*ToolCategoryItem `json:"categories"`
+ Total int64 `json:"total"`
+ List []*ArticleBriefItem `json:"list"`
+ Site SiteCommonData `json:"site"`
+ RecommendTools []*ToolRecommendItem `json:"recommendTools"` // 工具推荐(随机6个)
+ RecommendArticles []*ArticleRecommendItem `json:"recommendArticles"` // 文章推荐(随机6个)
+}
diff --git a/app/api/proto/appinfo.go b/app/api/proto/appinfo.go
new file mode 100644
index 0000000..3aee214
--- /dev/null
+++ b/app/api/proto/appinfo.go
@@ -0,0 +1,55 @@
+package proto
+
+// AppListResp 应用列表接口响应
+type AppListResp struct {
+ // 当前路径
+ Path string `json:"path"`
+ // 配置信息
+ Config ConfigInfo `json:"config"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 应用分类列表(含各分类下的应用列表)
+ 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/coin.go b/app/api/proto/coin.go
new file mode 100644
index 0000000..6bc9a7f
--- /dev/null
+++ b/app/api/proto/coin.go
@@ -0,0 +1,35 @@
+package proto
+
+import "videoweb/pub/models/cointransmod"
+
+// CoinTransListItemResp 金币余额明细列表项
+type CoinTransListItemResp struct {
+ // 流水ID
+ Id int64 `json:"id"`
+ // 标题,优先使用备注,备注为空时使用交易类型名称
+ Title string `json:"title"`
+ // 收支方向:1-收入,-1-支出
+ FlowDir cointransmod.FlowDir `json:"flowDir"`
+ // 收支方向名称:收入/支出
+ FlowDirName string `json:"flowDirName"`
+ // 交易类型 1-金币充值(购买金币)2-购买会员赠送金币 3-历史遗留 4-视频解锁 5-管理员手动调整
+ TranType cointransmod.TranType `json:"tranType"`
+ // 交易类型名称
+ TranTypeName string `json:"tranTypeName"`
+ // 本次交易金币数,始终为正数
+ Amount int64 `json:"amount"`
+ // 带方向的金币变动数,收入为正数,支出为负数
+ ChangeAmount int64 `json:"changeAmount"`
+ // 交易前余额
+ BalanceBefore int64 `json:"balanceBefore"`
+ // 交易后余额
+ BalanceAfter int64 `json:"balanceAfter"`
+ // 关联订单号
+ OrderNo string `json:"orderNo"`
+ // 关联内容ID
+ RelatedId int64 `json:"relatedId"`
+ // 备注
+ Remark string `json:"remark"`
+ // 交易时间,格式:2006-01-02 15:04:05
+ CreatedAt string `json:"createdAt"`
+}
diff --git a/app/api/proto/comment.go b/app/api/proto/comment.go
new file mode 100644
index 0000000..3fc92b2
--- /dev/null
+++ b/app/api/proto/comment.go
@@ -0,0 +1,65 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// GetCommentListReq 评论列表请求
+type GetCommentListReq struct {
+ ObjectId int64 `form:"objectId" json:"objectId" binding:"required"` // 对象ID(短剧id 短剧剧集id)
+ ObjectType int64 `form:"objectType" json:"objectType" binding:"required"` // 对象类型 10-短剧 11-短剧剧集
+ SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1-热门,按点赞数倒序 2-最新,按创建时间倒序
+ pubdef.Page
+}
+
+// GetCommentListResp 评论列表响应
+type GetCommentListResp struct {
+ ObjectId int64 `json:"objectId"` // 对象ID
+ TotalCount int64 `json:"totalCount"` // 总数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []CommentInfo `json:"list"` // 数据
+}
+
+// CommentSendReq 评论发布请求
+type CommentSendReq struct {
+ ObjectId int64 `json:"objectId"` // 对象ID,短剧评论传短剧ID,剧集评论传剧集ID
+ ObjectType int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集
+ ParentsId int64 `json:"parentsId"` // 一级评论ID,0-发布一级评论,大于0-发布二级评论
+ ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论ID,0时默认回复parentsId
+ Text string `json:"text"` // 评论内容
+}
+
+// CommentLikeSetReq 评论点赞设置请求
+type CommentLikeSetReq struct {
+ CommentId int64 `json:"commentId" binding:"required"` // 评论ID
+ IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
+}
+
+// CommentLikeSetResp 评论点赞设置响应
+type CommentLikeSetResp struct {
+ CommentId int64 `json:"commentId"` // 评论ID
+ IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
+ CountLike int64 `json:"countLike"` // 评论点赞数
+}
+
+// CommentInfo 评论信息
+type CommentInfo struct {
+ Id int64 `json:"id"` // 评论ID
+ ObjectId int64 `json:"objectId"` // 对象ID
+ ObjectType int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集
+ UserId int64 `json:"userId"` // 评论用户ID
+ UserName string `json:"userName"` // 用户名称
+ UserAvatar string `json:"userAvatar"` // 用户头像,已拼接图片域名
+ Text string `json:"text"` // 评论内容
+ Level int64 `json:"level"` // 评论级别 1-一级评论 2-二级评论
+ ParentsId int64 `json:"parentsId"` // 父评论ID,一级评论为0
+ ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论ID,一级评论为0
+ ReplyUserName string `json:"replyUserName"` // 被回复用户昵称
+ CommentCount int64 `json:"commentCount"` // 二级评论数量
+ CountLike int64 `json:"countLike"` // 评论点赞数
+ IsLike int64 `json:"isLike"` // 当前用户点赞状态 1-已点赞 2-未点赞
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ Children []CommentInfo `json:"children"` // 二级评论列表
+}
diff --git a/app/api/proto/config.go b/app/api/proto/config.go
new file mode 100644
index 0000000..caa2490
--- /dev/null
+++ b/app/api/proto/config.go
@@ -0,0 +1,125 @@
+package proto
+
+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下载地址(落地页)
+ OfficialAppAddr string `json:"officialAppAddr" bson:"officialAppAddr"` //官网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"` // 所有使用中的域名
+ AdvitiesTopicNum int64 `json:"advitiesTopicNum" bson:"advitiesTopicNum"` // 广告间隔
+ 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网站名称
+ ActressBackGround string `json:"actressBackGround" bson:"actressBackGround"` // 女优通用背景图
+ ReportSDKConfig string `json:"reportSdkConfig"` // 数据上报SDK域名配置
+ WatermarkImage string `json:"watermarkImage"` // 图片水印文件
+ CustomerServiceHost string `json:"customerServiceHost"` // 在线客服
+ ChannelCode string `json:"channelCode"` // 渠道码
+ EnableH265 bool `json:"enableH265"` // 是否允许客户端使用 H265 地址播放
+}
+
+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"` // 链接
+}
+
+// ConfigInfoResp APP配置响应
+type ConfigInfoResp struct {
+ // 最新地址信息
+ NewAddressInfo NewAddressInfo `json:"newAddressInfo"`
+ // 完整系统配置(与首页等接口的 config 一致,含 potaoGroup 等其余字段)
+ Config ConfigInfo `json:"config"`
+ // 广告配置
+ Advitise AdvitiseData `json:"advitise"`
+ // 全站SEO信息
+ SeoInfo SeoInfo `json:"seoInfo"`
+ // 导航SEO信息
+ SeoNavInfo SeoNavInfo `json:"seoNavInfo"`
+ // 导航页线路集合
+ NavPageList []NavPageItem `json:"navPageList"`
+ // 数据上报SDK域名配置
+ ReportSDKConfig string `json:"reportSdkConfig"`
+}
+
+// SeoNavInfo 导航SEO信息
+type SeoNavInfo struct {
+ DHTitle string `json:"dhTitle"` // 导航标题
+ DHKeyworld []string `json:"dhKeyworld"` // 关键词
+ DHDescription string `json:"dhDescription"` // 导航描述
+ DHSiteName string `json:"dhSiteName"` // 导航名称
+}
+
+// NavPageItem 导航页线路
+type NavPageItem struct {
+ Line int64 `json:"line"` // 线路序号
+ Url string `json:"url"` // 线路地址
+}
+
+// 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"` // 联系方式值
+}
+
+// NewAddressInfo 最新地址
+type NewAddressInfo struct {
+ NewUrlAddr string `json:"newUrlAddr"` // 新国内地址
+ NewUrlAddr1 string `json:"newUrlAddr1"` // 备用地址1
+ NewUrlAddr2 string `json:"newUrlAddr2"` // 备用地址1
+ ForeverUrlAddr string `json:"foreverUrlAddr"` // 永久海外地址(翻墙访问 永不迷路)
+ GmailAddr string `json:"gmailAddr"` // 邮箱地址
+ TgGroup string `json:"tgGroup"` // tg群
+ Telegram string `json:"telegram" bson:"telegram"` // 商务tg
+ CustomerServiceHost string `json:"customerServiceHost"` // 在线客服
+ TwitterAddr string `json:"twitterAddr"` // 推特地址
+ PdfAddr string `json:"pdfAddr"` // 回家的路PDF
+ AppAddr string `json:"appAddr"` // APP下载地址(落地页)
+ OfficialAppAddr string `json:"officialAppAddr"` // 官网app地址
+}
diff --git a/app/api/proto/contactus.go b/app/api/proto/contactus.go
new file mode 100644
index 0000000..16b7757
--- /dev/null
+++ b/app/api/proto/contactus.go
@@ -0,0 +1,21 @@
+package proto
+
+// ContactUsAddReq 联系我们请求
+type ContactUsAddReq struct {
+ // 邮箱
+ Email string `json:"email" binding:"required"`
+ // 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ Type int32 `json:"type" binding:"required"`
+ // 标题
+ Title string `json:"title" binding:"required"`
+ // 内容
+ Content string `json:"content" binding:"required"`
+ // 附件地址,可存放多张图片或文件地址
+ Attachments []string `json:"attachments"`
+}
+
+// ContactUsAddResp 联系我们响应
+type ContactUsAddResp struct {
+ // 提示信息
+ Message string `json:"message"`
+}
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..b34cbfb
--- /dev/null
+++ b/app/api/proto/dh.go
@@ -0,0 +1,37 @@
+package proto
+
+// 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"` // 配置文件
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+}
+
+// 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"` // 配置文件
+ 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"` // 配置文件
+ RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
+}
diff --git a/app/api/proto/feedback.go b/app/api/proto/feedback.go
new file mode 100644
index 0000000..94b596f
--- /dev/null
+++ b/app/api/proto/feedback.go
@@ -0,0 +1,19 @@
+package proto
+
+// FeedbackReq 意见反馈请求
+type FeedbackReq struct {
+ // 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
+ Type int32 `json:"type" binding:"required"`
+ // 描述内容,最多500字
+ Content string `json:"content" binding:"required"`
+ // 问题截图,可存放多张图片地址
+ Screenshots []string `json:"screenshots"`
+ // 联系方式,可为空
+ Contact string `json:"contact"`
+}
+
+// FeedbackResp 意见反馈响应
+type FeedbackResp struct {
+ // 提示信息
+ Message string `json:"message"`
+}
diff --git a/app/api/proto/findVideo.go b/app/api/proto/findVideo.go
new file mode 100644
index 0000000..2fbf1d1
--- /dev/null
+++ b/app/api/proto/findVideo.go
@@ -0,0 +1,5 @@
+package proto
+
+type FindVideoData struct {
+ RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
+}
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/lawandtrust.go b/app/api/proto/lawandtrust.go
new file mode 100644
index 0000000..a220011
--- /dev/null
+++ b/app/api/proto/lawandtrust.go
@@ -0,0 +1,12 @@
+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"` // 广告
+ 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..d0c3f21
--- /dev/null
+++ b/app/api/proto/playlet.go
@@ -0,0 +1,528 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// HomePlayletResponse 首页响应
+type HomePlayletResponse struct {
+ HomeBannerData []*HomeBannerData `json:"homeBannerData"` // 首页banner信息列表
+ HomeTodayUpdateData []*CommonData `json:"homeTodayUpdateData"` // 最近更新集合,最多16条
+ HomeCategoryData []*HomeCategoryData `json:"homeCategoryData"` // 启用分类短剧集合,按分类排序,每个分类最多16条
+ HomeRecommendThemes []*HomeRecommendTheme `json:"homeRecommendThemes"` // 推荐主题,最多4条
+ Announcements []*AnnouncementItem `json:"announcements"` // 启用中的公告,来自独立缓存
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
+}
+
+// HomeRecommendTheme 首页推荐主题
+type HomeRecommendTheme struct {
+ Id int64 `json:"id"` // 题材ID
+ Name string `json:"name"` // 题材名称
+ Slug string `json:"slug"` // slug
+ Icon string `json:"icon"` // 题材图标,已拼接图片域名
+ Rank int64 `json:"rank"` // 排序,越大越靠前
+ PlayletCount int64 `json:"playletCount"` // 该题材绑定的上架短剧数量
+}
+
+// AnnouncementItem 首页公告
+type AnnouncementItem struct {
+ Id int64 `json:"id"` // 公告ID
+ Title string `json:"title"` // 标题
+ Content string `json:"content"` // 内容
+}
+
+// HomeCategoryData 首页分类短剧集合数据
+type HomeCategoryData struct {
+ Id int64 `json:"id"` // 分类ID
+ Name string `json:"name"` // 分类名称
+ Slug string `json:"slug"` // slug
+ Desc string `json:"desc"` // 描述
+ Icon string `json:"icon"` // 分类图标
+ Rank int64 `json:"rank"` // 分类排序,越大越靠前
+ List []*CommonData `json:"list"` // 分类下短剧集合,最多16条
+}
+
+// PlayletRecommendListReq 分类短剧分页请求
+type PlayletRecommendListReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+ CategoryId int64 `form:"categoryId" json:"categoryId"` // 分类ID
+ CategorySlug string `form:"categorySlug" json:"categorySlug"` // 分类slug,非空时优先按slug解析分类ID
+}
+
+// PlayletRecommendListResp 分类短剧分页响应
+type PlayletRecommendListResp struct {
+ CategoryId int64 `json:"categoryId"` // 分类ID
+ CategoryName string `json:"categoryName"` // 分类名称
+ CategorySlug string `json:"categorySlug"` // 分类slug
+ CategoryDesc string `json:"categoryDesc"` // 分类描述
+ TotalCount int64 `json:"totalCount"` // 总共数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []CommonData `json:"list"` // 短剧集合
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(分类SEO)
+}
+
+// HomeBannerData 首页banner信息
+type HomeBannerData 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-停用
+}
+
+// CommonData 首页通用数据
+type CommonData struct {
+ Id int64 `json:"id"` // 短剧id
+ Status int32 `json:"status"` // 状态: 0下架 1上架
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧(true-是,false-否)
+ IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐,false-不推荐
+ IsNew bool `json:"isNew"` // 是否上新,本周内上架为true
+ OperateType int32 `json:"operateType"` // 运营类型 1-短剧 2-漫剧
+ ShowOnBanner bool `json:"showOnBanner"` // 是否显示在首页banner
+ Banner string `json:"banner"` // banner图,已拼接图片域名
+ OnlineAt time.Time `json:"onlineAt"` // 上架时间
+ MediaCreatedAt time.Time `json:"mediaCreatedAt"` // 媒资中心创建时间
+ MediaUpdatedAt time.Time `json:"mediaUpdatedAt"` // 媒资中心更新时间
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合
+ ThemeNames []string `json:"themeNames"` // 题材名称集合
+ ThemeSlugs []string `json:"themeSlugs"` // 题材slug集合
+ Title string `json:"title"` // 标题
+ PlayName string `json:"playName"` // 剧名
+ Description string `json:"description"` // SEO Description
+ Slug string `json:"slug"` // slug
+ Desc string `json:"desc"` // 简介
+ Anchor string `json:"anchor"` // 作者/演员
+ Cover string `json:"cover"` // 封面,已拼接图片域名
+ TagIds []int64 `json:"tagIds"` // 标签ID集合
+ TagNames []string `json:"tagNames"` // 标签名称集合
+ TagSlugs []string `json:"tagSlugs"` // 标签slug集合
+ 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"` // 点赞数,包含真实点赞数和后台虚拟点赞数
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 修改时间
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletVideoReq 短剧请求
+type PlayletVideoReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+ ThemeId int64 `form:"themeId" json:"themeId"` // 题材ID
+ ThemeSlug string `form:"themeSlug" json:"themeSlug"` // 题材slug,非空时优先按slug解析题材ID
+ SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1-热度优先,按收藏量倒序 2-今日更新,按上架时间倒序
+}
+
+// PlayletVideoResp 短剧响应
+type PlayletVideoResp struct {
+ // 总共数量
+ TotalCount int64 `json:"totalCount"`
+ // 每页数
+ PageSize int `json:"pageSize"`
+ // 页数
+ PageNum int `json:"pageNum"`
+ // 短剧集合
+ List []CommonData `json:"list"`
+ // 页面SEO信息:有 themeId 时返回题材 SEO,否则返回短剧列表页 SEO
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
+}
+
+// PlayletRankReq 短剧排行榜请求
+type PlayletRankReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+ SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1-热播榜,按热度值倒序 2-新剧榜,按上架时间倒序 3-追剧榜,按收藏数倒序
+}
+
+// PlayletRankResp 短剧排行榜响应
+type PlayletRankResp struct {
+ TotalCount int64 `json:"totalCount"` // 总共数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []CommonData `json:"list"` // 短剧集合
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
+}
+
+// PlayletOperateListReq 按运营类型分页查询短剧请求
+type PlayletOperateListReq struct {
+ pubdef.Page
+ OperateType int32 `form:"operateType" json:"operateType"` // 运营类型 1-短剧 2-漫剧
+ FilterType int64 `form:"filterType" json:"filterType"` // 筛选类型 1-最近更新 2-当前热播 3-独家原创
+}
+
+// PlayletOperateListResp 按运营类型分页查询短剧响应
+type PlayletOperateListResp struct {
+ TotalCount int64 `json:"totalCount"` // 总共数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ OperateType int32 `json:"operateType"` // 运营类型 1-短剧 2-漫剧
+ FilterType int64 `json:"filterType"` // 筛选类型 1-最近更新 2-当前热播 3-独家原创
+ List []CommonData `json:"list"` // 短剧集合
+}
+
+// PlayletTodayUpdateReq 今日更新短剧请求
+type PlayletTodayUpdateReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+}
+
+// PlayletTodayUpdateResp 今日更新短剧响应
+type PlayletTodayUpdateResp struct {
+ TotalCount int64 `json:"totalCount"` // 总共数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []CommonData `json:"list"` // 短剧集合
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
+}
+
+// PlayletTagVideoReq 根据标签ID分页查询短剧请求
+type PlayletTagVideoReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+ TagId int64 `form:"tagId" json:"tagId"` // 标签ID
+ TagSlug string `form:"tagSlug" json:"tagSlug"` // 标签slug,非空时优先按slug解析标签ID
+}
+
+// PlayletTagVideoResp 根据标签ID分页查询短剧响应
+type PlayletTagVideoResp struct {
+ TotalCount int64 `json:"totalCount"` // 总共数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []CommonData `json:"list"` // 短剧集合
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(标签SEO)
+}
+
+// PlayletSearchReq 短剧ES分页搜索请求
+type PlayletSearchReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+ Keyword string `form:"keyword" json:"keyword"` // 搜索关键词,支持剧名、题材名、简介和标签名
+}
+
+// PlayletSearchResp 短剧ES分页搜索响应
+type PlayletSearchResp struct {
+ TotalCount int64 `json:"totalCount"` // 总共数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []CommonData `json:"list"` // 短剧集合
+}
+
+// PlayletHotSearchItem 热搜词
+type PlayletHotSearchItem struct {
+ Keyword string `json:"keyword"` // 搜索词
+ Count int64 `json:"count"` // 搜索次数
+}
+
+// PlayletHotLikeResp 热搜词和猜你喜欢
+type PlayletHotLikeResp struct {
+ HotSearch []PlayletHotSearchItem `json:"hotSearch"` // 前10个热搜词
+ GuessLike []*CommonData `json:"guessLike"` // 按收藏量最多的15部上架短剧
+}
+
+// PlayletLikeSetReq 短剧点赞设置请求
+type PlayletLikeSetReq struct {
+ Id int64 `json:"id" binding:"required"` // 短剧ID
+ IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
+}
+
+// PlayletLikeSetResp 短剧点赞设置响应
+type PlayletLikeSetResp struct {
+ Id int64 `json:"id"` // 短剧ID
+ IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
+ CountLike int64 `json:"countLike"` // 短剧点赞数
+}
+
+// PlayletCollectSetReq 短剧收藏设置请求
+type PlayletCollectSetReq struct {
+ Id int64 `json:"id" binding:"required"` // 短剧ID
+ IsCollect *bool `json:"isCollect" binding:"required"` // 是否收藏,true-收藏,false-取消收藏
+}
+
+// PlayletCollectSetResp 短剧收藏设置响应
+type PlayletCollectSetResp struct {
+ Id int64 `json:"id"` // 短剧ID
+ IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
+ CountCollect int64 `json:"countCollect"` // 短剧收藏数
+}
+
+// PlayletForwardHeatReq 短剧转发加热度请求
+type PlayletForwardHeatReq struct {
+ Id int64 `json:"id" binding:"required"` // 短剧ID
+}
+
+// PlayletForwardHeatResp 短剧转发加热度响应
+type PlayletForwardHeatResp struct {
+ Id int64 `json:"id"` // 短剧ID
+ HeatValue int64 `json:"heatValue"` // 加热后的热度值
+}
+
+// PlayletCollectListReq 我的收藏列表请求
+type PlayletCollectListReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+}
+
+// PlayletCollectData 我的收藏列表项
+type PlayletCollectData struct {
+ Id int64 `json:"id"` // 短剧ID
+ Title string `json:"title"` // 标题
+ PlayName string `json:"playName"` // 剧名
+ Slug string `json:"slug"` // slug
+ Cover string `json:"cover"` // 封面,已拼接图片域名
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合
+ ThemeNames []string `json:"themeNames"` // 题材名称集合
+ ThemeSlugs []string `json:"themeSlugs"` // 题材slug集合
+ TagIds []int64 `json:"tagIds"` // 标签ID集合
+ TagNames []string `json:"tagNames"` // 标签名称集合
+ TagSlugs []string `json:"tagSlugs"` // 标签slug集合
+ TotalEpisode int64 `json:"totalEpisode"` // 总集数
+ UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
+ CountBrowse int64 `json:"countBrowse"` // 观看量
+ HeatValue int64 `json:"heatValue"` // 热度值
+ CountCollect int64 `json:"countCollect"` // 收藏数
+ IsCollect int64 `json:"isCollect"` // 收藏状态,收藏列表固定返回1-已收藏
+ CollectedAt time.Time `json:"collectedAt"` // 收藏时间
+ WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
+ UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数,按totalEpisode-watchedCount计算
+ LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID,无观看进度时为0
+ LastEpisode int64 `json:"lastEpisode"` // 最近观看集数,无观看进度时为0
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletCollectListResp 我的收藏列表响应
+type PlayletCollectListResp struct {
+ TotalCount int64 `json:"totalCount"` // 总收藏数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []PlayletCollectData `json:"list"` // 收藏短剧集合
+}
+
+// PlayletHistoryListReq 观看历史列表请求
+type PlayletHistoryListReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认20、最大100
+ pubdef.Page
+}
+
+// PlayletHistoryData 观看历史列表项
+type PlayletHistoryData struct {
+ CommonData
+ LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID
+ LastEpisode int64 `json:"lastEpisode"` // 最近观看集数
+ WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
+ UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数
+ IsFinished bool `json:"isFinished"` // 是否已看完
+ HistoryAt time.Time `json:"historyAt"` // 最近观看时间
+}
+
+// PlayletHistoryListResp 观看历史列表响应
+type PlayletHistoryListResp struct {
+ TotalCount int64 `json:"totalCount"` // 总历史数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []PlayletHistoryData `json:"list"` // 观看历史集合
+}
+
+// PlayletHistoryClearResp 清空观看历史响应
+type PlayletHistoryClearResp struct {
+ DeletedCount int64 `json:"deletedCount"` // 删除记录数量
+}
+
+// PlayletChapterLikeSetReq 短剧剧集点赞设置请求
+type PlayletChapterLikeSetReq struct {
+ ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
+ IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
+}
+
+// PlayletChapterLikeSetResp 短剧剧集点赞设置响应
+type PlayletChapterLikeSetResp struct {
+ ChapterId int64 `json:"chapterId"` // 短剧剧集ID
+ IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
+ CountLike int64 `json:"countLike"` // 短剧剧集点赞数
+}
+
+// PlayletChapterCollectSetReq 短剧剧集收藏设置请求
+type PlayletChapterCollectSetReq struct {
+ ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
+ IsCollect *bool `json:"isCollect" binding:"required"` // 是否收藏,true-收藏,false-取消收藏
+}
+
+// PlayletChapterCollectSetResp 短剧剧集收藏设置响应
+type PlayletChapterCollectSetResp struct {
+ ChapterId int64 `json:"chapterId"` // 短剧剧集ID
+ IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
+ CountCollect int64 `json:"countCollect"` // 短剧剧集收藏数
+}
+
+// PlayletChapterCollectListReq 我的剧集收藏列表请求
+type PlayletChapterCollectListReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+}
+
+// PlayletChapterCollectData 我的剧集收藏列表项
+type PlayletChapterCollectData struct {
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ PlayletTitle string `json:"playletTitle"` // 短剧标题
+ PlayletCover string `json:"playletCover"` // 短剧封面,已拼接图片域名
+ TotalEpisode int64 `json:"totalEpisode"` // 短剧总集数
+ UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
+ ChapterId int64 `json:"chapterId"` // 短剧剧集ID
+ CurrentEpisode int64 `json:"currentEpisode"` // 第几集
+ ChapterTitle string `json:"chapterTitle"` // 剧集标题
+ VideoUrl string `json:"videoUrl"` // 播放地址
+ MediaTime int64 `json:"mediaTime"` // 影片长度(秒)
+ PayType int32 `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币
+ CountBrowse int64 `json:"countBrowse"` // 剧集观看数
+ CountCollect int64 `json:"countCollect"` // 剧集收藏数
+ IsCollect int64 `json:"isCollect"` // 收藏状态,收藏列表固定返回1-已收藏
+ CollectedAt time.Time `json:"collectedAt"` // 收藏时间
+}
+
+// PlayletChapterCollectListResp 我的剧集收藏列表响应
+type PlayletChapterCollectListResp struct {
+ TotalCount int64 `json:"totalCount"` // 总收藏数量
+ PageSize int `json:"pageSize"` // 每页数
+ PageNum int `json:"pageNum"` // 页数
+ List []PlayletChapterCollectData `json:"list"` // 收藏剧集集合
+}
+
+// PlayletChapterWatchReportReq 短剧剧集观看上报请求
+type PlayletChapterWatchReportReq struct {
+ PlayletId int64 `json:"playletId"` // 短剧ID,传入时用于校验剧集归属
+ ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
+}
+
+// PlayletChapterWatchReportResp 短剧剧集观看上报响应
+type PlayletChapterWatchReportResp struct {
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ ChapterId int64 `json:"chapterId"` // 短剧剧集ID
+ CurrentEpisode int64 `json:"currentEpisode"` // 当前集数
+ WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
+ UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数
+ LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID
+ LastEpisode int64 `json:"lastEpisode"` // 最近观看集数
+}
+
+// PlayletInfoReq 详情请求
+type PlayletInfoReq struct {
+ Id int64 `form:"id" json:"id"` // 短剧id
+ Slug string `form:"slug" json:"slug"` // 短剧slug,非空时优先按slug解析短剧ID
+}
+
+// PlayletChapterInfoReq 短剧剧集详情请求
+type PlayletChapterInfoReq struct {
+ PlayletId int64 `form:"playletId" json:"playletId"` // 短剧ID,传入时用于校验剧集归属
+ PlayletSlug string `form:"playletSlug" json:"playletSlug"` // 短剧slug,非空时优先按slug解析短剧ID
+ ChapterId int64 `form:"chapterId" json:"chapterId" binding:"required"` // 短剧剧集ID
+}
+
+// PlayletChapterListReq 短剧剧集列表请求
+type PlayletChapterListReq struct {
+ PlayletId int64 `form:"playletId" json:"playletId"` // 短剧ID
+ PlayletSlug string `form:"playletSlug" json:"playletSlug"` // 短剧slug,非空时优先按slug解析短剧ID
+}
+
+// PlayletChapterListResp 短剧剧集列表响应
+type PlayletChapterListResp struct {
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ List []PlayletChapterData `json:"list"` // 短剧剧集集合
+}
+
+// PlayletChapterInfoResp 短剧剧集详情响应
+type PlayletChapterInfoResp struct {
+ Id int64 `json:"id"` // 短剧剧集ID
+ PlayletId int64 `json:"playletId"` // 短剧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"` // 影片长度(秒)
+ CountComment int64 `json:"countComment"` // 短剧剧集评论数
+ CountBrowse int64 `json:"countBrowse"` // 短剧剧集观看数
+ CountCollect int64 `json:"countCollect"` // 短剧剧集收藏数
+ CountLike int64 `json:"countLike"` // 短剧剧集点赞数
+ IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
+ IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
+ PayType int32 `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ CanWatch bool `json:"canWatch"` // 是否可观看
+ NeedUnlock bool `json:"needUnlock"` // 是否需要金币解锁
+ NeedVip bool `json:"needVip"` // 是否需要开通VIP
+ CostCoins int64 `json:"costCoins"` // 本次解锁消耗金币
+ Balance int64 `json:"balance"` // 当前金币余额
+ VipValid bool `json:"vipValid"` // 当前用户VIP是否有效
+ IsUnlocked bool `json:"isUnlocked"` // 当前用户是否已解锁本集
+ // 页面SEO信息
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
+}
+
+// PlayletChapterUnlockReq 短剧剧集金币解锁请求
+type PlayletChapterUnlockReq struct {
+ PlayletId int64 `json:"playletId" binding:"required"` // 短剧ID
+ ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
+}
+
+// PlayletChapterUnlockResp 短剧剧集金币解锁响应
+type PlayletChapterUnlockResp struct {
+ PlayletId int64 `json:"playletId"` // 短剧ID
+ ChapterId int64 `json:"chapterId"` // 短剧剧集ID
+ Unlocked bool `json:"unlocked"` // 是否已解锁
+ CanWatch bool `json:"canWatch"` // 是否可观看
+ Balance int64 `json:"balance"` // 当前金币余额
+ CostCoins int64 `json:"costCoins"` // 本次解锁消耗金币
+ ChangeCoin int64 `json:"changeCoin"` // 本次实际扣减金币,幂等命中时为0
+}
+
+// PlayletInfoResp 详情响应
+type PlayletInfoResp struct {
+ CommonData
+ // 是否点赞(1-已点赞,2-未点赞)
+ IsLike int64 `json:"isLike"`
+ // 是否收藏(1-已收藏,2-未收藏)
+ IsCollect int64 `json:"isCollect"`
+ // 相关推荐集合
+ RelatedRecommendData []*CommonData `json:"relatedRecommendData"`
+ // 短剧章节集合
+ PlayletChapterInfo []PlayletChapterData `json:"playletChapterInfo"`
+ // 页面SEO信息
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
+}
+
+// PlayletChapterData 短剧章节信息
+type PlayletChapterData struct {
+ Id int64 `json:"id"` // 短剧剧集ID
+ PlayletId int64 `json:"playletId"` // 短剧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 int32 `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ CanWatch bool `json:"canWatch"` // 是否可观看
+ NeedUnlock bool `json:"needUnlock"` // 是否需要金币解锁
+ NeedVip bool `json:"needVip"` // 是否需要开通VIP
+ CostCoins int64 `json:"costCoins"` // 本次解锁消耗金币
+ Balance int64 `json:"balance"` // 当前金币余额
+ VipValid bool `json:"vipValid"` // 当前用户VIP是否有效
+ IsUnlocked bool `json:"isUnlocked"` // 当前用户是否已解锁本集
+}
diff --git a/app/api/proto/playlet_theme.go b/app/api/proto/playlet_theme.go
new file mode 100644
index 0000000..a5b9f69
--- /dev/null
+++ b/app/api/proto/playlet_theme.go
@@ -0,0 +1,19 @@
+package proto
+
+// PlayletThemeListResponse app端题材列表响应
+type PlayletThemeListResponse struct {
+ List []*PlayletThemeItem `json:"list"` // 题材列表
+}
+
+// PlayletThemeItem app端题材列表项
+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"` // 排序,越大越靠前
+ Status bool `json:"status"` // 上架状态
+ IsRecommend bool `json:"isRecommend"` // 是否推荐
+ PlayletCount int64 `json:"playletCount"` // 该题材绑定的上架短剧数量
+ SeoInfo *SeoInfo `json:"seoInfo"` // 题材对应页面 SEO 信息
+}
diff --git a/app/api/proto/recharge_package.go b/app/api/proto/recharge_package.go
new file mode 100644
index 0000000..df20d81
--- /dev/null
+++ b/app/api/proto/recharge_package.go
@@ -0,0 +1,32 @@
+package proto
+
+import "videoweb/pub/models/rechargepackagemod"
+
+// PayMethodInfo 单个支付方式
+type PayMethodInfo struct {
+ Type string `json:"type"` // alipay / wechat / union
+ TypeName string `json:"typeName"` // 支付宝 / 微信 / 银联
+}
+
+// RechargePackageItem App购买套餐列表项
+type RechargePackageItem struct {
+ Id int64 `json:"id"` // 套餐ID
+ PackageType rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
+ Name string `json:"name"` // 展示标题
+ SubTitle string `json:"subTitle"` // 展示副标题
+ Price int64 `json:"price"` // 售价,单位分
+ OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
+ DurationDays int64 `json:"durationDays"` // 会员有效天数
+ DurationHours int64 `json:"durationHours"` // 会员有效小时数,由durationDays计算
+ BaseCoin int64 `json:"baseCoin"` // 基础金币数
+ GiftCoin int64 `json:"giftCoin"` // 赠送金币数
+ TotalCoin int64 `json:"totalCoin"` // 到账总金币数,由baseCoin+giftCoin计算
+ Sort int32 `json:"sort"` // 排序,越小越靠前
+ PayMethods []PayMethodInfo `json:"payMethods"` // 该套餐支持的支付方式
+}
+
+// RechargePackageListResp App购买套餐列表响应。
+type RechargePackageListResp struct {
+ VipList []*RechargePackageItem `json:"vipList"` // 会员畅享列表
+ CoinList []*RechargePackageItem `json:"coinList"` // 金币充值列表
+}
diff --git a/app/api/proto/richtext.go b/app/api/proto/richtext.go
new file mode 100644
index 0000000..3ce0fc6
--- /dev/null
+++ b/app/api/proto/richtext.go
@@ -0,0 +1,29 @@
+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
+ Id int64 `json:"id"`
+ // 标题
+ Title string `json:"title"`
+ // 内容
+ Content string `json:"content"`
+ // 标题(app)
+ Desc string `json:"desc"`
+}
+
+// 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..07b1c68
--- /dev/null
+++ b/app/api/proto/search.go
@@ -0,0 +1,85 @@
+package proto
+
+import "time"
+
+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"` // 广告
+ 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"` // 热度值
+ Images string `json:"images" form:"images"` // 封面
+ Works int64 `json:"works" bson:"works,omitempty"` // 作品数量
+ Desc string `json:"desc" bson:"desc"` // 描述
+ AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 上架shi时间
+}
+
+type SearchResp struct {
+ Path string `json:"path"`
+ CompletePath string `json:"completePath" form:"completePath"`
+ InTitle string `json:"inTitle"` //输入标题
+ InType int64 `json:"inType"` // 输入类型 0=视频 1=女优 2=影评 3=帖子
+ 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"` // 搜索推荐
+ Config ConfigInfo `json:"config" form:"config"` // 配置文件
+}
+
+type SearchSortItem struct {
+ Key string `json:"key"` // 值
+ Title string `json:"title"` // 标题
+}
diff --git a/app/api/proto/seo.go b/app/api/proto/seo.go
new file mode 100644
index 0000000..3329e99
--- /dev/null
+++ b/app/api/proto/seo.go
@@ -0,0 +1,15 @@
+package proto
+
+// SeoInfo 页面SEO信息,用于前端渲染 title、meta 和 canonical。
+type SeoInfo struct {
+ // 页面标题
+ Title string `json:"title"`
+ // 页面关键词
+ Keywords []string `json:"keywords"`
+ // 页面描述
+ Description string `json:"description"`
+ // 网站名称
+ WebSiteName string `json:"webSiteName"`
+ // 当前内容的标准URL
+ CanonicalURL string `json:"canonicalUrl"`
+}
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..1117adf
--- /dev/null
+++ b/app/api/proto/translation.go
@@ -0,0 +1,24 @@
+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"` // 导航类型
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+type TranslationInfo struct {
+ Name string `json:"name" bson:"name"` // 名称
+ IsFriendAds bool `json:"isFriendAds"` // 是否是友链广告位
+ UrlValue string `json:"urlValue" bson:"urlValue"` // URl路径
+ TransCategoryId int64 `json:"-"` // 导航分类
+}
diff --git a/app/api/proto/video.go b/app/api/proto/video.go
new file mode 100644
index 0000000..486c048
--- /dev/null
+++ b/app/api/proto/video.go
@@ -0,0 +1,265 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+type TagsInfoData struct {
+ Id int64 `json:"id" `
+ Name string `json:"name"` // 帖子标题
+ 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 // 广告
+ VideoTypeVideoV2 VideoType = 3 // 视频
+)
+
+type VideoInfoData struct {
+ Id int64 `json:"id"`
+ Type VideoType `json:"type"` // 类型
+ Title string `json:"title"` // 帖子标题
+ Keyworld []string `json:"keyworld"` // 关键字
+ AddedTime time.Time `json:"addedTime"` // 帖子上架时间
+ CreatorId int64 `json:"creatorId"` // 作者Id
+ CreatorName string `json:"creatorName"` // 作者名称
+ 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
+ Link string `json:"link"` // 跳转链接
+ Location int64 `json:"location" bson:"location"` // 位置id
+ CategoryIds []int64 `json:"categoryIds"` // 分类信息
+ Categorys []VideoCategoryInfo `json:"categorys"` // 分类信息
+ TopicIds []int64 `json:"topicIds"` // 专题信息
+ Topics []VideoTopicInfo `json:"topics"` // 专题信息
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ UpdatedAt time.Time `json:"updatedAt"` //更新时间
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Content string `json:"content"` // 富文本
+ ImgCdn string `json:"imgCdn" bson:"imgCdn"` // 图片CDN
+ PreviewVtt string `json:"previewVtt,omitempty"` // 雪碧图 vtt
+}
+
+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"`
+ ContentType int64 `json:"contentType" bson:"contentType"` // 内容:0: 默认 1:首页 2:今日
+ SortType int64 `json:"sortType" bson:"sortType"` // 排序方式 0:上线时间 1:观看次数 2排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Title string `json:"title" bson:"title"` // APP显示
+ Desc string `json:"desc" bson:"desc"` // 描述
+}
+
+// 面包屑
+type BreadcrumbItem struct {
+ Name string `json:"name"` // 名称
+ Url string `json:"url"` // 跳转链接
+}
+
+// 详情页导航
+type DetailNavItem struct {
+ Name string `json:"name"` // 导航名称
+ Url string `json:"url"` // 导航链接
+}
+
+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"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+}
+
+func VideoInfoToData(videoInfo *videoinformationmod.VideoInformation, imgCdn string) 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_VideoV2:
+ t = VideoTypeVideoV2
+ }
+ videotemp := VideoInfoData{
+ Id: videoInfo.Id,
+ Type: t,
+ Title: videoInfo.Title,
+ Keyworld: videoInfo.Keyworld,
+ AddedTime: videoInfo.AddedTime,
+ Desc: videoInfo.Desc,
+ UpdatedAt: videoInfo.PageUpdatedAt,
+ CreatorId: videoInfo.CreatorId,
+ TopicIds: videoInfo.TopicIds,
+ Topics: make([]VideoTopicInfo, 0),
+ CreatorName: videoInfo.CreatorName,
+ CoverImg: netutil.MergeURL(imgCdn, videoInfo.CoverImg),
+ Nodes: make([]videoinformationmod.Node, 0),
+ Video: videoInfo.Video,
+ IsHot: videoInfo.IsHot,
+ CategoryIds: videoInfo.CategoryIds,
+ Categorys: make([]VideoCategoryInfo, 0),
+ Tags: make([]TagsInfoData, 0),
+ ShowTimes: videoInfo.Real.ShowTimes,
+ WatchTimes: videoInfo.Real.WatchTimes,
+ Content: videoInfo.Content,
+ ImgCdn: imgCdn,
+ PreviewVtt: videoInfo.PreviewVtt,
+ }
+ ti := time.Date(2020, 1, 1, 0, 0, 0, 0, utils.LocBJ)
+ if videotemp.UpdatedAt.Before(ti) {
+ videotemp.UpdatedAt = videoInfo.UpdatedAt
+ }
+ for _, nodeInfo := range videoInfo.Nodes {
+ nodeTmp := videoinformationmod.Node{
+ Images: make([]string, 0),
+ Url: nodeInfo.Url,
+ Text: nodeInfo.Text,
+ Videos: nodeInfo.Videos,
+ Title: nodeInfo.Title,
+ }
+ for _, image := range nodeInfo.Images {
+ nodeTmp.Images = append(nodeTmp.Images, netutil.MergeURL(imgCdn, image))
+ }
+ videotemp.Nodes = append(videotemp.Nodes, nodeTmp)
+ }
+ return videotemp
+}
+
+// StripH265Url 清空主视频与 nodes 内视频的 H265 地址。
+func StripH265Url(data *VideoInfoData) {
+ if data == nil {
+ return
+ }
+ data.Video.H265Url = ""
+ for i := range data.Nodes {
+ for j := range data.Nodes[i].Videos {
+ data.Nodes[i].Videos[j].H265Url = ""
+ }
+ }
+}
+
+// VideoReq 普通分页查询视频请求
+type VideoReq struct {
+ // 普通分页参数,pageNum默认1,pageSize默认10、最大100
+ pubdef.Page
+}
+
+// VideoItemResp 查询视频列表项
+type VideoItemResp struct {
+ // id
+ Id int64 `json:"id"`
+ // 标题
+ Title string `json:"title"`
+ // 上架时间
+ AddedTime time.Time `json:"addedTime"`
+ // 作者Id
+ CreatorId int64 `json:"creatorId"`
+ // 作者名称
+ CreatorName string `json:"creatorName"`
+ // 封面
+ CoverImg string `json:"coverImg"`
+ // 关键字
+ Keyworld []string `json:"keyworld"`
+ // 标签
+ Tags []int64 `json:"tags"`
+ // 观看数(真假之和)
+ WatchTimes int64 `json:"watchTimes"`
+ // 影片长度
+ VideoTime int32 `json:"videoTime"`
+}
+
+// VideoResp 普通分页查询视频响应
+type VideoResp struct {
+ // 总共数量
+ TotalCount int64 `json:"totalCount"`
+ // 每页数
+ PageSize int `json:"pageSize"`
+ // 页数
+ PageNum int `json:"pageNum"`
+ // 视频列表
+ List []*VideoItemResp `json:"list"`
+}
+
+// VideoInfoReq 查询视频详情请求
+type VideoInfoReq struct {
+ // 视频的id必填
+ Id int64 `json:"id" form:"id" binding:"required"`
+}
+
+// VideoInfoResp 查询视频详情响应
+type VideoInfoResp struct {
+ // id
+ Id int64 `json:"id"`
+ // 标题
+ Title string `json:"title"`
+ // 封面
+ CoverImg string `json:"coverImg"`
+ // 关键字
+ Keyworld []string `json:"keyworld"`
+ // 标签
+ Tags []int64 `json:"tags"`
+ // 标签名称
+ TagsName []string `json:"tagsName"`
+ // 观看数(真假之和)
+ WatchTimes int64 `json:"watchTimes"`
+ // 视频
+ Video VideoData `json:"video"`
+ // 相关推荐,按当前视频标签匹配并按上架时间倒序返回最多20条
+ RecommendVideos []*VideoItemResp `json:"recommendVideos"`
+ // 广告
+ Advitise AdvitiseData `json:"advitise"`
+ // 页面SEO信息
+ SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
+}
+
+type VideoData struct {
+ // 视频ID(视频才会存在)
+ TaskId string `json:"taskId"`
+ // 媒资库ID
+ FileId string `json:"fileId"`
+ // 视频URl
+ Url string `json:"url"`
+ // 视频封面
+ Cover string `json:"cover"`
+ // 分辨率高(视频才会存在)
+ Height int64 `json:"height"`
+ // 分辨率宽(视频才会存在)
+ Width int64 `json:"width"`
+ // 影片长度
+ VideoTime int32 `json:"videoTime"`
+ // 预览MP4文件
+ PreMp4FileNameFs string `json:"preMp4FileNameFs"`
+ // 不加密的预览MP4
+ UnencryptedPreMp4 string `json:"unencryptedPreMp4"`
+}
diff --git a/app/api/proto/videotopic.go b/app/api/proto/videotopic.go
new file mode 100644
index 0000000..7e565c2
--- /dev/null
+++ b/app/api/proto/videotopic.go
@@ -0,0 +1,16 @@
+package proto
+
+import (
+ "videoweb/pub/pubdef"
+)
+
+type VideoTopicInfo 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"` // 描述
+ Image string `json:"image" bson:"image"` // 背景
+ VideoCategoryId int64 `json:"videoCategoryId" bson:"videoCategoryId"` // 视频分类id
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Works int64 `json:"works" bson:"works"` // 作品数
+}
diff --git a/app/api/proto/webdata.go b/app/api/proto/webdata.go
new file mode 100644
index 0000000..d32340b
--- /dev/null
+++ b/app/api/proto/webdata.go
@@ -0,0 +1,215 @@
+package proto
+
+import (
+ "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"` // 广告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+}
+
+type VideoCategoryData struct {
+ Id int64 `json:"id" form:"id"`
+ Name string `json:"name" form:"name"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"`
+ TopicList []VideoTopicData `json:"topicList" form:"topicList"` // 专题列表
+}
+
+type VideoTopicData struct {
+ Id int64 `json:"id" form:"id"`
+ Name string `json:"name" form:"name"`
+ Image string `json:"image" form:"image"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Words int64 `json:"words" form:"words"`
+ ShowType int64 `json:"showType" bson:"showType"` // 展示类型 0=默认 1=横版左右滑动 2=竖版左右滑动
+ TopicType int64 `json:"topicType" bson:"topicType"` // 专题类型 0视频专题 1绑定专题 2绑定标签 3=绑定女优 4=随便看
+ LinkUrl string `json:"linkUrl" bson:"linkUrl"` // 跳转URL
+}
+
+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"` // 广告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ VideoCategory VideoCategoryData `json:"videoCategory" form:"videoCategory"` // 当前分类数据
+}
+
+type VideoTopicHtmlData 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"` // 广告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+ VideoInfos []VideoInfoData `json:"videoInfos" form:"videoInfos"` // 视频列表
+}
+
+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"` // 广告
+ 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"` // 广告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
+ 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"`
+ Video VideoInfoData `json:"video" form:"video"` // 视频详情
+ Breadcrumbs []BreadcrumbItem `json:"breadcrumbs" form:"breadcrumbs"` // 面包屑
+ 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"` // 广告
+ CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
+}
+
+type PostDetailsHtmlData 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"` // 广告
+ CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
+ CreateInfo PostCreateInfo `json:"createInfo" form:"createInfo"` // 作者信息
+}
+
+type PostCreateInfo struct {
+ Id int64 `json:"id" form:"id"`
+ Name string `json:"name" form:"name"`
+ 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"` // 背景
+}
+
+// TagHtmlData 首页
+type TagHtmlData 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"` // 广告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ 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"` // 广告
+ PageNum int64 `json:"pageNum" form:"pageNum"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+ TotalSize int64 `json:"totalSize" form:"totalSize"`
+}
+type VideoTagCategoryHtmlData 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"` // 广告
+ AllCategory []VideoTagCategoryData `json:"allCategory" form:"allCategory"` //所有分类
+
+}
+type VideoTagCategoryData struct {
+ Id int64 `json:"id" form:"id"`
+ Name string `json:"name" form:"name"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo" form:"seoInfo"`
+ Image string `json:"image" form:"image"`
+ TotalTags int64 `json:"totalTags" form:"totalTags"` // 总标签数量
+}
+type VideoTagCategoryInfoData struct {
+ 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"` // 广告
+ 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"` // 配置文件
+ 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"` // 配置文件
+ 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"` // 广告
+ HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
+ 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"` // 主旨分类
+}
diff --git a/app/api/rechargeapi/recharge.go b/app/api/rechargeapi/recharge.go
new file mode 100644
index 0000000..4cd47f2
--- /dev/null
+++ b/app/api/rechargeapi/recharge.go
@@ -0,0 +1,85 @@
+package rechargeapi
+
+import (
+ "videoweb/app/service/rechargeser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+type submitOrderReq struct {
+ PackageId int64 `json:"packageId" binding:"required"` // 购买套餐ID(会员畅享)
+ PayMode string `json:"payMode" binding:"required"` // 支付方式:alipay/wechat
+}
+
+// SubmitOrder 发起第三方支付下单
+// @Tags 充值
+// @Summary 创建 VIP 充值订单(第三方支付)
+// @Accept json
+// @Produce json
+// @Param param body submitOrderReq true "下单参数"
+// @Success 200 {object} rechargeser.SubmitOrderResp "支付链接"
+// @Security ApiKeyAuth
+// @Router /api/app/vip/pay [POST]
+func SubmitOrder(ctx *gin.Context) {
+ var req submitOrderReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ userId := common.GetUserId(ctx)
+ sysType := ctx.GetString(consts.CtxSysType)
+ devType := ctx.GetString(consts.CtxDevType)
+ ip := ipaddr.ResolveClientIP(ctx)
+
+ user, serr := rechargeser.GetUserInfo(ctx, userId)
+ if serr != nil || user == nil {
+ common.ServerJSON(ctx, srvdef.ErrUserNotExsit, nil)
+ return
+ }
+
+ resp, serr := rechargeser.CreateOrder(ctx, userId, user.NickName, req.PackageId, req.PayMode, sysType, devType, ip, user.DistrictCode)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+type orderHistoryReq struct {
+ Page int64 `json:"page" form:"page"`
+ PageSize int64 `json:"pageSize" form:"pageSize"`
+}
+
+// GetOrderHistory 查询当前用户 VIP 订单历史
+// @Tags 充值
+// @Summary 查询 VIP 充值记录
+// @Accept json
+// @Produce json
+// @Param page query int false "页码(默认1)"
+// @Param pageSize query int false "每页数量(默认10)"
+// @Success 200 {array} vipordermod.VipOrder "订单列表"
+// @Security ApiKeyAuth
+// @Router /api/app/vip/orders [GET]
+func GetOrderHistory(ctx *gin.Context) {
+ var req orderHistoryReq
+ _ = ctx.ShouldBindQuery(&req)
+ if req.Page <= 0 {
+ req.Page = 1
+ }
+ if req.PageSize <= 0 {
+ req.PageSize = 10
+ }
+
+ userId := common.GetUserId(ctx)
+ list, total, serr := rechargeser.GetOrderHistory(ctx, userId, req.Page, req.PageSize)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, gin.H{"total": total, "list": list})
+}
diff --git a/app/api/rechargepackageapi/package.go b/app/api/rechargepackageapi/package.go
new file mode 100644
index 0000000..c5a774f
--- /dev/null
+++ b/app/api/rechargepackageapi/package.go
@@ -0,0 +1,30 @@
+package rechargepackageapi
+
+import (
+ "videoweb/app/service/rechargepackageser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 查询购买套餐列表
+// @Tags 金币与会员
+// @Summary 查询购买套餐列表 -- by 王鸣
+// @Description 返回会员畅享和金币充值两个列表,并附带各套餐可用支付方式
+// @Produce json
+// @Success 200 {object} proto.RechargePackageListResp "成功"
+// @Router /api/app/recharge/package/list [GET]
+func List(ctx *gin.Context) {
+ sysType := ctx.GetString(consts.CtxSysType)
+ if sysType == "" {
+ sysType = "android"
+ }
+ resp, err := rechargepackageser.GetRechargePackageList(ctx, sysType)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/api/statcenterapi/sync.go b/app/api/statcenterapi/sync.go
new file mode 100644
index 0000000..005243b
--- /dev/null
+++ b/app/api/statcenterapi/sync.go
@@ -0,0 +1,437 @@
+package statcenterapi
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "net/http"
+ "strconv"
+ "videoweb/pub/common"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/statcenter"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/uservisitlog"
+ "videoweb/pub/models/vipordermod"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+ "github.com/shopspring/decimal"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// @Tags 数据同步
+// @Summary 统计中心增量同步
+// @Description 按 job 拉取注册/日活/充值/绑定/消费/售卡等增量数据
+// @Accept json
+// @Produce json
+// @Param param body statcenter.StatcenterSyncReq true "参数"
+// @Success 200 {object} statcenter.StatcenterSyncResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/statcenter/sync [POST]
+func StatcenterSyncList(ctx *gin.Context) {
+ var request statcenter.StatcenterSyncReq
+ if err := ctx.ShouldBind(&request); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ var resp statcenter.StatcenterSyncResp
+ resp.Job = request.Job
+ resp.Code = 200
+ appId := statcenter.STAT_APPID
+
+ var err error
+ switch request.Job {
+ case string(statcenter.USER_ACCE):
+ resp.AccessList, err = UserAccessList(appId, request, true)
+ case string(statcenter.USER_REG):
+ resp.RegisterList, err = UserRegisterList(appId, request, true)
+ case string(statcenter.USER_BINDING):
+ resp.BindingList, err = UserBindingList(appId, request)
+ case string(statcenter.USER_INVITE):
+ resp.InviteList, err = UserInviteList(appId, request)
+ case string(statcenter.ConsumeRecordJob):
+ resp.ConsumeList, err = ConsumeRecordList(appId, request)
+ case string(statcenter.USER_RECH):
+ resp.RechargeList, err = UserRechargeList(appId, request, true)
+ case string(statcenter.USER_RECH_ALL):
+ resp.AllRechargeList, err = UserAllRechargeList(appId, request, true)
+ case string(statcenter.CardSellJob):
+ resp.CardSellList, err = CardSellList(appId, request)
+ case string(statcenter.AI_SELL_JOB):
+ resp.AiSellList, err = AiSellList(appId, request)
+ case string(statcenter.VipJob):
+ resp.VipList, err = UserSyncVipList(appId, request)
+ case string(statcenter.RechargeAll): // 兼容旧任务名
+ resp.RechargeList, err = UserAllRechargeList(appId, request, true)
+ default:
+ resp.Code = 501
+ resp.Msg = "job is error"
+ ctx.JSON(http.StatusOK, resp)
+ return
+ }
+
+ if err != nil {
+ resp.Code = 502
+ resp.Msg = err.Error()
+ }
+ ctx.JSON(http.StatusOK, resp)
+}
+
+// UserAccessList 日活数据同步
+func UserAccessList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (accessList []statcenter.UserVisitMsg, err error) {
+ platformId := request.PlatformId
+ if platformId == "" {
+ platformId = "000000000000000000000000"
+ }
+ visitList, err := uservisitlog.AccessSyncById(context.Background(), platformId, request.MaxSize)
+ if err != nil {
+ log.Error("UserAccessList AccessSyncById", log.E(err))
+ return nil, err
+ }
+ accessList = make([]statcenter.UserVisitMsg, 0, len(visitList))
+ userIds := make([]int64, 0, len(visitList))
+ for _, visitInfo := range visitList {
+ userIds = append(userIds, visitInfo.UserId)
+ }
+ userList, _ := usermod.FindManyUser(context.Background(), bson.M{usermod.FN.User.Id: bson.M{"$in": userIds}})
+ userMap := make(map[int64]*usermod.User, len(userList))
+ for _, u := range userList {
+ userMap[u.Id] = u
+ }
+ for _, visitInfo := range visitList {
+ sType, dType := visitInfo.SysType, visitInfo.DevType
+ if appId == statcenter.STAT_APPID && isNewStat {
+ sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(visitInfo.SysType), visitInfo.DevType)
+ }
+ accessInfo := statcenter.UserVisitMsg{
+ UserId: uint64(visitInfo.UserId),
+ AppId: appId,
+ SysType: sType,
+ DevType: dType,
+ IP: visitInfo.IP,
+ Version: visitInfo.Version,
+ DevID: visitInfo.DevID,
+ VisitAt: visitInfo.VisitAt,
+ PlatformId: visitInfo.Id.Hex(),
+ }
+ if userInfo, ok := userMap[visitInfo.UserId]; ok {
+ accessInfo.IsDirect = userInfo.IsDirect
+ accessInfo.DistrictCode = userInfo.DistrictCode
+ accessInfo.RegisterTime = userInfo.CreatedAt
+ }
+ accessList = append(accessList, accessInfo)
+ }
+ return
+}
+
+// UserRegisterList 注册数据同步
+func UserRegisterList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (registerList []statcenter.UserRegisterMsg, err error) {
+ var userList usermod.UserList
+ if appId == statcenter.STAT_APPID && isNewStat {
+ if request.UserId > math.MaxInt64 {
+ return nil, fmt.Errorf("invalid register cursor userId: %d", request.UserId)
+ }
+ userList, err = usermod.StatcenterSyncListByUserID(context.Background(), int64(request.UserId), request.MaxSize)
+ } else {
+ userList, err = usermod.StatcenterSyncList(context.Background(), request.SuccessTime, request.MaxSize)
+ }
+ if err != nil {
+ log.Error("UserRegisterList", log.E(err))
+ return nil, err
+ }
+ registerList = make([]statcenter.UserRegisterMsg, 0, len(userList))
+ for _, userInfo := range userList {
+ if userInfo.Device == "robot" {
+ continue
+ }
+ isNativeIOS := pubdef.SysTypeFromString(userInfo.SysType) == pubdef.ST_IOS
+ districtCode := userInfo.DistrictCode
+ if isNativeIOS && statcenter.IOSRegistrationDistrictCode != "" {
+ districtCode = statcenter.IOSRegistrationDistrictCode
+ }
+
+ sType, dType := userInfo.SysType, userInfo.Device
+ if appId == statcenter.STAT_APPID && isNewStat {
+ sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(userInfo.SysType), userInfo.Device)
+ }
+ registerList = append(registerList, statcenter.UserRegisterMsg{
+ UserId: uint64(userInfo.Id),
+ AppId: appId,
+ SysType: sType,
+ DevType: dType,
+ Mobile: userInfo.Email,
+ Name: userInfo.NickName,
+ IP: userInfo.RegIp,
+ IsDirect: userInfo.IsDirect,
+ DistrictCode: districtCode,
+ PromSeqe: userInfo.AgentInviteCode,
+ PromCode: userInfo.InviteCode,
+ RegisterTime: userInfo.CreatedAt,
+ })
+ }
+ return
+}
+
+// UserBindingList 绑定数据同步(本项目:正式注册并绑定邮箱)
+func UserBindingList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.UserBindingMsg, error) {
+ userList, err := usermod.StatcenterSyncBindUserList(context.Background(), request.SuccessTime, request.MaxSize)
+ if err != nil {
+ log.Error("UserBindingList", log.E(err))
+ return nil, err
+ }
+ list := make([]statcenter.UserBindingMsg, 0, len(userList))
+ for _, userInfo := range userList {
+ sType, dType := pubdef.GetCenterType(pubdef.SysTypeFromString(userInfo.SysType), userInfo.Device)
+ bindAt := userInfo.UpdatedAt
+ list = append(list, statcenter.UserBindingMsg{
+ UserId: uint64(userInfo.Id),
+ AppId: appId,
+ PlatformId: fmt.Sprintf("%d", userInfo.Id),
+ SysType: sType,
+ DevType: dType,
+ Mobile: userInfo.Email,
+ BindingTime: &bindAt,
+ })
+ }
+ return list, nil
+}
+
+// UserInviteList 邀请绑定同步
+func UserInviteList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.UserInviteBindMsg, error) {
+ userList, err := usermod.StatcenterSyncInviteList(context.Background(), request.SuccessTime, request.MaxSize)
+ if err != nil {
+ log.Error("UserInviteList", log.E(err))
+ return nil, err
+ }
+ list := make([]statcenter.UserInviteBindMsg, 0, len(userList))
+ for _, userInfo := range userList {
+ list = append(list, statcenter.UserInviteBindMsg{
+ UserId: uint64(userInfo.Id),
+ AppId: appId,
+ ParentPromCode: userInfo.AgentInviteCode,
+ InviteTime: userInfo.CreatedAt,
+ })
+ }
+ return list, nil
+}
+
+// ConsumeRecordList 金币消费流水同步
+func ConsumeRecordList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.ConsumeRecordMsg, error) {
+ startId := int64(0)
+ if request.PlatformId != "" {
+ id, err := strconv.ParseInt(request.PlatformId, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("invalid consume cursor platformId: %s", request.PlatformId)
+ }
+ startId = id
+ }
+ transList, err := cointransmod.StatSyncConsumeList(context.Background(), startId, request.MaxSize)
+ if err != nil {
+ log.Error("ConsumeRecordList", log.E(err))
+ return nil, err
+ }
+ list := make([]statcenter.ConsumeRecordMsg, 0, len(transList))
+ for _, item := range transList {
+ consumeType := statcenter.StatValueAddSer
+ if item.TranType == cointransmod.TranTypeVideoUnlock {
+ consumeType = statcenter.StatVideoUnlock
+ }
+ list = append(list, statcenter.ConsumeRecordMsg{
+ AppID: appId,
+ UID: uint64(item.UserId),
+ Type: consumeType,
+ CurrencyType: statcenter.CurrencyTypeGold,
+ Money: decimal.Zero,
+ Amount: decimal.NewFromInt(item.Amount),
+ Uniq: strconv.FormatInt(item.Id, 10),
+ CreatedAt: item.CreatedAt,
+ })
+ }
+ return list, nil
+}
+
+// UserRechargeList 成功充值订单同步(会员+金币)
+func UserRechargeList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (registerList []statcenter.UserRechargeMsg, err error) {
+ orderList, err := vipordermod.StatSyncSuccessOrderList(context.Background(), request.SuccessTime, request.MaxSize)
+ if err != nil {
+ log.Error("UserRechargeList StatSyncSuccessOrderList", log.E(err))
+ return nil, err
+ }
+ userMap := findUserDistrictMap(orderList)
+ registerList = make([]statcenter.UserRechargeMsg, 0, len(orderList))
+ for _, order := range orderList {
+ sType, dType := order.SysType, ""
+ if appId == statcenter.STAT_APPID && isNewStat {
+ sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(order.SysType), "")
+ }
+ msg := statcenter.UserRechargeMsg{
+ UserId: uint64(order.UserId),
+ AppId: appId,
+ SysType: sType,
+ DevType: dType,
+ PlatformId: fmt.Sprintf("%d", order.Id),
+ ChannelName: order.PayChannel,
+ CID: order.Oid,
+ Type: order.PayMode,
+ OrderId: order.TradeNo,
+ OID: order.Oid,
+ Money: order.Amount,
+ PayMoney: order.Amount,
+ Status: statcenter.MapOrderStatusToStat(order.Status),
+ Rate: order.PayRate,
+ SuccessAt: order.FinishedAt,
+ CreatedAt: order.CreatedAt,
+ }
+ fillRechargeDistrict(&msg, userMap)
+ if order.OrderType == vipordermod.OrderTypeCoin {
+ msg.ChanShareMod = 1
+ } else {
+ msg.ChanShareMod = 2
+ }
+ registerList = append(registerList, msg)
+ }
+ return
+}
+
+// UserAllRechargeList 全部非失败充值订单同步
+func UserAllRechargeList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (registerList []statcenter.UserRechargeMsg, err error) {
+ orderList, err := vipordermod.StatSyncAllOrderList(context.Background(), request.SuccessTime, request.MaxSize)
+ if err != nil {
+ log.Error("UserAllRechargeList StatSyncAllOrderList", log.E(err))
+ return nil, err
+ }
+ userMap := findUserDistrictMap(orderList)
+ registerList = make([]statcenter.UserRechargeMsg, 0, len(orderList))
+ for _, order := range orderList {
+ sType, dType := order.SysType, ""
+ if appId == statcenter.STAT_APPID && isNewStat {
+ sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(order.SysType), "")
+ }
+ msg := statcenter.UserRechargeMsg{
+ UserId: uint64(order.UserId),
+ AppId: appId,
+ SysType: sType,
+ DevType: dType,
+ PlatformId: fmt.Sprintf("%d", order.Id),
+ ChannelName: order.PayChannel,
+ CID: order.Oid,
+ Type: order.PayMode,
+ OrderId: order.TradeNo,
+ OID: order.Oid,
+ Money: order.Amount,
+ PayMoney: order.Amount,
+ Status: statcenter.MapOrderStatusToStat(order.Status),
+ Rate: order.PayRate,
+ CreatedAt: order.CreatedAt,
+ SuccessAt: order.CreatedAt,
+ }
+ fillRechargeDistrict(&msg, userMap)
+ registerList = append(registerList, msg)
+ }
+ return
+}
+
+// CardSellList 会员卡销售流水同步(成功会员订单)
+func CardSellList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.CardSellMsg, error) {
+ orderList, err := vipordermod.StatSyncVipOrderList(context.Background(), request.SuccessTime, request.MaxSize)
+ if err != nil {
+ log.Error("CardSellList", log.E(err))
+ return nil, err
+ }
+ list := make([]statcenter.CardSellMsg, 0, len(orderList))
+ for _, order := range orderList {
+ sType, _ := pubdef.GetCenterType(pubdef.SysTypeFromString(order.SysType), "")
+ list = append(list, statcenter.CardSellMsg{
+ AppID: appId,
+ UID: uint64(order.UserId),
+ UniqID: fmt.Sprintf("%d", order.Id),
+ Amount: order.Amount,
+ TranTypeInt: int64(order.OrderType),
+ TranType: "会员卡",
+ SysType: sType,
+ CurrencyType: statcenter.CurrencyTypeCash,
+ TranCreatedAt: order.FinishedAt,
+ Product: statcenter.Product{
+ ID: fmt.Sprintf("%d", order.CardId),
+ Name: order.CardName,
+ DiscountedPrice: order.Amount,
+ },
+ })
+ }
+ return list, nil
+}
+
+// AiSellList AI 销售流水(本项目无 AI 业务,返回空列表)
+func AiSellList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.AiSellMsg, error) {
+ return make([]statcenter.AiSellMsg, 0), nil
+}
+
+// UserSyncVipList VIP 购买成功订单同步
+func UserSyncVipList(appId int32, request statcenter.StatcenterSyncReq) (registerList []statcenter.UserBuyVIPMsg, err error) {
+ orderList, err := vipordermod.StatSyncVipOrderList(context.Background(), request.SuccessTime, request.MaxSize)
+ if err != nil {
+ log.Error("UserSyncVipList StatSyncVipOrderList", log.E(err))
+ return nil, err
+ }
+ userMap := findUserDistrictMap(orderList)
+ registerList = make([]statcenter.UserBuyVIPMsg, 0, len(orderList))
+ for _, order := range orderList {
+ msg := statcenter.UserBuyVIPMsg{
+ UID: uint64(order.UserId),
+ AppId: appId,
+ PlatformId: fmt.Sprintf("%d", order.Id),
+ Name: order.CardName,
+ Money: order.Amount,
+ BuyVIPType: statcenter.BuyVIPTypeMoney,
+ OID: order.Oid,
+ DeviceType: order.SysType,
+ Status: 3,
+ SuccessAt: order.FinishedAt,
+ }
+ if userInfo, ok := userMap[order.UserId]; ok {
+ msg.IsDirect = userInfo.IsDirect
+ msg.DistrictCode = userInfo.DistrictCode
+ }
+ registerList = append(registerList, msg)
+ }
+ return
+}
+
+func findUserDistrictMap(orderList vipordermod.VipOrderList) map[int64]*usermod.User {
+ userIds := make([]int64, 0, len(orderList))
+ seen := make(map[int64]struct{}, len(orderList))
+ for _, order := range orderList {
+ if order == nil || order.UserId <= 0 {
+ continue
+ }
+ if _, ok := seen[order.UserId]; ok {
+ continue
+ }
+ seen[order.UserId] = struct{}{}
+ userIds = append(userIds, order.UserId)
+ }
+ userMap := make(map[int64]*usermod.User, len(userIds))
+ if len(userIds) == 0 {
+ return userMap
+ }
+ userList, _ := usermod.FindManyUser(context.Background(), bson.M{usermod.FN.User.Id: bson.M{"$in": userIds}})
+ for _, u := range userList {
+ userMap[u.Id] = u
+ }
+ return userMap
+}
+
+func fillRechargeDistrict(msg *statcenter.UserRechargeMsg, userMap map[int64]*usermod.User) {
+ if msg == nil {
+ return
+ }
+ userInfo, ok := userMap[int64(msg.UserId)]
+ if !ok || userInfo == nil {
+ return
+ }
+ msg.DistrictCode = userInfo.DistrictCode
+ msg.RegisterTime = userInfo.CreatedAt
+}
diff --git a/app/api/translationapi/translation.go b/app/api/translationapi/translation.go
new file mode 100644
index 0000000..478dafd
--- /dev/null
+++ b/app/api/translationapi/translation.go
@@ -0,0 +1,22 @@
+package translationapi
+
+import (
+ "videoweb/app/service/translationser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// CategoryList app端导航分类列表
+// @Tags APP导航相关接口
+// @Summary app端导航分类列表 --by 王鸣
+// @Description 无请求参数,不分页,返回启用的顶部导航分类
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.AppTranslationInfo "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/translation/category/list [get]
+func CategoryList(ctx *gin.Context) {
+ common.ServerJSON(ctx, srvdef.Success, translationser.GetTranslationData())
+}
diff --git a/app/api/uploadapi/upload.go b/app/api/uploadapi/upload.go
new file mode 100644
index 0000000..66f4300
--- /dev/null
+++ b/app/api/uploadapi/upload.go
@@ -0,0 +1,49 @@
+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"
+)
+
+// UploadImage 上传图片
+// @Tags 上传
+// @Summary 图片上传
+// @Description 将图片转发至存储服务 /media/web/autopublish/image
+// @Accept multipart/form-data
+// @Produce json
+// @Param file formData file true "图片文件"
+// @Param watermark formData string false "是否加水印:1加,其他情况不加(头像不加)"
+// @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
+ }
+
+ watermark := int64(0)
+ if ctx.PostForm("watermark") == "1" {
+ watermark = 1
+ }
+
+ param := &uploadser.ImageParam{
+ FileHeader: f,
+ Watermark: watermark,
+ }
+ 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..f2108e0
--- /dev/null
+++ b/app/api/videoapi/video.go
@@ -0,0 +1,60 @@
+package videoapi
+
+import (
+ "videoweb/app/api/proto"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 普通分页查询视频
+// @Tags 视频相关接口
+// @Summary 普通分页查询视频 --by 旺鸣
+// @Description 按上架时间倒序普通分页查询已上架视频
+// @Accept json
+// @Produce json
+// @Param params query proto.VideoReq true "普通分页查询视频请求"
+// @Success 200 {object} proto.VideoResp{list=[]proto.VideoItemResp} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/video/list [get]
+func List(ctx *gin.Context) {
+ var req proto.VideoReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := videoser.VideoList(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Detail 查询视频详情
+// @Tags 视频相关接口
+// @Summary 查询视频详情 --by 旺鸣
+// @Description 查询已上架视频详情和该视频相关推荐列表
+// @Accept json
+// @Produce json
+// @Param params query proto.VideoInfoReq true "查询视频详情请求"
+// @Success 200 {object} proto.VideoInfoResp{recommendVideos=[]proto.VideoItemResp} "成功"
+// @Failure 400 {string} string "失败"
+// @Router /api/app/video/detail [get]
+func Detail(ctx *gin.Context) {
+ var req proto.VideoInfoReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ ua, _ := common.GetUA(ctx)
+ resp, err := videoser.VideoInfo(ctx, &req, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), ua.UserAgent)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/app/appg/appg.go b/app/appg/appg.go
new file mode 100644
index 0000000..5ebd138
--- /dev/null
+++ b/app/appg/appg.go
@@ -0,0 +1,123 @@
+package appg
+
+import (
+ "videoweb/pub/common/ipaddr"
+
+ "github.com/patrickmn/go-cache"
+)
+
+const EsIndex = "sz"
+
+var (
+ Conf GlobalCfgEx
+ Cache *cache.Cache
+ 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"`
+ 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"`
+ TrustedProxies string `yaml:"trustedProxies"`
+ BucketVideoAddressV2 string `yaml:"bucketVideoAddressV2"`
+ 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"`
+ LsjMediaConf struct {
+ AppId string `yaml:"appId"`
+ ApiKey string `yaml:"apiKey"`
+ ApiUrl string `yaml:"apiUrl"`
+ ImageYuan string `yaml:"imageYuan"`
+ KeyHex string `yaml:"keyHex"`
+ M3u8Yuan string `yaml:"m3u8Yuan"`
+ TsYuan string `yaml:"tsYuan"`
+ DomainsRefreshHr int64 `yaml:"domainsRefreshHr"`
+ } `yaml:"lsjMediaConf"`
+}
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..1e589ef
--- /dev/null
+++ b/app/appg/helper.go
@@ -0,0 +1,34 @@
+package appg
+
+import (
+ "fmt"
+ "math/rand/v2"
+ "time"
+)
+
+var familyNames = []string{
+ "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈",
+ "褚", "卫", "蒋", "沈", "韩", "杨", "朱", "秦", "尤", "许",
+ "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏",
+ "陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章",
+ "云", "苏", "潘", "葛", "奚", "范", "彭", "郎", "鲁", "韦",
+}
+
+var givenNames = []string{
+ "伟", "芳", "娜", "秀英", "敏", "静", "丽", "强", "磊", "洋",
+ "艳", "勇", "军", "杰", "娟", "涛", "明", "超", "秀兰", "霞",
+ "平", "刚", "桂英", "华", "玲", "辉", "英", "志明", "建国", "文",
+ "国强", "小红", "建军", "小明", "宇", "浩", "晨", "鑫", "博", "宸",
+ "若曦", "雨桐", "梓涵", "紫萱", "雨萱", "诗涵", "欣怡", "嘉怡", "子涵", "梦琪",
+}
+
+// GenerateNickname 随机生成一个中文昵称,格式:姓+名
+// 若词表为空则降级为"用户"+时间戳后6位
+func GenerateNickname() string {
+ if len(familyNames) == 0 || len(givenNames) == 0 {
+ return fmt.Sprintf("用户%06d", time.Now().UnixNano()%1000000)
+ }
+ family := familyNames[rand.IntN(len(familyNames))]
+ given := givenNames[rand.IntN(len(givenNames))]
+ return family + given
+}
diff --git a/app/conf/app.yaml b/app/conf/app.yaml
new file mode 100644
index 0000000..7eb45e6
--- /dev/null
+++ b/app/conf/app.yaml
@@ -0,0 +1,47 @@
+redis:
+ addr: "172.104.181.99:16789"
+ db: 2
+ 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_qmdj?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_qmdj_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: "25119"
+origDomain: "https://yuan.bbaazz.me/"
+trustedProxies: ""
+bucketVideoAddressV2: "http://139.162.22.207:24996"
+transactionUrl: "https://bill.ruishengces.com"
+adCenter:
+ apiDomain: "https://adopenapi.adcs01.top"
+ merchantCode: "1991129808640122880"
+ appCode: "JHA-1009"
+ aesKey: "gD4+rmbX5TS5cLbb3wAN5uEAYp/1j4hkQrS9SzuSjc8"
+dataReport:
+ url: "https://api.shuifeng.cc"
+lsjMediaConf:
+ appId: "430696"
+ apiKey: "ed48e96ce490bc94bd5eb5133dd542d0"
+ apiUrl: "https://zhlsaj.zrr4ij4.site"
+ imageYuan: "https://d1kh52pvweq1c0.cloudfront.net"
+ keyHex: "525202f9149e061d"
+ m3u8Yuan: "https://zhlsaj.zrr4ij4.site"
+ tsYuan: "https://tssdaa.hlvwcn.cn"
\ 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/lsjm3u8.key b/app/conf/lsjm3u8.key
new file mode 100644
index 0000000..d93e501
Binary files /dev/null and b/app/conf/lsjm3u8.key 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/deploy_prod.sh b/app/deploy_prod.sh
new file mode 100755
index 0000000..09ed892
--- /dev/null
+++ b/app/deploy_prod.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+FILE_NAME="web_mdcom_app"
+REMOTE_FILE_PATH="/home/server2/web_mdcom/app/"
+TAG_NAME="web_mdcom_app.tar.gz"
+
+set -e
+set -o
+function send_msg() {
+ msg="@jscdsndj @xiuyuan_Lee @steven0yu %0A服务:SERVER2 站点-91spz.com管理服务APP%0A更新内容:$content%0A创建时间: $(date +%F_%X)%0A操作人:$(git config user.name)"
+ curl -X POST "https://api.telegram.org/bot6357085965:AAEmXfjZnZwnDTicL7Wjbd7cXvYv_G98w8A/sendMessage" -d "chat_id=$1&text=$msg" >/dev/null 2>&1
+}
+
+read -p "输入yes, 开始上传: " word
+if [[ ! "yes" =~ $word ]]; then
+ echo "终止"
+else
+ read -e -p "本次更新内容(输入q退出编辑): " -r content
+ echo "开始"
+ rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
+ GOOS=linux GOARCH=amd64 go build -o $FILE_NAME
+ md5sum $FILE_NAME | cut -d ' ' -f1 >yinsemd5.txt
+ tar zvcf $TAG_NAME ./$FILE_NAME yinsemd5.txt
+ md5sum $TAG_NAME | cut -d ' ' -f1 >yinsemd5.txt
+ scp -i ~/.ssh/id_rsa -P 22889 ./yinsemd5.txt server2@116.212.120.2:$REMOTE_FILE_PATH
+ scp -i ~/.ssh/id_rsa -P 22889 ./$TAG_NAME server2@116.212.120.2:$REMOTE_FILE_PATH
+ send_msg -4791758991 #服务器小组
+fi
+rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
\ No newline at end of file
diff --git a/app/deploy_test.sh b/app/deploy_test.sh
new file mode 100755
index 0000000..80d3c94
--- /dev/null
+++ b/app/deploy_test.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+sudo make test
+
+scp -P 22889 web_91av_app.tar.gz root@172.104.181.99:/server/web_91av/app/
+ssh root@172.104.181.99 -p 22889 "cd /server/web_91av/app/ && /bin/tar -xf /server/web_91av/app/web_91av_app.tar.gz && /usr/bin/supervisorctl restart web_91av_app"
+if [ $? -eq 0 ];then
+ echo -e "\033[32m 91spz.com H5更新成功 \033[0m"
+else
+ echo -e "\033[31m 91spz.com H5更新失败 \033[0m"
+fi
+rm -f web_91av_app.tar.gz
diff --git a/app/docs/docs.go b/app/docs/docs.go
new file mode 100644
index 0000000..40d4ae2
--- /dev/null
+++ b/app/docs/docs.go
@@ -0,0 +1,6781 @@
+// Package docs Code generated by swaggo/swag. DO NOT EDIT
+package 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/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 获取最新用户信息,包含金币余额、VIP状态等",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "获取当前用户信息",
+ "responses": {
+ "200": {
+ "description": "用户信息",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/guest": {
+ "post": {
+ "description": "按设备ID创建或找回游客账号;请求头可带 Authorization 复用已有游客会话。需传 X-User-Agent",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "游客登录",
+ "parameters": [
+ {
+ "description": "游客登录参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authser.GuestLoginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "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": "用户信息通过 token 获取,无需传账号;nickName、avatar、introduction、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": "传入账号、邮箱、明文密码。请求头带游客 Authorization 时升级该游客为普通用户,否则新建用户",
+ "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/auth/resetPassword": {
+ "post": {
+ "description": "通过账号和邮箱双重校验后重置密码,两者必须同时匹配同一用户",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "找回密码",
+ "parameters": [
+ {
+ "description": "找回密码参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.resetPasswordReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/coin/buy": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据指定金币套餐发起第三方支付,返回支付链接",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币"
+ ],
+ "summary": "购买金币",
+ "parameters": [
+ {
+ "description": "购买参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/coinapi.buyCoinReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "支付链接",
+ "schema": {
+ "$ref": "#/definitions/coinser.BuyCoinResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/coin/orders": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币"
+ ],
+ "summary": "查询金币购买记录",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "页码(默认1)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量(默认10)",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "订单列表",
+ "schema": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/coin/trans/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "返回当前登录用户的金币收入、支出流水,按交易时间倒序",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币"
+ ],
+ "summary": "查询金币余额明细 -- by 旺鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "页码(默认1)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量(默认10,最大100)",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/response.BasePageResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CoinTransListItemResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/like/set": {
+ "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.CommentLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.CommentLikeSetResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/list": {
+ "get": {
+ "description": "分页查询短剧或短剧剧集评论树,一级评论分页返回,二级评论挂在children中,objectType 10-短剧 11-短剧剧集",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论APP"
+ ],
+ "summary": "评论列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "对象ID(短剧id 短剧剧集id)",
+ "name": "objectId",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "对象类型 10-短剧 11-短剧剧集",
+ "name": "objectType",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "排序 1-热门,按点赞数倒序 2-最新,按创建时间倒序",
+ "name": "sortOrder",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.GetCommentListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/send": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "发布短剧或短剧剧集评论,parentsId为0发布一级评论,大于0发布二级评论,replyCommentId表示实际回复的评论,objectType 10-短剧 11-短剧剧集",
+ "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/info": {
+ "get": {
+ "description": "返回最新地址信息、完整系统配置、广告配置和全站SEO配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "APP配置相关接口"
+ ],
+ "summary": "查询APP公共配置 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ConfigInfoResp"
+ }
+ },
+ "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/contactus/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "用户提交联系我们表单,问题类型:1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们相关接口"
+ ],
+ "summary": "提交联系我们 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "联系我们请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/feedback/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "用户提交意见反馈,问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "意见反馈相关接口"
+ ],
+ "summary": "提交意见反馈 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "意见反馈请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/collect/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按当前用户收藏时间倒序分页返回短剧剧集收藏列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端我的短剧剧集收藏列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/collect/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧剧集收藏状态,收藏数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集收藏/取消收藏 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集收藏设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/detail": {
+ "get": {
+ "description": "根据短剧剧集ID查询播放页当前剧集详情;可传playletId或playletSlug校验剧集归属,slug非空时优先解析短剧ID",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据ID查询短剧剧集详情 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧剧集ID",
+ "name": "chapterId",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "短剧ID,传入时用于校验剧集归属",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "playletSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/like/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧剧集点赞状态,点赞数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集点赞/取消点赞 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集点赞设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterLikeSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/list": {
+ "get": {
+ "description": "根据短剧ID或slug查询右侧选集列表;slug非空时优先按slug解析短剧ID",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据短剧ID查询剧集列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "playletSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/unlock": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "仅金币付费剧集可调用;接口具备幂等性,已解锁剧集重复调用不会重复扣金币",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端使用金币逐集解锁短剧剧集 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集金币解锁请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterUnlockReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterUnlockResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/watch/report": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "上报当前用户短剧剧集播放进度,达到已看条件时用于我的收藏未看集数计算",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集观看上报 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集观看上报请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterWatchReportReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterWatchReportResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/collect/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按当前用户收藏时间倒序分页返回短剧收藏列表,并返回已看/未看集数和最近观看位置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端我的短剧收藏列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCollectListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/collect/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧收藏状态,收藏数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧收藏/取消收藏 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧收藏设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCollectSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCollectSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/detail": {
+ "get": {
+ "description": "根据短剧ID或slug查询已上架短剧详情;slug非空时优先按slug解析短剧ID",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据ID查询短剧详情 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧id",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "slug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/forward/heat": {
+ "post": {
+ "description": "请求一次短剧热度值增加5,仅支持已上架短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧转发增加热度 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧转发加热度请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletForwardHeatReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletForwardHeatResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/history/clear": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "仅清空观看历史,不影响收藏列表和观看进度",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端清空我的短剧观看历史 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletHistoryClearResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/history/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按当前用户最近观看时间倒序分页返回短剧观看历史;pageSize默认20、最大100",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端我的短剧观看历史列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletHistoryListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/home": {
+ "get": {
+ "description": "无请求参数,返回首页banner、最近更新和启用短剧分类数据;homeCategoryData按分类rank倒序返回全部启用分类,每个分类最多16条短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端首页短剧接口 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomePlayletResponse"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/hot-like": {
+ "get": {
+ "description": "返回缓存的前10个热搜词,以及按收藏量最多的15部猜你喜欢短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端热搜词和猜你喜欢",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletHotLikeResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/like/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧点赞状态,点赞数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧点赞/取消点赞 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧点赞设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletLikeSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/list": {
+ "get": {
+ "description": "分页查询已上架短剧;可按题材ID或题材slug筛选,slug非空时优先按slug解析题材ID",
+ "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": "排序 1-热度优先,按收藏量倒序 2-今日更新,按上架时间倒序",
+ "name": "sortOrder",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "题材ID",
+ "name": "themeId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "题材slug,非空时优先按slug解析题材ID",
+ "name": "themeSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletVideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/operate/list": {
+ "get": {
+ "description": "按运营类型查询已上架短剧;operateType=1短剧 2漫剧;filterType=1最近更新(按上架时间) 2当前热播(按收藏数) 3独家原创(原创且按推荐排序)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端按运营类型分页查询短剧",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "筛选类型 1-最近更新 2-当前热播 3-独家原创",
+ "name": "filterType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "运营类型 1-短剧 2-漫剧",
+ "name": "operateType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletOperateListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/rank": {
+ "get": {
+ "description": "分页查询已上架短剧排行榜;sortOrder=1按热度值倒序,2按上架时间倒序,3按收藏数倒序",
+ "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": "排序 1-热播榜,按热度值倒序 2-新剧榜,按上架时间倒序 3-追剧榜,按收藏数倒序",
+ "name": "sortOrder",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRankResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/recommend/list": {
+ "get": {
+ "description": "根据短剧分类ID或分类slug分页查询已上架短剧;slug非空时优先按slug解析分类ID;pageSize默认10、最大100",
+ "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"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/search": {
+ "get": {
+ "description": "使用ES分页搜索已上架短剧,支持剧名、题材名、简介和标签名检索",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端ES分页搜索短剧 --by 王鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "搜索关键词,支持剧名、题材名、简介和标签名",
+ "name": "keyword",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletSearchResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/tag/list": {
+ "get": {
+ "description": "根据标签ID或标签slug分页查询已上架短剧;slug非空时优先按slug解析标签ID",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据标签ID分页查询短剧 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "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.PlayletTagVideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/today-update": {
+ "get": {
+ "description": "分页查询已上架短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端分页查询今日更新短剧 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletTodayUpdateResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/recharge/package/list": {
+ "get": {
+ "description": "返回会员畅享和金币充值两个列表,并附带各套餐可用支付方式",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币与会员"
+ ],
+ "summary": "查询购买套餐列表 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.RechargePackageListResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/statcenter/sync": {
+ "post": {
+ "description": "按 job 拉取注册/日活/充值/绑定/消费/售卡等增量数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "数据同步"
+ ],
+ "summary": "统计中心增量同步",
+ "parameters": [
+ {
+ "description": "参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/statcenter.StatcenterSyncReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/statcenter.StatcenterSyncResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/theme/list": {
+ "get": {
+ "description": "无请求参数,不分页,返回已上架题材列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧题材相关接口"
+ ],
+ "summary": "app端短剧题材列表 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeListResponse"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/translation/category/list": {
+ "get": {
+ "description": "无请求参数,不分页,返回启用的顶部导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "APP导航相关接口"
+ ],
+ "summary": "app端导航分类列表 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ },
+ "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
+ },
+ {
+ "type": "string",
+ "description": "是否加水印:1加,其他情况不加(头像不加)",
+ "name": "watermark",
+ "in": "formData"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.ImageUploadResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/detail": {
+ "get": {
+ "description": "查询已上架视频详情和该视频相关推荐列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "查询视频详情 --by 旺鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "视频的id必填",
+ "name": "id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "recommendVideos": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/list": {
+ "get": {
+ "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.VideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/vip/orders": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "充值"
+ ],
+ "summary": "查询 VIP 充值记录",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "页码(默认1)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量(默认10)",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "订单列表",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/vipordermod.VipOrder"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/app/vip/pay": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "充值"
+ ],
+ "summary": "创建 VIP 充值订单(第三方支付)",
+ "parameters": [
+ {
+ "description": "下单参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/rechargeapi.submitOrderReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "支付链接",
+ "schema": {
+ "$ref": "#/definitions/rechargeser.SubmitOrderResp"
+ }
+ }
+ }
+ }
+ },
+ "/richtext/: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"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "authapi.loginReq": {
+ "type": "object",
+ "required": [
+ "account",
+ "password"
+ ],
+ "properties": {
+ "account": {
+ "description": "账号或邮箱",
+ "type": "string"
+ },
+ "channel": {
+ "description": "渠道码(可选,用户无商区码时写入)",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示(可选)",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.registerReq": {
+ "type": "object",
+ "required": [
+ "accountNo",
+ "email",
+ "password"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "agentInviteCode": {
+ "description": "邀请人的邀请码(可选)",
+ "type": "string"
+ },
+ "channel": {
+ "description": "渠道码(可选,用户无商区码时写入)",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示(可选)",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.resetPasswordReq": {
+ "type": "object",
+ "required": [
+ "accountNo",
+ "email",
+ "newPassword"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "newPassword": {
+ "description": "新密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.updateProfileReq": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "description": "新头像 URL(可选)",
+ "type": "string"
+ },
+ "introduction": {
+ "description": "简介(可选)",
+ "type": "string"
+ },
+ "newPassword": {
+ "description": "新密码明文(修改密码时必填)",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "新昵称(可选)",
+ "type": "string"
+ },
+ "oldPassword": {
+ "description": "旧密码明文(修改密码时必填)",
+ "type": "string"
+ },
+ "privacyMode": {
+ "description": "隐私模式:true-开启,false-关闭(可选)",
+ "type": "boolean"
+ }
+ }
+ },
+ "authser.GuestLoginReq": {
+ "type": "object",
+ "properties": {
+ "channel": {
+ "description": "渠道码(可选,用户无商区码时写入 DistrictCode)",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示(必填)",
+ "type": "string"
+ },
+ "deviceId": {
+ "description": "备用设备ID(可选,写入 DevV2)",
+ "type": "string"
+ },
+ "mac": {
+ "description": "设备 Mac(可选)",
+ "type": "string"
+ }
+ }
+ },
+ "authser.LoginResp": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "currentVipExpire": {
+ "description": "VIP 过期时间,客户端需结合 vipType 判断是否真正有效",
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "introduction": {
+ "description": "简介",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "isNewUser": {
+ "description": "本次是否新建游客",
+ "type": "boolean"
+ },
+ "isRegister": {
+ "description": "是否已注册",
+ "type": "boolean"
+ },
+ "loginType": {
+ "description": "登陆方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "nickName": {
+ "type": "string"
+ },
+ "privacyMode": {
+ "description": "隐私模式",
+ "type": "boolean"
+ },
+ "recharge": {
+ "description": "金币余额",
+ "type": "integer"
+ },
+ "score": {
+ "description": "积分",
+ "type": "integer"
+ },
+ "showCardId": {
+ "description": "最新会员卡id",
+ "type": "integer"
+ },
+ "showCardName": {
+ "description": "最新会员卡名称",
+ "type": "string"
+ },
+ "status": {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ },
+ "token": {
+ "type": "string"
+ },
+ "type": {
+ "description": "身份类型:0游客 1普通用户",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ },
+ "userId": {
+ "type": "integer"
+ },
+ "vipType": {
+ "$ref": "#/definitions/pubdef.VipType"
+ }
+ }
+ },
+ "coinapi.buyCoinReq": {
+ "type": "object",
+ "required": [
+ "packageId",
+ "payMode"
+ ],
+ "properties": {
+ "packageId": {
+ "description": "购买套餐ID(金币充值)",
+ "type": "integer"
+ },
+ "payMode": {
+ "description": "支付方式:alipay/wechat",
+ "type": "string"
+ }
+ }
+ },
+ "coinser.BuyCoinResp": {
+ "type": "object",
+ "properties": {
+ "isOpenNewBrowser": {
+ "type": "boolean"
+ },
+ "payUrl": {
+ "type": "string"
+ },
+ "payUrlMode": {
+ "type": "string"
+ },
+ "tradeNo": {
+ "type": "string"
+ }
+ }
+ },
+ "cointransmod.FlowDir": {
+ "type": "integer",
+ "enum": [
+ 1,
+ -1
+ ],
+ "x-enum-comments": {
+ "FlowIn": "收入(充值、赠送)",
+ "FlowOut": "支出(消费、解锁)"
+ },
+ "x-enum-varnames": [
+ "FlowIn",
+ "FlowOut"
+ ]
+ },
+ "cointransmod.TranType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "x-enum-comments": {
+ "TranTypeAdminAdjust": "管理员手动调整",
+ "TranTypeCoinRecharge": "金币充值(购买金币)",
+ "TranTypeDarkAlley": "历史遗留交易类型(已下线)",
+ "TranTypeVideoUnlock": "视频解锁",
+ "TranTypeVipGiftCoin": "购买会员赠送金币"
+ },
+ "x-enum-varnames": [
+ "TranTypeCoinRecharge",
+ "TranTypeVipGiftCoin",
+ "TranTypeDarkAlley",
+ "TranTypeVideoUnlock",
+ "TranTypeAdminAdjust"
+ ]
+ },
+ "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"
+ }
+ },
+ "bottomBanner": {
+ "description": "底部banner",
+ "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"
+ }
+ },
+ "downList": {
+ "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"
+ }
+ },
+ "nineGridList": {
+ "description": "9宫格",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "pauseyList": {
+ "description": "暂停广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "playList": {
+ "description": "播放强制跳转",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "titleList": {
+ "description": "标题广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "videoInfoList": {
+ "description": "视频详情",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ }
+ }
+ },
+ "proto.AnnouncementItem": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "id": {
+ "description": "公告ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "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.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"
+ }
+ ]
+ },
+ "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"
+ }
+ }
+ },
+ "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.CoinTransListItemResp": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "本次交易金币数,始终为正数",
+ "type": "integer"
+ },
+ "balanceAfter": {
+ "description": "交易后余额",
+ "type": "integer"
+ },
+ "balanceBefore": {
+ "description": "交易前余额",
+ "type": "integer"
+ },
+ "changeAmount": {
+ "description": "带方向的金币变动数,收入为正数,支出为负数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "交易时间,格式:2006-01-02 15:04:05",
+ "type": "string"
+ },
+ "flowDir": {
+ "description": "收支方向:1-收入,-1-支出",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.FlowDir"
+ }
+ ]
+ },
+ "flowDirName": {
+ "description": "收支方向名称:收入/支出",
+ "type": "string"
+ },
+ "id": {
+ "description": "流水ID",
+ "type": "integer"
+ },
+ "orderNo": {
+ "description": "关联订单号",
+ "type": "string"
+ },
+ "relatedId": {
+ "description": "关联内容ID",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题,优先使用备注,备注为空时使用交易类型名称",
+ "type": "string"
+ },
+ "tranType": {
+ "description": "交易类型 1-金币充值(购买金币)2-购买会员赠送金币 3-历史遗留 4-视频解锁 5-管理员手动调整",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.TranType"
+ }
+ ]
+ },
+ "tranTypeName": {
+ "description": "交易类型名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommentInfo": {
+ "type": "object",
+ "properties": {
+ "children": {
+ "description": "二级评论列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ },
+ "commentCount": {
+ "description": "二级评论数量",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "评论点赞数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "当前用户点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ },
+ "level": {
+ "description": "评论级别 1-一级评论 2-二级评论",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "父评论ID,一级评论为0",
+ "type": "integer"
+ },
+ "replyCommentId": {
+ "description": "被回复评论ID,一级评论为0",
+ "type": "integer"
+ },
+ "replyUserName": {
+ "description": "被回复用户昵称",
+ "type": "string"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userAvatar": {
+ "description": "用户头像,已拼接图片域名",
+ "type": "string"
+ },
+ "userId": {
+ "description": "评论用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommentLikeSetReq": {
+ "type": "object",
+ "required": [
+ "commentId",
+ "isLike"
+ ],
+ "properties": {
+ "commentId": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞,false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.CommentLikeSetResp": {
+ "type": "object",
+ "properties": {
+ "commentId": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "评论点赞数",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.CommentSendReq": {
+ "type": "object",
+ "properties": {
+ "objectId": {
+ "description": "对象ID,短剧评论传短剧ID,剧集评论传剧集ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "一级评论ID,0-发布一级评论,大于0-发布二级评论",
+ "type": "integer"
+ },
+ "replyCommentId": {
+ "description": "被回复评论ID,0时默认回复parentsId",
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommonData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图,已拼接图片域名",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "点赞数,包含真实点赞数和后台虚拟点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "description": {
+ "description": "SEO Description",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "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"
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态: 0下架 1上架",
+ "type": "integer"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ }
+ }
+ },
+ "proto.ConfigInfo": {
+ "type": "object",
+ "properties": {
+ "actressBackGround": {
+ "description": "女优通用背景图",
+ "type": "string"
+ },
+ "advitiesTopicNum": {
+ "description": "广告间隔",
+ "type": "integer"
+ },
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "customerServiceHost": {
+ "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"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "最新地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "officialAppAddr": {
+ "description": "官网app地址",
+ "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"
+ }
+ },
+ "watermarkImage": {
+ "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"
+ }
+ }
+ },
+ "proto.ConfigInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告配置",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "config": {
+ "description": "完整系统配置(与首页等接口的 config 一致,含 potaoGroup 等其余字段)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "navPageList": {
+ "description": "导航页线路集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.NavPageItem"
+ }
+ },
+ "newAddressInfo": {
+ "description": "最新地址信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.NewAddressInfo"
+ }
+ ]
+ },
+ "reportSdkConfig": {
+ "description": "数据上报SDK域名配置",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "全站SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "seoNavInfo": {
+ "description": "导航SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoNavInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.ContactUsAddReq": {
+ "type": "object",
+ "required": [
+ "content",
+ "email",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "attachments": {
+ "description": "附件地址,可存放多张图片或文件地址",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsAddResp": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "description": "提示信息",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FeedbackReq": {
+ "type": "object",
+ "required": [
+ "content",
+ "type"
+ ],
+ "properties": {
+ "contact": {
+ "description": "联系方式,可为空",
+ "type": "string"
+ },
+ "content": {
+ "description": "描述内容,最多500字",
+ "type": "string"
+ },
+ "screenshots": {
+ "description": "问题截图,可存放多张图片地址",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "type": {
+ "description": "问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackResp": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "description": "提示信息",
+ "type": "string"
+ }
+ }
+ },
+ "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.GetCommentListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerData": {
+ "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.HomeCategoryData": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "icon": {
+ "description": "分类图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "list": {
+ "description": "分类下短剧集合,最多16条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "分类排序,越大越靠前",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HomeHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "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.HomePlayletResponse": {
+ "type": "object",
+ "properties": {
+ "announcements": {
+ "description": "启用中的公告,来自独立缓存",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AnnouncementItem"
+ }
+ },
+ "homeBannerData": {
+ "description": "首页banner信息列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeBannerData"
+ }
+ },
+ "homeCategoryData": {
+ "description": "启用分类短剧集合,按分类排序,每个分类最多16条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeCategoryData"
+ }
+ },
+ "homeRecommendThemes": {
+ "description": "推荐主题,最多4条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeRecommendTheme"
+ }
+ },
+ "homeTodayUpdateData": {
+ "description": "最近更新集合,最多16条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(短剧列表页SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomeRecommendTheme": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "description": "题材图标,已拼接图片域名",
+ "type": "string"
+ },
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "playletCount": {
+ "description": "该题材绑定的上架短剧数量",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HotSearchItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架shi时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "images": {
+ "description": "封面",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.NavPageItem": {
+ "type": "object",
+ "properties": {
+ "line": {
+ "description": "线路序号",
+ "type": "integer"
+ },
+ "url": {
+ "description": "线路地址",
+ "type": "string"
+ }
+ }
+ },
+ "proto.NewAddressInfo": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "customerServiceHost": {
+ "description": "在线客服",
+ "type": "string"
+ },
+ "foreverUrlAddr": {
+ "description": "永久海外地址(翻墙访问 永不迷路)",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "新国内地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "officialAppAddr": {
+ "description": "官网app地址",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PayMethodInfo": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "alipay / wechat / union",
+ "type": "string"
+ },
+ "typeName": {
+ "description": "支付宝 / 微信 / 银联",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectData": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "chapterTitle": {
+ "description": "剧集标题",
+ "type": "string"
+ },
+ "collectedAt": {
+ "description": "收藏时间",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "剧集观看数",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "剧集收藏数",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态,收藏列表固定返回1-已收藏",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "type": "integer"
+ },
+ "playletCover": {
+ "description": "短剧封面,已拼接图片域名",
+ "type": "string"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "playletTitle": {
+ "description": "短剧标题",
+ "type": "string"
+ },
+ "singleCoins": {
+ "description": "单集金币",
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "description": "短剧总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "videoUrl": {
+ "description": "播放地址",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "收藏剧集集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总收藏数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectSetReq": {
+ "type": "object",
+ "required": [
+ "chapterId",
+ "isCollect"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "是否收藏,true-收藏,false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectSetResp": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "短剧剧集收藏数",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态 1-已收藏 2-未收藏",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterData": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "description": "当前金币余额",
+ "type": "integer"
+ },
+ "canWatch": {
+ "description": "是否可观看",
+ "type": "boolean"
+ },
+ "costCoins": {
+ "description": "本次解锁消耗金币",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isUnlocked": {
+ "description": "当前用户是否已解锁本集",
+ "type": "boolean"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "needUnlock": {
+ "description": "是否需要金币解锁",
+ "type": "boolean"
+ },
+ "needVip": {
+ "description": "是否需要开通VIP",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币(支付方式为1-金币时候生效)",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "视频地址",
+ "type": "string"
+ },
+ "vipValid": {
+ "description": "当前用户VIP是否有效",
+ "type": "boolean"
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterInfoResp": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "description": "当前金币余额",
+ "type": "integer"
+ },
+ "canWatch": {
+ "description": "是否可观看",
+ "type": "boolean"
+ },
+ "costCoins": {
+ "description": "本次解锁消耗金币",
+ "type": "integer"
+ },
+ "countBrowse": {
+ "description": "短剧剧集观看数",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "短剧剧集收藏数",
+ "type": "integer"
+ },
+ "countComment": {
+ "description": "短剧剧集评论数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "短剧剧集点赞数",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态 1-已收藏 2-未收藏",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ },
+ "isUnlocked": {
+ "description": "当前用户是否已解锁本集",
+ "type": "boolean"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "needUnlock": {
+ "description": "是否需要金币解锁",
+ "type": "boolean"
+ },
+ "needVip": {
+ "description": "是否需要开通VIP",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币(支付方式为1-金币时候生效)",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "视频地址",
+ "type": "string"
+ },
+ "vipValid": {
+ "description": "当前用户VIP是否有效",
+ "type": "boolean"
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterLikeSetReq": {
+ "type": "object",
+ "required": [
+ "chapterId",
+ "isLike"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞,false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterLikeSetResp": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "短剧剧集点赞数",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧剧集集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterData"
+ }
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterUnlockReq": {
+ "type": "object",
+ "required": [
+ "chapterId",
+ "playletId"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterUnlockResp": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "description": "当前金币余额",
+ "type": "integer"
+ },
+ "canWatch": {
+ "description": "是否可观看",
+ "type": "boolean"
+ },
+ "changeCoin": {
+ "description": "本次实际扣减金币,幂等命中时为0",
+ "type": "integer"
+ },
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "costCoins": {
+ "description": "本次解锁消耗金币",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "unlocked": {
+ "description": "是否已解锁",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterWatchReportReq": {
+ "type": "object",
+ "required": [
+ "chapterId"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID,传入时用于校验剧集归属",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterWatchReportResp": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "当前集数",
+ "type": "integer"
+ },
+ "lastChapterId": {
+ "description": "最近观看剧集ID",
+ "type": "integer"
+ },
+ "lastEpisode": {
+ "description": "最近观看集数",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "unwatchedCount": {
+ "description": "当前用户未看集数",
+ "type": "integer"
+ },
+ "watchedCount": {
+ "description": "当前用户已看集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCollectData": {
+ "type": "object",
+ "properties": {
+ "collectedAt": {
+ "description": "收藏时间",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态,收藏列表固定返回1-已收藏",
+ "type": "integer"
+ },
+ "lastChapterId": {
+ "description": "最近观看剧集ID,无观看进度时为0",
+ "type": "integer"
+ },
+ "lastEpisode": {
+ "description": "最近观看集数,无观看进度时为0",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "unwatchedCount": {
+ "description": "当前用户未看集数,按totalEpisode-watchedCount计算",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "watchedCount": {
+ "description": "当前用户已看集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCollectListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "收藏短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletCollectData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总收藏数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCollectSetReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isCollect"
+ ],
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "是否收藏,true-收藏,false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletCollectSetResp": {
+ "type": "object",
+ "properties": {
+ "countCollect": {
+ "description": "短剧收藏数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态 1-已收藏 2-未收藏",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletForwardHeatReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletForwardHeatResp": {
+ "type": "object",
+ "properties": {
+ "heatValue": {
+ "description": "加热后的热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHistoryClearResp": {
+ "type": "object",
+ "properties": {
+ "deletedCount": {
+ "description": "删除记录数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHistoryData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图,已拼接图片域名",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "点赞数,包含真实点赞数和后台虚拟点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "description": {
+ "description": "SEO Description",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "historyAt": {
+ "description": "最近观看时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isFinished": {
+ "description": "是否已看完",
+ "type": "boolean"
+ },
+ "isNew": {
+ "description": "是否上新,本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐,false-不推荐",
+ "type": "boolean"
+ },
+ "lastChapterId": {
+ "description": "最近观看剧集ID",
+ "type": "integer"
+ },
+ "lastEpisode": {
+ "description": "最近观看集数",
+ "type": "integer"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态: 0下架 1上架",
+ "type": "integer"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "unwatchedCount": {
+ "description": "当前用户未看集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "watchedCount": {
+ "description": "当前用户已看集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHistoryListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "观看历史集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletHistoryData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总历史数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHotLikeResp": {
+ "type": "object",
+ "properties": {
+ "guessLike": {
+ "description": "按收藏量最多的15部上架短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "hotSearch": {
+ "description": "前10个热搜词",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletHotSearchItem"
+ }
+ }
+ }
+ },
+ "proto.PlayletHotSearchItem": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "description": "搜索次数",
+ "type": "integer"
+ },
+ "keyword": {
+ "description": "搜索词",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletInfoResp": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图,已拼接图片域名",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "点赞数,包含真实点赞数和后台虚拟点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "description": {
+ "description": "SEO Description",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "是否收藏(1-已收藏,2-未收藏)",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isLike": {
+ "description": "是否点赞(1-已点赞,2-未点赞)",
+ "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"
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "playletChapterInfo": {
+ "description": "短剧章节集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterData"
+ }
+ },
+ "relatedRecommendData": {
+ "description": "相关推荐集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "seoInfo": {
+ "description": "页面SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态: 0下架 1上架",
+ "type": "integer"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletLikeSetReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isLike"
+ ],
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞,false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletLikeSetResp": {
+ "type": "object",
+ "properties": {
+ "countLike": {
+ "description": "短剧点赞数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletOperateListResp": {
+ "type": "object",
+ "properties": {
+ "filterType": {
+ "description": "筛选类型 1-最近更新 2-当前热播 3-独家原创",
+ "type": "integer"
+ },
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRankResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(短剧列表页SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRecommendListResp": {
+ "type": "object",
+ "properties": {
+ "categoryDesc": {
+ "description": "分类描述",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "categoryName": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "categorySlug": {
+ "description": "分类slug",
+ "type": "string"
+ },
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(分类SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletSearchResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagVideoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(标签SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeItem": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "description": "题材图标,已拼接图片域名",
+ "type": "string"
+ },
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ },
+ "isRecommend": {
+ "description": "是否推荐",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "playletCount": {
+ "description": "该题材绑定的上架短剧数量",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "题材对应页面 SEO 信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeListResponse": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "题材列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletThemeItem"
+ }
+ }
+ }
+ },
+ "proto.PlayletTodayUpdateResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(短剧列表页SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletVideoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息:有 themeId 时返回题材 SEO,否则返回短剧列表页 SEO",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.RechargePackageItem": {
+ "type": "object",
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数",
+ "type": "integer"
+ },
+ "durationDays": {
+ "description": "会员有效天数",
+ "type": "integer"
+ },
+ "durationHours": {
+ "description": "会员有效小时数,由durationDays计算",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "payMethods": {
+ "description": "该套餐支持的支付方式",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PayMethodInfo"
+ }
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "sort": {
+ "description": "排序,越小越靠前",
+ "type": "integer"
+ },
+ "subTitle": {
+ "description": "展示副标题",
+ "type": "string"
+ },
+ "totalCoin": {
+ "description": "到账总金币数,由baseCoin+giftCoin计算",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.RechargePackageListResp": {
+ "type": "object",
+ "properties": {
+ "coinList": {
+ "description": "金币充值列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.RechargePackageItem"
+ }
+ },
+ "vipList": {
+ "description": "会员畅享列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.RechargePackageItem"
+ }
+ }
+ }
+ },
+ "proto.SeoInfo": {
+ "type": "object",
+ "properties": {
+ "canonicalUrl": {
+ "description": "当前内容的标准URL",
+ "type": "string"
+ },
+ "description": {
+ "description": "页面描述",
+ "type": "string"
+ },
+ "keywords": {
+ "description": "页面关键词",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "页面标题",
+ "type": "string"
+ },
+ "webSiteName": {
+ "description": "网站名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.SeoNavInfo": {
+ "type": "object",
+ "properties": {
+ "dhDescription": {
+ "description": "导航描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "关键词",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhSiteName": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TranslationCategory": {
+ "type": "object",
+ "properties": {
+ "categoryType": {
+ "description": "导航类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.TranslationType"
+ }
+ ]
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "translationList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationInfo"
+ }
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TranslationInfo": {
+ "type": "object",
+ "properties": {
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.VideoData": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "fileId": {
+ "description": "媒资库ID",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "视频URl",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "recommendVideos": {
+ "description": "相关推荐,按当前视频标签匹配并按上架时间倒序返回最多20条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ },
+ "seoInfo": {
+ "description": "页面SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoData"
+ }
+ ]
+ },
+ "watchTimes": {
+ "description": "观看数(真假之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoItemResp": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "观看数(真假之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "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.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeEmail": "邮箱登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeAccount",
+ "LoginTypeEmail"
+ ]
+ },
+ "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"
+ ]
+ },
+ "pubdef.UserType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 0
+ ],
+ "x-enum-comments": {
+ "UserTypeGuest": "游客用户",
+ "UserTypeNormal": "普通用户(已注册)"
+ },
+ "x-enum-varnames": [
+ "UserTypeGuest",
+ "UserTypeNormal",
+ "UserTypeNone"
+ ]
+ },
+ "pubdef.VipOrderStatus": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "VipOrderFailed": "支付失败",
+ "VipOrderPending": "待支付",
+ "VipOrderSuccess": "支付成功"
+ },
+ "x-enum-varnames": [
+ "VipOrderPending",
+ "VipOrderSuccess",
+ "VipOrderFailed"
+ ]
+ },
+ "pubdef.VipType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "VipTypeMonth": "月卡",
+ "VipTypeNone": "没卡",
+ "VipTypeYear": "年卡"
+ },
+ "x-enum-varnames": [
+ "VipTypeNone",
+ "VipTypeMonth",
+ "VipTypeYear"
+ ]
+ },
+ "rechargeapi.submitOrderReq": {
+ "type": "object",
+ "required": [
+ "packageId",
+ "payMode"
+ ],
+ "properties": {
+ "packageId": {
+ "description": "购买套餐ID(会员畅享)",
+ "type": "integer"
+ },
+ "payMode": {
+ "description": "支付方式:alipay/wechat",
+ "type": "string"
+ }
+ }
+ },
+ "rechargepackagemod.PackageType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PackageTypeCoin": "金币充值",
+ "PackageTypeVip": "会员畅享"
+ },
+ "x-enum-varnames": [
+ "PackageTypeVip",
+ "PackageTypeCoin"
+ ]
+ },
+ "rechargeser.SubmitOrderResp": {
+ "type": "object",
+ "properties": {
+ "isOpenNewBrowser": {
+ "description": "是否需要外部浏览器打开",
+ "type": "boolean"
+ },
+ "payUrl": {
+ "description": "支付链接",
+ "type": "string"
+ },
+ "payUrlMode": {
+ "description": "链接打开方式",
+ "type": "string"
+ },
+ "tradeNo": {
+ "description": "本地订单号(可用于查询)",
+ "type": "string"
+ }
+ }
+ },
+ "response.BasePageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.AiSellMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": "integer"
+ },
+ "appID": {
+ "type": "integer"
+ },
+ "currencyType": {
+ "type": "string"
+ },
+ "devType": {
+ "type": "string"
+ },
+ "isRepurchase": {
+ "type": "string"
+ },
+ "sysType": {
+ "type": "string"
+ },
+ "tranCreatedAt": {
+ "type": "string"
+ },
+ "tranType": {
+ "type": "string"
+ },
+ "uid": {
+ "type": "integer"
+ },
+ "uniqID": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.BuyVIPType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "BuyVIPTypeGold": "金币",
+ "BuyVIPTypeMoney": "现金"
+ },
+ "x-enum-varnames": [
+ "BuyVIPTypeMoney",
+ "BuyVIPTypeGold"
+ ]
+ },
+ "statcenter.CardSellMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": "integer"
+ },
+ "appID": {
+ "type": "integer"
+ },
+ "currencyType": {
+ "description": "0金币 1现金",
+ "type": "integer"
+ },
+ "product": {
+ "$ref": "#/definitions/statcenter.Product"
+ },
+ "sysType": {
+ "type": "string"
+ },
+ "tranCreatedAt": {
+ "type": "string"
+ },
+ "tranType": {
+ "type": "string"
+ },
+ "tranTypeInt": {
+ "type": "integer"
+ },
+ "uid": {
+ "type": "integer"
+ },
+ "uniqID": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.ConsumeRecordMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "金币",
+ "type": "number"
+ },
+ "appID": {
+ "type": "integer"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "currencyType": {
+ "description": "0金币 1人民币",
+ "type": "integer"
+ },
+ "money": {
+ "description": "人民币 元",
+ "type": "number"
+ },
+ "type": {
+ "description": "消费类型",
+ "type": "integer"
+ },
+ "uid": {
+ "type": "integer"
+ },
+ "uniq": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.Product": {
+ "type": "object",
+ "properties": {
+ "discountedPrice": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.StatcenterSyncReq": {
+ "type": "object",
+ "properties": {
+ "job": {
+ "description": "同步任务类型",
+ "type": "string"
+ },
+ "maxSize": {
+ "description": "最大条数",
+ "type": "integer"
+ },
+ "platformId": {
+ "description": "原始平台Id(访问/流水)",
+ "type": "string"
+ },
+ "successTime": {
+ "description": "游标时间(购买会员、提现、充值)",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户(注册)",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.StatcenterSyncResp": {
+ "type": "object",
+ "properties": {
+ "accessList": {
+ "description": "日活记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserVisitMsg"
+ }
+ },
+ "aiSellList": {
+ "description": "AI销售流水",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.AiSellMsg"
+ }
+ },
+ "allRechargeList": {
+ "description": "全部支付订单",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserRechargeMsg"
+ }
+ },
+ "bindingList": {
+ "description": "用户绑定记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserBindingMsg"
+ }
+ },
+ "buyVipList": {
+ "description": "VIP购买记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserBuyVIPMsg"
+ }
+ },
+ "cardSellList": {
+ "description": "会员卡销售流水",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.CardSellMsg"
+ }
+ },
+ "code": {
+ "description": "200正常 其他异常",
+ "type": "integer"
+ },
+ "consumeList": {
+ "description": "消费流水",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.ConsumeRecordMsg"
+ }
+ },
+ "inviteList": {
+ "description": "邀请绑定",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserInviteBindMsg"
+ }
+ },
+ "job": {
+ "description": "类型",
+ "type": "string"
+ },
+ "msg": {
+ "description": "错误消息",
+ "type": "string"
+ },
+ "rechargeList": {
+ "description": "充值记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserRechargeMsg"
+ }
+ },
+ "registerList": {
+ "description": "注册记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserRegisterMsg"
+ }
+ },
+ "visitList": {
+ "description": "日活记录(旧字段,兼容)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserVisitMsg"
+ }
+ }
+ }
+ },
+ "statcenter.UserBindingMsg": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appID",
+ "type": "integer"
+ },
+ "bindingTime": {
+ "description": "绑定时间",
+ "type": "string"
+ },
+ "devType": {
+ "description": "设备类型",
+ "type": "string"
+ },
+ "mobile": {
+ "description": "手机号/绑定账号(本项目填邮箱)",
+ "type": "string"
+ },
+ "platformId": {
+ "description": "原始平台流水Id",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "操作系统类型",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserBuyVIPMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": "integer"
+ },
+ "appId": {
+ "type": "integer"
+ },
+ "buyVIPType": {
+ "$ref": "#/definitions/statcenter.BuyVIPType"
+ },
+ "deviceType": {
+ "type": "string"
+ },
+ "districtCode": {
+ "type": "string"
+ },
+ "isDirect": {
+ "type": "boolean"
+ },
+ "money": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "oid": {
+ "type": "string"
+ },
+ "platformId": {
+ "type": "string"
+ },
+ "promCode": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer"
+ },
+ "successTime": {
+ "type": "string"
+ },
+ "uid": {
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserInviteBindMsg": {
+ "type": "object",
+ "required": [
+ "appId",
+ "inviteTime",
+ "userId"
+ ],
+ "properties": {
+ "appId": {
+ "type": "integer"
+ },
+ "inviteTime": {
+ "type": "string"
+ },
+ "parentPromCode": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserRechargeMsg": {
+ "type": "object",
+ "required": [
+ "appId",
+ "money",
+ "orderId",
+ "payMoney",
+ "sysType",
+ "userId"
+ ],
+ "properties": {
+ "appId": {
+ "type": "integer"
+ },
+ "chanShareMod": {
+ "type": "integer"
+ },
+ "channelName": {
+ "type": "string"
+ },
+ "cid": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "devType": {
+ "type": "string"
+ },
+ "districtCode": {
+ "type": "string"
+ },
+ "isNewOrOldUser": {
+ "type": "boolean"
+ },
+ "money": {
+ "type": "integer"
+ },
+ "oid": {
+ "type": "string"
+ },
+ "orderId": {
+ "type": "string"
+ },
+ "payMoney": {
+ "type": "integer"
+ },
+ "paymentAt": {
+ "type": "string"
+ },
+ "platformId": {
+ "type": "string"
+ },
+ "productType": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "string"
+ },
+ "registerTime": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer"
+ },
+ "successAt": {
+ "type": "string"
+ },
+ "sysType": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserRegisterMsg": {
+ "type": "object",
+ "required": [
+ "appId",
+ "isDirect",
+ "registerTime",
+ "sysType",
+ "userId"
+ ],
+ "properties": {
+ "appId": {
+ "description": "appID",
+ "type": "integer"
+ },
+ "devType": {
+ "description": "设备类型",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "ip": {
+ "description": "IP",
+ "type": "string"
+ },
+ "isDirect": {
+ "description": "true:是直推用户",
+ "type": "boolean"
+ },
+ "mobile": {
+ "description": "手机号",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "platformId": {
+ "description": "原始平台流水Id",
+ "type": "string"
+ },
+ "promCode": {
+ "description": "推广序列",
+ "type": "string"
+ },
+ "promSeqe": {
+ "description": "全名代理",
+ "type": "string"
+ },
+ "puc": {
+ "description": "应用中心推广",
+ "type": "string"
+ },
+ "registerTime": {
+ "description": "注册时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "操作系统类型 安卓 IOS",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserVisitMsg": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appID",
+ "type": "integer"
+ },
+ "devID": {
+ "description": "设备ID",
+ "type": "string"
+ },
+ "devType": {
+ "description": "设备型号",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "ip": {
+ "description": "IP",
+ "type": "string"
+ },
+ "isDirect": {
+ "description": "true:是直推用户",
+ "type": "boolean"
+ },
+ "platformId": {
+ "description": "原始平台流水Id",
+ "type": "string"
+ },
+ "registerTime": {
+ "description": "用户注册时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "操作系统类型 安卓 IOS",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "version": {
+ "description": "APP版本号",
+ "type": "string"
+ },
+ "visitAt": {
+ "description": "访问时间",
+ "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"
+ }
+ }
+ },
+ "vipordermod.OrderType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "OrderTypeCoin": "金币购买",
+ "OrderTypeVip": "会员购买"
+ },
+ "x-enum-varnames": [
+ "OrderTypeVip",
+ "OrderTypeCoin"
+ ]
+ },
+ "vipordermod.VipOrder": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "支付金额(分)",
+ "type": "integer"
+ },
+ "cardId": {
+ "description": "── VIP 专用 ──",
+ "type": "integer"
+ },
+ "cardName": {
+ "description": "套餐名快照",
+ "type": "string"
+ },
+ "coin": {
+ "description": "到账金币数(快照)",
+ "type": "integer"
+ },
+ "coinCfgId": {
+ "description": "── 金币专用 ──",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "下单时用户渠道码",
+ "type": "string"
+ },
+ "finishedAt": {
+ "description": "完成时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "唯一ID",
+ "type": "integer"
+ },
+ "ipAddr": {
+ "description": "客户端 IP",
+ "type": "string"
+ },
+ "oid": {
+ "description": "第三方订单号",
+ "type": "string"
+ },
+ "orderType": {
+ "description": "订单类型:1会员 2金币",
+ "allOf": [
+ {
+ "$ref": "#/definitions/vipordermod.OrderType"
+ }
+ ]
+ },
+ "payChannel": {
+ "description": "支付渠道",
+ "type": "string"
+ },
+ "payMode": {
+ "description": "支付方式:overage/alipay/wechat",
+ "type": "string"
+ },
+ "payRate": {
+ "description": "费率",
+ "type": "string"
+ },
+ "payUrl": {
+ "description": "支付链接",
+ "type": "string"
+ },
+ "payUrlMode": {
+ "description": "链接打开方式",
+ "type": "string"
+ },
+ "status": {
+ "description": "订单状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipOrderStatus"
+ }
+ ]
+ },
+ "sysType": {
+ "description": "ios/android",
+ "type": "string"
+ },
+ "tradeNo": {
+ "description": "本地订单号",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "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: "https://视频站-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..ec17eba
--- /dev/null
+++ b/app/docs/swagger.json
@@ -0,0 +1,6761 @@
+{
+ "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": "https://视频站-api.lcncctn.com",
+ "basePath": "/",
+ "paths": {
+ "/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 获取最新用户信息,包含金币余额、VIP状态等",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "获取当前用户信息",
+ "responses": {
+ "200": {
+ "description": "用户信息",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/auth/guest": {
+ "post": {
+ "description": "按设备ID创建或找回游客账号;请求头可带 Authorization 复用已有游客会话。需传 X-User-Agent",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "游客登录",
+ "parameters": [
+ {
+ "description": "游客登录参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authser.GuestLoginReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/authser.LoginResp"
+ }
+ },
+ "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": "用户信息通过 token 获取,无需传账号;nickName、avatar、introduction、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": "传入账号、邮箱、明文密码。请求头带游客 Authorization 时升级该游客为普通用户,否则新建用户",
+ "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/auth/resetPassword": {
+ "post": {
+ "description": "通过账号和邮箱双重校验后重置密码,两者必须同时匹配同一用户",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户认证"
+ ],
+ "summary": "找回密码",
+ "parameters": [
+ {
+ "description": "找回密码参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/authapi.resetPasswordReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/coin/buy": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据指定金币套餐发起第三方支付,返回支付链接",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币"
+ ],
+ "summary": "购买金币",
+ "parameters": [
+ {
+ "description": "购买参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/coinapi.buyCoinReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "支付链接",
+ "schema": {
+ "$ref": "#/definitions/coinser.BuyCoinResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/coin/orders": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币"
+ ],
+ "summary": "查询金币购买记录",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "页码(默认1)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量(默认10)",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "订单列表",
+ "schema": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/coin/trans/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "返回当前登录用户的金币收入、支出流水,按交易时间倒序",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币"
+ ],
+ "summary": "查询金币余额明细 -- by 旺鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "页码(默认1)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量(默认10,最大100)",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/response.BasePageResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CoinTransListItemResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/like/set": {
+ "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.CommentLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.CommentLikeSetResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/list": {
+ "get": {
+ "description": "分页查询短剧或短剧剧集评论树,一级评论分页返回,二级评论挂在children中,objectType 10-短剧 11-短剧剧集",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "评论APP"
+ ],
+ "summary": "评论列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "对象ID(短剧id 短剧剧集id)",
+ "name": "objectId",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "对象类型 10-短剧 11-短剧剧集",
+ "name": "objectType",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "排序 1-热门,按点赞数倒序 2-最新,按创建时间倒序",
+ "name": "sortOrder",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.GetCommentListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/comment/send": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "发布短剧或短剧剧集评论,parentsId为0发布一级评论,大于0发布二级评论,replyCommentId表示实际回复的评论,objectType 10-短剧 11-短剧剧集",
+ "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/info": {
+ "get": {
+ "description": "返回最新地址信息、完整系统配置、广告配置和全站SEO配置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "APP配置相关接口"
+ ],
+ "summary": "查询APP公共配置 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ConfigInfoResp"
+ }
+ },
+ "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/contactus/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "用户提交联系我们表单,问题类型:1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "联系我们相关接口"
+ ],
+ "summary": "提交联系我们 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "联系我们请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsAddResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/feedback/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "用户提交意见反馈,问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "意见反馈相关接口"
+ ],
+ "summary": "提交意见反馈 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "意见反馈请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/collect/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按当前用户收藏时间倒序分页返回短剧剧集收藏列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端我的短剧剧集收藏列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/collect/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧剧集收藏状态,收藏数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集收藏/取消收藏 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集收藏设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/detail": {
+ "get": {
+ "description": "根据短剧剧集ID查询播放页当前剧集详情;可传playletId或playletSlug校验剧集归属,slug非空时优先解析短剧ID",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据ID查询短剧剧集详情 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧剧集ID",
+ "name": "chapterId",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "短剧ID,传入时用于校验剧集归属",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "playletSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/like/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧剧集点赞状态,点赞数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集点赞/取消点赞 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集点赞设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterLikeSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/list": {
+ "get": {
+ "description": "根据短剧ID或slug查询右侧选集列表;slug非空时优先按slug解析短剧ID",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据短剧ID查询剧集列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧ID",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "playletSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/unlock": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "仅金币付费剧集可调用;接口具备幂等性,已解锁剧集重复调用不会重复扣金币",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端使用金币逐集解锁短剧剧集 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集金币解锁请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterUnlockReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterUnlockResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/chapter/watch/report": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "上报当前用户短剧剧集播放进度,达到已看条件时用于我的收藏未看集数计算",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧剧集观看上报 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧剧集观看上报请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterWatchReportReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterWatchReportResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/collect/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按当前用户收藏时间倒序分页返回短剧收藏列表,并返回已看/未看集数和最近观看位置",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端我的短剧收藏列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCollectListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/collect/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧收藏状态,收藏数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧收藏/取消收藏 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧收藏设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCollectSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletCollectSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/detail": {
+ "get": {
+ "description": "根据短剧ID或slug查询已上架短剧详情;slug非空时优先按slug解析短剧ID",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据ID查询短剧详情 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "短剧id",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "短剧slug,非空时优先按slug解析短剧ID",
+ "name": "slug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletInfoResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/forward/heat": {
+ "post": {
+ "description": "请求一次短剧热度值增加5,仅支持已上架短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧转发增加热度 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧转发加热度请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletForwardHeatReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletForwardHeatResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/history/clear": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "仅清空观看历史,不影响收藏列表和观看进度",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端清空我的短剧观看历史 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletHistoryClearResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/history/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "按当前用户最近观看时间倒序分页返回短剧观看历史;pageSize默认20、最大100",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端我的短剧观看历史列表 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletHistoryListResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/home": {
+ "get": {
+ "description": "无请求参数,返回首页banner、最近更新和启用短剧分类数据;homeCategoryData按分类rank倒序返回全部启用分类,每个分类最多16条短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端首页短剧接口 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.HomePlayletResponse"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/hot-like": {
+ "get": {
+ "description": "返回缓存的前10个热搜词,以及按收藏量最多的15部猜你喜欢短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端热搜词和猜你喜欢",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletHotLikeResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/like/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "设置当前用户短剧点赞状态,点赞数只在状态真实变化时增减",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端短剧点赞/取消点赞 --by 王鸣",
+ "parameters": [
+ {
+ "description": "短剧点赞设置请求",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletLikeSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletLikeSetResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/list": {
+ "get": {
+ "description": "分页查询已上架短剧;可按题材ID或题材slug筛选,slug非空时优先按slug解析题材ID",
+ "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": "排序 1-热度优先,按收藏量倒序 2-今日更新,按上架时间倒序",
+ "name": "sortOrder",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "题材ID",
+ "name": "themeId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "题材slug,非空时优先按slug解析题材ID",
+ "name": "themeSlug",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletVideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/operate/list": {
+ "get": {
+ "description": "按运营类型查询已上架短剧;operateType=1短剧 2漫剧;filterType=1最近更新(按上架时间) 2当前热播(按收藏数) 3独家原创(原创且按推荐排序)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端按运营类型分页查询短剧",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "筛选类型 1-最近更新 2-当前热播 3-独家原创",
+ "name": "filterType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "运营类型 1-短剧 2-漫剧",
+ "name": "operateType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletOperateListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/rank": {
+ "get": {
+ "description": "分页查询已上架短剧排行榜;sortOrder=1按热度值倒序,2按上架时间倒序,3按收藏数倒序",
+ "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": "排序 1-热播榜,按热度值倒序 2-新剧榜,按上架时间倒序 3-追剧榜,按收藏数倒序",
+ "name": "sortOrder",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRankResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/recommend/list": {
+ "get": {
+ "description": "根据短剧分类ID或分类slug分页查询已上架短剧;slug非空时优先按slug解析分类ID;pageSize默认10、最大100",
+ "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"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletRecommendListResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/search": {
+ "get": {
+ "description": "使用ES分页搜索已上架短剧,支持剧名、题材名、简介和标签名检索",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端ES分页搜索短剧 --by 王鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "搜索关键词,支持剧名、题材名、简介和标签名",
+ "name": "keyword",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletSearchResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/tag/list": {
+ "get": {
+ "description": "根据标签ID或标签slug分页查询已上架短剧;slug非空时优先按slug解析标签ID",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端根据标签ID分页查询短剧 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "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.PlayletTagVideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/playlet/today-update": {
+ "get": {
+ "description": "分页查询已上架短剧",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧相关接口"
+ ],
+ "summary": "app端分页查询今日更新短剧 --by 王鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.PlayletTodayUpdateResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/recharge/package/list": {
+ "get": {
+ "description": "返回会员畅享和金币充值两个列表,并附带各套餐可用支付方式",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币与会员"
+ ],
+ "summary": "查询购买套餐列表 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.RechargePackageListResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/statcenter/sync": {
+ "post": {
+ "description": "按 job 拉取注册/日活/充值/绑定/消费/售卡等增量数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "数据同步"
+ ],
+ "summary": "统计中心增量同步",
+ "parameters": [
+ {
+ "description": "参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/statcenter.StatcenterSyncReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/statcenter.StatcenterSyncResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/theme/list": {
+ "get": {
+ "description": "无请求参数,不分页,返回已上架题材列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "app短剧题材相关接口"
+ ],
+ "summary": "app端短剧题材列表 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletThemeListResponse"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/translation/category/list": {
+ "get": {
+ "description": "无请求参数,不分页,返回启用的顶部导航分类",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "APP导航相关接口"
+ ],
+ "summary": "app端导航分类列表 --by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.AppTranslationInfo"
+ }
+ },
+ "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
+ },
+ {
+ "type": "string",
+ "description": "是否加水印:1加,其他情况不加(头像不加)",
+ "name": "watermark",
+ "in": "formData"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/uploadser.ImageUploadResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/detail": {
+ "get": {
+ "description": "查询已上架视频详情和该视频相关推荐列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频相关接口"
+ ],
+ "summary": "查询视频详情 --by 旺鸣",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "视频的id必填",
+ "name": "id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoInfoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "recommendVideos": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/video/list": {
+ "get": {
+ "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.VideoResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/app/vip/orders": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "充值"
+ ],
+ "summary": "查询 VIP 充值记录",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "页码(默认1)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量(默认10)",
+ "name": "pageSize",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "订单列表",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/vipordermod.VipOrder"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/app/vip/pay": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "充值"
+ ],
+ "summary": "创建 VIP 充值订单(第三方支付)",
+ "parameters": [
+ {
+ "description": "下单参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/rechargeapi.submitOrderReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "支付链接",
+ "schema": {
+ "$ref": "#/definitions/rechargeser.SubmitOrderResp"
+ }
+ }
+ }
+ }
+ },
+ "/richtext/: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"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "authapi.loginReq": {
+ "type": "object",
+ "required": [
+ "account",
+ "password"
+ ],
+ "properties": {
+ "account": {
+ "description": "账号或邮箱",
+ "type": "string"
+ },
+ "channel": {
+ "description": "渠道码(可选,用户无商区码时写入)",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示(可选)",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.registerReq": {
+ "type": "object",
+ "required": [
+ "accountNo",
+ "email",
+ "password"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "agentInviteCode": {
+ "description": "邀请人的邀请码(可选)",
+ "type": "string"
+ },
+ "channel": {
+ "description": "渠道码(可选,用户无商区码时写入)",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示(可选)",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "password": {
+ "description": "明文密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.resetPasswordReq": {
+ "type": "object",
+ "required": [
+ "accountNo",
+ "email",
+ "newPassword"
+ ],
+ "properties": {
+ "accountNo": {
+ "description": "账号",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "newPassword": {
+ "description": "新密码",
+ "type": "string"
+ }
+ }
+ },
+ "authapi.updateProfileReq": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "description": "新头像 URL(可选)",
+ "type": "string"
+ },
+ "introduction": {
+ "description": "简介(可选)",
+ "type": "string"
+ },
+ "newPassword": {
+ "description": "新密码明文(修改密码时必填)",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "新昵称(可选)",
+ "type": "string"
+ },
+ "oldPassword": {
+ "description": "旧密码明文(修改密码时必填)",
+ "type": "string"
+ },
+ "privacyMode": {
+ "description": "隐私模式:true-开启,false-关闭(可选)",
+ "type": "boolean"
+ }
+ }
+ },
+ "authser.GuestLoginReq": {
+ "type": "object",
+ "properties": {
+ "channel": {
+ "description": "渠道码(可选,用户无商区码时写入 DistrictCode)",
+ "type": "string"
+ },
+ "devID": {
+ "description": "设备唯一标示(必填)",
+ "type": "string"
+ },
+ "deviceId": {
+ "description": "备用设备ID(可选,写入 DevV2)",
+ "type": "string"
+ },
+ "mac": {
+ "description": "设备 Mac(可选)",
+ "type": "string"
+ }
+ }
+ },
+ "authser.LoginResp": {
+ "type": "object",
+ "properties": {
+ "accountNo": {
+ "type": "string"
+ },
+ "avatar": {
+ "description": "头像",
+ "type": "string"
+ },
+ "currentVipExpire": {
+ "description": "VIP 过期时间,客户端需结合 vipType 判断是否真正有效",
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "introduction": {
+ "description": "简介",
+ "type": "string"
+ },
+ "inviteCode": {
+ "description": "邀请码",
+ "type": "string"
+ },
+ "isNewUser": {
+ "description": "本次是否新建游客",
+ "type": "boolean"
+ },
+ "isRegister": {
+ "description": "是否已注册",
+ "type": "boolean"
+ },
+ "loginType": {
+ "description": "登陆方式",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.LoginType"
+ }
+ ]
+ },
+ "nickName": {
+ "type": "string"
+ },
+ "privacyMode": {
+ "description": "隐私模式",
+ "type": "boolean"
+ },
+ "recharge": {
+ "description": "金币余额",
+ "type": "integer"
+ },
+ "score": {
+ "description": "积分",
+ "type": "integer"
+ },
+ "showCardId": {
+ "description": "最新会员卡id",
+ "type": "integer"
+ },
+ "showCardName": {
+ "description": "最新会员卡名称",
+ "type": "string"
+ },
+ "status": {
+ "$ref": "#/definitions/pubdef.UserStatus"
+ },
+ "token": {
+ "type": "string"
+ },
+ "type": {
+ "description": "身份类型:0游客 1普通用户",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ },
+ "userId": {
+ "type": "integer"
+ },
+ "vipType": {
+ "$ref": "#/definitions/pubdef.VipType"
+ }
+ }
+ },
+ "coinapi.buyCoinReq": {
+ "type": "object",
+ "required": [
+ "packageId",
+ "payMode"
+ ],
+ "properties": {
+ "packageId": {
+ "description": "购买套餐ID(金币充值)",
+ "type": "integer"
+ },
+ "payMode": {
+ "description": "支付方式:alipay/wechat",
+ "type": "string"
+ }
+ }
+ },
+ "coinser.BuyCoinResp": {
+ "type": "object",
+ "properties": {
+ "isOpenNewBrowser": {
+ "type": "boolean"
+ },
+ "payUrl": {
+ "type": "string"
+ },
+ "payUrlMode": {
+ "type": "string"
+ },
+ "tradeNo": {
+ "type": "string"
+ }
+ }
+ },
+ "cointransmod.FlowDir": {
+ "type": "integer",
+ "enum": [
+ 1,
+ -1
+ ],
+ "x-enum-comments": {
+ "FlowIn": "收入(充值、赠送)",
+ "FlowOut": "支出(消费、解锁)"
+ },
+ "x-enum-varnames": [
+ "FlowIn",
+ "FlowOut"
+ ]
+ },
+ "cointransmod.TranType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "x-enum-comments": {
+ "TranTypeAdminAdjust": "管理员手动调整",
+ "TranTypeCoinRecharge": "金币充值(购买金币)",
+ "TranTypeDarkAlley": "历史遗留交易类型(已下线)",
+ "TranTypeVideoUnlock": "视频解锁",
+ "TranTypeVipGiftCoin": "购买会员赠送金币"
+ },
+ "x-enum-varnames": [
+ "TranTypeCoinRecharge",
+ "TranTypeVipGiftCoin",
+ "TranTypeDarkAlley",
+ "TranTypeVideoUnlock",
+ "TranTypeAdminAdjust"
+ ]
+ },
+ "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"
+ }
+ },
+ "bottomBanner": {
+ "description": "底部banner",
+ "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"
+ }
+ },
+ "downList": {
+ "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"
+ }
+ },
+ "nineGridList": {
+ "description": "9宫格",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "pauseyList": {
+ "description": "暂停广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "playList": {
+ "description": "播放强制跳转",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "titleList": {
+ "description": "标题广告",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ },
+ "videoInfoList": {
+ "description": "视频详情",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proxy.AdvitiseInfo"
+ }
+ }
+ }
+ },
+ "proto.AnnouncementItem": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "id": {
+ "description": "公告ID",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ }
+ }
+ },
+ "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.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"
+ }
+ ]
+ },
+ "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"
+ }
+ }
+ },
+ "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.CoinTransListItemResp": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "本次交易金币数,始终为正数",
+ "type": "integer"
+ },
+ "balanceAfter": {
+ "description": "交易后余额",
+ "type": "integer"
+ },
+ "balanceBefore": {
+ "description": "交易前余额",
+ "type": "integer"
+ },
+ "changeAmount": {
+ "description": "带方向的金币变动数,收入为正数,支出为负数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "交易时间,格式:2006-01-02 15:04:05",
+ "type": "string"
+ },
+ "flowDir": {
+ "description": "收支方向:1-收入,-1-支出",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.FlowDir"
+ }
+ ]
+ },
+ "flowDirName": {
+ "description": "收支方向名称:收入/支出",
+ "type": "string"
+ },
+ "id": {
+ "description": "流水ID",
+ "type": "integer"
+ },
+ "orderNo": {
+ "description": "关联订单号",
+ "type": "string"
+ },
+ "relatedId": {
+ "description": "关联内容ID",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题,优先使用备注,备注为空时使用交易类型名称",
+ "type": "string"
+ },
+ "tranType": {
+ "description": "交易类型 1-金币充值(购买金币)2-购买会员赠送金币 3-历史遗留 4-视频解锁 5-管理员手动调整",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.TranType"
+ }
+ ]
+ },
+ "tranTypeName": {
+ "description": "交易类型名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommentInfo": {
+ "type": "object",
+ "properties": {
+ "children": {
+ "description": "二级评论列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ },
+ "commentCount": {
+ "description": "二级评论数量",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "评论点赞数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "当前用户点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ },
+ "level": {
+ "description": "评论级别 1-一级评论 2-二级评论",
+ "type": "integer"
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "父评论ID,一级评论为0",
+ "type": "integer"
+ },
+ "replyCommentId": {
+ "description": "被回复评论ID,一级评论为0",
+ "type": "integer"
+ },
+ "replyUserName": {
+ "description": "被回复用户昵称",
+ "type": "string"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userAvatar": {
+ "description": "用户头像,已拼接图片域名",
+ "type": "string"
+ },
+ "userId": {
+ "description": "评论用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommentLikeSetReq": {
+ "type": "object",
+ "required": [
+ "commentId",
+ "isLike"
+ ],
+ "properties": {
+ "commentId": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞,false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.CommentLikeSetResp": {
+ "type": "object",
+ "properties": {
+ "commentId": {
+ "description": "评论ID",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "评论点赞数",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.CommentSendReq": {
+ "type": "object",
+ "properties": {
+ "objectId": {
+ "description": "对象ID,短剧评论传短剧ID,剧集评论传剧集ID",
+ "type": "integer"
+ },
+ "objectType": {
+ "description": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "parentsId": {
+ "description": "一级评论ID,0-发布一级评论,大于0-发布二级评论",
+ "type": "integer"
+ },
+ "replyCommentId": {
+ "description": "被回复评论ID,0时默认回复parentsId",
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ }
+ }
+ },
+ "proto.CommonData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图,已拼接图片域名",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "点赞数,包含真实点赞数和后台虚拟点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "description": {
+ "description": "SEO Description",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "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"
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态: 0下架 1上架",
+ "type": "integer"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ }
+ }
+ },
+ "proto.ConfigInfo": {
+ "type": "object",
+ "properties": {
+ "actressBackGround": {
+ "description": "女优通用背景图",
+ "type": "string"
+ },
+ "advitiesTopicNum": {
+ "description": "广告间隔",
+ "type": "integer"
+ },
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "customerServiceHost": {
+ "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"
+ },
+ "mediaAppUsCdn": {
+ "description": "视频CDN(海外app使用)",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "最新地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "最新地址(备用地址1)",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "最新地址(备用地址2)",
+ "type": "string"
+ },
+ "officialAppAddr": {
+ "description": "官网app地址",
+ "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"
+ }
+ },
+ "watermarkImage": {
+ "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"
+ }
+ }
+ },
+ "proto.ConfigInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告配置",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "config": {
+ "description": "完整系统配置(与首页等接口的 config 一致,含 potaoGroup 等其余字段)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ConfigInfo"
+ }
+ ]
+ },
+ "navPageList": {
+ "description": "导航页线路集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.NavPageItem"
+ }
+ },
+ "newAddressInfo": {
+ "description": "最新地址信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.NewAddressInfo"
+ }
+ ]
+ },
+ "reportSdkConfig": {
+ "description": "数据上报SDK域名配置",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "全站SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "seoNavInfo": {
+ "description": "导航SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoNavInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.ContactUsAddReq": {
+ "type": "object",
+ "required": [
+ "content",
+ "email",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "attachments": {
+ "description": "附件地址,可存放多张图片或文件地址",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsAddResp": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "description": "提示信息",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FeedbackReq": {
+ "type": "object",
+ "required": [
+ "content",
+ "type"
+ ],
+ "properties": {
+ "contact": {
+ "description": "联系方式,可为空",
+ "type": "string"
+ },
+ "content": {
+ "description": "描述内容,最多500字",
+ "type": "string"
+ },
+ "screenshots": {
+ "description": "问题截图,可存放多张图片地址",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "type": {
+ "description": "问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackResp": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "description": "提示信息",
+ "type": "string"
+ }
+ }
+ },
+ "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.GetCommentListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommentInfo"
+ }
+ },
+ "objectId": {
+ "description": "对象ID",
+ "type": "integer"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.HomeBannerData": {
+ "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.HomeCategoryData": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "icon": {
+ "description": "分类图标",
+ "type": "string"
+ },
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "list": {
+ "description": "分类下短剧集合,最多16条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "分类排序,越大越靠前",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HomeHtmlData": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "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.HomePlayletResponse": {
+ "type": "object",
+ "properties": {
+ "announcements": {
+ "description": "启用中的公告,来自独立缓存",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.AnnouncementItem"
+ }
+ },
+ "homeBannerData": {
+ "description": "首页banner信息列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeBannerData"
+ }
+ },
+ "homeCategoryData": {
+ "description": "启用分类短剧集合,按分类排序,每个分类最多16条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeCategoryData"
+ }
+ },
+ "homeRecommendThemes": {
+ "description": "推荐主题,最多4条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.HomeRecommendTheme"
+ }
+ },
+ "homeTodayUpdateData": {
+ "description": "最近更新集合,最多16条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(短剧列表页SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ }
+ }
+ },
+ "proto.HomeRecommendTheme": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "description": "题材图标,已拼接图片域名",
+ "type": "string"
+ },
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "playletCount": {
+ "description": "该题材绑定的上架短剧数量",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ }
+ }
+ },
+ "proto.HotSearchItem": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架shi时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "hotValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "images": {
+ "description": "封面",
+ "type": "string"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "works": {
+ "description": "作品数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.NavPageItem": {
+ "type": "object",
+ "properties": {
+ "line": {
+ "description": "线路序号",
+ "type": "integer"
+ },
+ "url": {
+ "description": "线路地址",
+ "type": "string"
+ }
+ }
+ },
+ "proto.NewAddressInfo": {
+ "type": "object",
+ "properties": {
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "customerServiceHost": {
+ "description": "在线客服",
+ "type": "string"
+ },
+ "foreverUrlAddr": {
+ "description": "永久海外地址(翻墙访问 永不迷路)",
+ "type": "string"
+ },
+ "gmailAddr": {
+ "description": "邮箱地址",
+ "type": "string"
+ },
+ "newUrlAddr": {
+ "description": "新国内地址",
+ "type": "string"
+ },
+ "newUrlAddr1": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "newUrlAddr2": {
+ "description": "备用地址1",
+ "type": "string"
+ },
+ "officialAppAddr": {
+ "description": "官网app地址",
+ "type": "string"
+ },
+ "pdfAddr": {
+ "description": "回家的路PDF",
+ "type": "string"
+ },
+ "telegram": {
+ "description": "商务tg",
+ "type": "string"
+ },
+ "tgGroup": {
+ "description": "tg群",
+ "type": "string"
+ },
+ "twitterAddr": {
+ "description": "推特地址",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PayMethodInfo": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "alipay / wechat / union",
+ "type": "string"
+ },
+ "typeName": {
+ "description": "支付宝 / 微信 / 银联",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectData": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "chapterTitle": {
+ "description": "剧集标题",
+ "type": "string"
+ },
+ "collectedAt": {
+ "description": "收藏时间",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "剧集观看数",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "剧集收藏数",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态,收藏列表固定返回1-已收藏",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "type": "integer"
+ },
+ "playletCover": {
+ "description": "短剧封面,已拼接图片域名",
+ "type": "string"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "playletTitle": {
+ "description": "短剧标题",
+ "type": "string"
+ },
+ "singleCoins": {
+ "description": "单集金币",
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "description": "短剧总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "videoUrl": {
+ "description": "播放地址",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "收藏剧集集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterCollectData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总收藏数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectSetReq": {
+ "type": "object",
+ "required": [
+ "chapterId",
+ "isCollect"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "是否收藏,true-收藏,false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterCollectSetResp": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "短剧剧集收藏数",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态 1-已收藏 2-未收藏",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterData": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "description": "当前金币余额",
+ "type": "integer"
+ },
+ "canWatch": {
+ "description": "是否可观看",
+ "type": "boolean"
+ },
+ "costCoins": {
+ "description": "本次解锁消耗金币",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isUnlocked": {
+ "description": "当前用户是否已解锁本集",
+ "type": "boolean"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "needUnlock": {
+ "description": "是否需要金币解锁",
+ "type": "boolean"
+ },
+ "needVip": {
+ "description": "是否需要开通VIP",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "singleCoins": {
+ "description": "单集金币(支付方式为1-金币时候生效)",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "视频地址",
+ "type": "string"
+ },
+ "vipValid": {
+ "description": "当前用户VIP是否有效",
+ "type": "boolean"
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterInfoResp": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "description": "当前金币余额",
+ "type": "integer"
+ },
+ "canWatch": {
+ "description": "是否可观看",
+ "type": "boolean"
+ },
+ "costCoins": {
+ "description": "本次解锁消耗金币",
+ "type": "integer"
+ },
+ "countBrowse": {
+ "description": "短剧剧集观看数",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "短剧剧集收藏数",
+ "type": "integer"
+ },
+ "countComment": {
+ "description": "短剧剧集评论数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "短剧剧集点赞数",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "第几集",
+ "type": "integer"
+ },
+ "hash": {
+ "description": "媒体hash",
+ "type": "string"
+ },
+ "height": {
+ "description": "高",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态 1-已收藏 2-未收藏",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ },
+ "isUnlocked": {
+ "description": "当前用户是否已解锁本集",
+ "type": "boolean"
+ },
+ "md5": {
+ "description": "媒体md5",
+ "type": "string"
+ },
+ "mediaSize": {
+ "description": "资源大小",
+ "type": "integer"
+ },
+ "mediaTime": {
+ "description": "影片长度(秒)",
+ "type": "integer"
+ },
+ "needUnlock": {
+ "description": "是否需要金币解锁",
+ "type": "boolean"
+ },
+ "needVip": {
+ "description": "是否需要开通VIP",
+ "type": "boolean"
+ },
+ "payType": {
+ "description": "支付方式 0-免费 1-金币 2-VIP",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "ratio": {
+ "description": "宽高比",
+ "type": "number"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "singleCoins": {
+ "description": "单集金币(支付方式为1-金币时候生效)",
+ "type": "integer"
+ },
+ "title": {
+ "description": "章节名",
+ "type": "string"
+ },
+ "videoUrl": {
+ "description": "视频地址",
+ "type": "string"
+ },
+ "vipValid": {
+ "description": "当前用户VIP是否有效",
+ "type": "boolean"
+ },
+ "width": {
+ "description": "宽",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterLikeSetReq": {
+ "type": "object",
+ "required": [
+ "chapterId",
+ "isLike"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞,false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterLikeSetResp": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "短剧剧集点赞数",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧剧集集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterData"
+ }
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterUnlockReq": {
+ "type": "object",
+ "required": [
+ "chapterId",
+ "playletId"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterUnlockResp": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "description": "当前金币余额",
+ "type": "integer"
+ },
+ "canWatch": {
+ "description": "是否可观看",
+ "type": "boolean"
+ },
+ "changeCoin": {
+ "description": "本次实际扣减金币,幂等命中时为0",
+ "type": "integer"
+ },
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "costCoins": {
+ "description": "本次解锁消耗金币",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "unlocked": {
+ "description": "是否已解锁",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletChapterWatchReportReq": {
+ "type": "object",
+ "required": [
+ "chapterId"
+ ],
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID,传入时用于校验剧集归属",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterWatchReportResp": {
+ "type": "object",
+ "properties": {
+ "chapterId": {
+ "description": "短剧剧集ID",
+ "type": "integer"
+ },
+ "currentEpisode": {
+ "description": "当前集数",
+ "type": "integer"
+ },
+ "lastChapterId": {
+ "description": "最近观看剧集ID",
+ "type": "integer"
+ },
+ "lastEpisode": {
+ "description": "最近观看集数",
+ "type": "integer"
+ },
+ "playletId": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "unwatchedCount": {
+ "description": "当前用户未看集数",
+ "type": "integer"
+ },
+ "watchedCount": {
+ "description": "当前用户已看集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCollectData": {
+ "type": "object",
+ "properties": {
+ "collectedAt": {
+ "description": "收藏时间",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态,收藏列表固定返回1-已收藏",
+ "type": "integer"
+ },
+ "lastChapterId": {
+ "description": "最近观看剧集ID,无观看进度时为0",
+ "type": "integer"
+ },
+ "lastEpisode": {
+ "description": "最近观看集数,无观看进度时为0",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "unwatchedCount": {
+ "description": "当前用户未看集数,按totalEpisode-watchedCount计算",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "watchedCount": {
+ "description": "当前用户已看集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCollectListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "收藏短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletCollectData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总收藏数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletCollectSetReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isCollect"
+ ],
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "是否收藏,true-收藏,false-取消收藏",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletCollectSetResp": {
+ "type": "object",
+ "properties": {
+ "countCollect": {
+ "description": "短剧收藏数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "收藏状态 1-已收藏 2-未收藏",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletForwardHeatReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletForwardHeatResp": {
+ "type": "object",
+ "properties": {
+ "heatValue": {
+ "description": "加热后的热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHistoryClearResp": {
+ "type": "object",
+ "properties": {
+ "deletedCount": {
+ "description": "删除记录数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHistoryData": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图,已拼接图片域名",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "点赞数,包含真实点赞数和后台虚拟点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "description": {
+ "description": "SEO Description",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "historyAt": {
+ "description": "最近观看时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isFinished": {
+ "description": "是否已看完",
+ "type": "boolean"
+ },
+ "isNew": {
+ "description": "是否上新,本周内上架为true",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "description": "是否推荐 true-推荐,false-不推荐",
+ "type": "boolean"
+ },
+ "lastChapterId": {
+ "description": "最近观看剧集ID",
+ "type": "integer"
+ },
+ "lastEpisode": {
+ "description": "最近观看集数",
+ "type": "integer"
+ },
+ "mediaCreatedAt": {
+ "description": "媒资中心创建时间",
+ "type": "string"
+ },
+ "mediaUpdatedAt": {
+ "description": "媒资中心更新时间",
+ "type": "string"
+ },
+ "onlineAt": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态: 0下架 1上架",
+ "type": "integer"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "unwatchedCount": {
+ "description": "当前用户未看集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ },
+ "watchedCount": {
+ "description": "当前用户已看集数",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHistoryListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "观看历史集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletHistoryData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总历史数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletHotLikeResp": {
+ "type": "object",
+ "properties": {
+ "guessLike": {
+ "description": "按收藏量最多的15部上架短剧",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "hotSearch": {
+ "description": "前10个热搜词",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletHotSearchItem"
+ }
+ }
+ }
+ },
+ "proto.PlayletHotSearchItem": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "description": "搜索次数",
+ "type": "integer"
+ },
+ "keyword": {
+ "description": "搜索词",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletInfoResp": {
+ "type": "object",
+ "properties": {
+ "anchor": {
+ "description": "作者/演员",
+ "type": "string"
+ },
+ "banner": {
+ "description": "banner图,已拼接图片域名",
+ "type": "string"
+ },
+ "countBrowse": {
+ "description": "观看量",
+ "type": "integer"
+ },
+ "countCollect": {
+ "description": "收藏数",
+ "type": "integer"
+ },
+ "countLike": {
+ "description": "点赞数,包含真实点赞数和后台虚拟点赞数",
+ "type": "integer"
+ },
+ "cover": {
+ "description": "封面,已拼接图片域名",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "简介",
+ "type": "string"
+ },
+ "description": {
+ "description": "SEO Description",
+ "type": "string"
+ },
+ "heatValue": {
+ "description": "热度值",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧id",
+ "type": "integer"
+ },
+ "isCollect": {
+ "description": "是否收藏(1-已收藏,2-未收藏)",
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isLike": {
+ "description": "是否点赞(1-已点赞,2-未点赞)",
+ "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"
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "playName": {
+ "description": "剧名",
+ "type": "string"
+ },
+ "playletChapterInfo": {
+ "description": "短剧章节集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletChapterData"
+ }
+ },
+ "relatedRecommendData": {
+ "description": "相关推荐集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "seoInfo": {
+ "description": "页面SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "showOnBanner": {
+ "description": "是否显示在首页banner",
+ "type": "boolean"
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "状态: 0下架 1上架",
+ "type": "integer"
+ },
+ "tagIds": {
+ "description": "标签ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagNames": {
+ "description": "标签名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tagSlugs": {
+ "description": "标签slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeIds": {
+ "description": "题材ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeNames": {
+ "description": "题材名称集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "themeSlugs": {
+ "description": "题材slug集合",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "totalEpisode": {
+ "description": "总集数",
+ "type": "integer"
+ },
+ "updateStatus": {
+ "description": "更新状态 1-连载中 2-已完结",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "修改时间",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletLikeSetReq": {
+ "type": "object",
+ "required": [
+ "id",
+ "isLike"
+ ],
+ "properties": {
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "是否点赞,true-点赞,false-取消点赞",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletLikeSetResp": {
+ "type": "object",
+ "properties": {
+ "countLike": {
+ "description": "短剧点赞数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "短剧ID",
+ "type": "integer"
+ },
+ "isLike": {
+ "description": "点赞状态 1-已点赞 2-未点赞",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletOperateListResp": {
+ "type": "object",
+ "properties": {
+ "filterType": {
+ "description": "筛选类型 1-最近更新 2-当前热播 3-独家原创",
+ "type": "integer"
+ },
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "operateType": {
+ "description": "运营类型 1-短剧 2-漫剧",
+ "type": "integer"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRankResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(短剧列表页SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletRecommendListResp": {
+ "type": "object",
+ "properties": {
+ "categoryDesc": {
+ "description": "分类描述",
+ "type": "string"
+ },
+ "categoryId": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "categoryName": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "categorySlug": {
+ "description": "分类slug",
+ "type": "string"
+ },
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(分类SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletSearchResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletTagVideoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(标签SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeItem": {
+ "type": "object",
+ "properties": {
+ "icon": {
+ "description": "题材图标,已拼接图片域名",
+ "type": "string"
+ },
+ "id": {
+ "description": "题材ID",
+ "type": "integer"
+ },
+ "isRecommend": {
+ "description": "是否推荐",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "题材名称",
+ "type": "string"
+ },
+ "playletCount": {
+ "description": "该题材绑定的上架短剧数量",
+ "type": "integer"
+ },
+ "rank": {
+ "description": "排序,越大越靠前",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "题材对应页面 SEO 信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "slug": {
+ "description": "slug",
+ "type": "string"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeListResponse": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "题材列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PlayletThemeItem"
+ }
+ }
+ }
+ },
+ "proto.PlayletTodayUpdateResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息(短剧列表页SEO)",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletVideoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "短剧集合",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.CommonData"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "页面SEO信息:有 themeId 时返回题材 SEO,否则返回短剧列表页 SEO",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.RechargePackageItem": {
+ "type": "object",
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数",
+ "type": "integer"
+ },
+ "durationDays": {
+ "description": "会员有效天数",
+ "type": "integer"
+ },
+ "durationHours": {
+ "description": "会员有效小时数,由durationDays计算",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "payMethods": {
+ "description": "该套餐支持的支付方式",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.PayMethodInfo"
+ }
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "sort": {
+ "description": "排序,越小越靠前",
+ "type": "integer"
+ },
+ "subTitle": {
+ "description": "展示副标题",
+ "type": "string"
+ },
+ "totalCoin": {
+ "description": "到账总金币数,由baseCoin+giftCoin计算",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.RechargePackageListResp": {
+ "type": "object",
+ "properties": {
+ "coinList": {
+ "description": "金币充值列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.RechargePackageItem"
+ }
+ },
+ "vipList": {
+ "description": "会员畅享列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.RechargePackageItem"
+ }
+ }
+ }
+ },
+ "proto.SeoInfo": {
+ "type": "object",
+ "properties": {
+ "canonicalUrl": {
+ "description": "当前内容的标准URL",
+ "type": "string"
+ },
+ "description": {
+ "description": "页面描述",
+ "type": "string"
+ },
+ "keywords": {
+ "description": "页面关键词",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "页面标题",
+ "type": "string"
+ },
+ "webSiteName": {
+ "description": "网站名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.SeoNavInfo": {
+ "type": "object",
+ "properties": {
+ "dhDescription": {
+ "description": "导航描述",
+ "type": "string"
+ },
+ "dhKeyworld": {
+ "description": "关键词",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dhSiteName": {
+ "description": "导航名称",
+ "type": "string"
+ },
+ "dhTitle": {
+ "description": "导航标题",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TranslationCategory": {
+ "type": "object",
+ "properties": {
+ "categoryType": {
+ "description": "导航类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.TranslationType"
+ }
+ ]
+ },
+ "icon": {
+ "description": "Icon",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "translationList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.TranslationInfo"
+ }
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.TranslationInfo": {
+ "type": "object",
+ "properties": {
+ "isFriendAds": {
+ "description": "是否是友链广告位",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "urlValue": {
+ "description": "URl路径",
+ "type": "string"
+ }
+ }
+ },
+ "proto.VideoData": {
+ "type": "object",
+ "properties": {
+ "cover": {
+ "description": "视频封面",
+ "type": "string"
+ },
+ "fileId": {
+ "description": "媒资库ID",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "视频URl",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoInfoResp": {
+ "type": "object",
+ "properties": {
+ "advitise": {
+ "description": "广告",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.AdvitiseData"
+ }
+ ]
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "recommendVideos": {
+ "description": "相关推荐,按当前视频标签匹配并按上架时间倒序返回最多20条",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ },
+ "seoInfo": {
+ "description": "页面SEO信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.SeoInfo"
+ }
+ ]
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "tagsName": {
+ "description": "标签名称",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "video": {
+ "description": "视频",
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.VideoData"
+ }
+ ]
+ },
+ "watchTimes": {
+ "description": "观看数(真假之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoItemResp": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "上架时间",
+ "type": "string"
+ },
+ "coverImg": {
+ "description": "封面",
+ "type": "string"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "id": {
+ "description": "id",
+ "type": "integer"
+ },
+ "keyworld": {
+ "description": "关键字",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "观看数(真假之和)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.VideoResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "视频列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.VideoItemResp"
+ }
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "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.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeEmail": "邮箱登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeAccount",
+ "LoginTypeEmail"
+ ]
+ },
+ "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"
+ ]
+ },
+ "pubdef.UserType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 0
+ ],
+ "x-enum-comments": {
+ "UserTypeGuest": "游客用户",
+ "UserTypeNormal": "普通用户(已注册)"
+ },
+ "x-enum-varnames": [
+ "UserTypeGuest",
+ "UserTypeNormal",
+ "UserTypeNone"
+ ]
+ },
+ "pubdef.VipOrderStatus": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "VipOrderFailed": "支付失败",
+ "VipOrderPending": "待支付",
+ "VipOrderSuccess": "支付成功"
+ },
+ "x-enum-varnames": [
+ "VipOrderPending",
+ "VipOrderSuccess",
+ "VipOrderFailed"
+ ]
+ },
+ "pubdef.VipType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "VipTypeMonth": "月卡",
+ "VipTypeNone": "没卡",
+ "VipTypeYear": "年卡"
+ },
+ "x-enum-varnames": [
+ "VipTypeNone",
+ "VipTypeMonth",
+ "VipTypeYear"
+ ]
+ },
+ "rechargeapi.submitOrderReq": {
+ "type": "object",
+ "required": [
+ "packageId",
+ "payMode"
+ ],
+ "properties": {
+ "packageId": {
+ "description": "购买套餐ID(会员畅享)",
+ "type": "integer"
+ },
+ "payMode": {
+ "description": "支付方式:alipay/wechat",
+ "type": "string"
+ }
+ }
+ },
+ "rechargepackagemod.PackageType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PackageTypeCoin": "金币充值",
+ "PackageTypeVip": "会员畅享"
+ },
+ "x-enum-varnames": [
+ "PackageTypeVip",
+ "PackageTypeCoin"
+ ]
+ },
+ "rechargeser.SubmitOrderResp": {
+ "type": "object",
+ "properties": {
+ "isOpenNewBrowser": {
+ "description": "是否需要外部浏览器打开",
+ "type": "boolean"
+ },
+ "payUrl": {
+ "description": "支付链接",
+ "type": "string"
+ },
+ "payUrlMode": {
+ "description": "链接打开方式",
+ "type": "string"
+ },
+ "tradeNo": {
+ "description": "本地订单号(可用于查询)",
+ "type": "string"
+ }
+ }
+ },
+ "response.BasePageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.AiSellMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": "integer"
+ },
+ "appID": {
+ "type": "integer"
+ },
+ "currencyType": {
+ "type": "string"
+ },
+ "devType": {
+ "type": "string"
+ },
+ "isRepurchase": {
+ "type": "string"
+ },
+ "sysType": {
+ "type": "string"
+ },
+ "tranCreatedAt": {
+ "type": "string"
+ },
+ "tranType": {
+ "type": "string"
+ },
+ "uid": {
+ "type": "integer"
+ },
+ "uniqID": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.BuyVIPType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "BuyVIPTypeGold": "金币",
+ "BuyVIPTypeMoney": "现金"
+ },
+ "x-enum-varnames": [
+ "BuyVIPTypeMoney",
+ "BuyVIPTypeGold"
+ ]
+ },
+ "statcenter.CardSellMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": "integer"
+ },
+ "appID": {
+ "type": "integer"
+ },
+ "currencyType": {
+ "description": "0金币 1现金",
+ "type": "integer"
+ },
+ "product": {
+ "$ref": "#/definitions/statcenter.Product"
+ },
+ "sysType": {
+ "type": "string"
+ },
+ "tranCreatedAt": {
+ "type": "string"
+ },
+ "tranType": {
+ "type": "string"
+ },
+ "tranTypeInt": {
+ "type": "integer"
+ },
+ "uid": {
+ "type": "integer"
+ },
+ "uniqID": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.ConsumeRecordMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "金币",
+ "type": "number"
+ },
+ "appID": {
+ "type": "integer"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "currencyType": {
+ "description": "0金币 1人民币",
+ "type": "integer"
+ },
+ "money": {
+ "description": "人民币 元",
+ "type": "number"
+ },
+ "type": {
+ "description": "消费类型",
+ "type": "integer"
+ },
+ "uid": {
+ "type": "integer"
+ },
+ "uniq": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.Product": {
+ "type": "object",
+ "properties": {
+ "discountedPrice": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "statcenter.StatcenterSyncReq": {
+ "type": "object",
+ "properties": {
+ "job": {
+ "description": "同步任务类型",
+ "type": "string"
+ },
+ "maxSize": {
+ "description": "最大条数",
+ "type": "integer"
+ },
+ "platformId": {
+ "description": "原始平台Id(访问/流水)",
+ "type": "string"
+ },
+ "successTime": {
+ "description": "游标时间(购买会员、提现、充值)",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户(注册)",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.StatcenterSyncResp": {
+ "type": "object",
+ "properties": {
+ "accessList": {
+ "description": "日活记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserVisitMsg"
+ }
+ },
+ "aiSellList": {
+ "description": "AI销售流水",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.AiSellMsg"
+ }
+ },
+ "allRechargeList": {
+ "description": "全部支付订单",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserRechargeMsg"
+ }
+ },
+ "bindingList": {
+ "description": "用户绑定记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserBindingMsg"
+ }
+ },
+ "buyVipList": {
+ "description": "VIP购买记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserBuyVIPMsg"
+ }
+ },
+ "cardSellList": {
+ "description": "会员卡销售流水",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.CardSellMsg"
+ }
+ },
+ "code": {
+ "description": "200正常 其他异常",
+ "type": "integer"
+ },
+ "consumeList": {
+ "description": "消费流水",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.ConsumeRecordMsg"
+ }
+ },
+ "inviteList": {
+ "description": "邀请绑定",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserInviteBindMsg"
+ }
+ },
+ "job": {
+ "description": "类型",
+ "type": "string"
+ },
+ "msg": {
+ "description": "错误消息",
+ "type": "string"
+ },
+ "rechargeList": {
+ "description": "充值记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserRechargeMsg"
+ }
+ },
+ "registerList": {
+ "description": "注册记录",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserRegisterMsg"
+ }
+ },
+ "visitList": {
+ "description": "日活记录(旧字段,兼容)",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/statcenter.UserVisitMsg"
+ }
+ }
+ }
+ },
+ "statcenter.UserBindingMsg": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appID",
+ "type": "integer"
+ },
+ "bindingTime": {
+ "description": "绑定时间",
+ "type": "string"
+ },
+ "devType": {
+ "description": "设备类型",
+ "type": "string"
+ },
+ "mobile": {
+ "description": "手机号/绑定账号(本项目填邮箱)",
+ "type": "string"
+ },
+ "platformId": {
+ "description": "原始平台流水Id",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "操作系统类型",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserBuyVIPMsg": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": "integer"
+ },
+ "appId": {
+ "type": "integer"
+ },
+ "buyVIPType": {
+ "$ref": "#/definitions/statcenter.BuyVIPType"
+ },
+ "deviceType": {
+ "type": "string"
+ },
+ "districtCode": {
+ "type": "string"
+ },
+ "isDirect": {
+ "type": "boolean"
+ },
+ "money": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "oid": {
+ "type": "string"
+ },
+ "platformId": {
+ "type": "string"
+ },
+ "promCode": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer"
+ },
+ "successTime": {
+ "type": "string"
+ },
+ "uid": {
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserInviteBindMsg": {
+ "type": "object",
+ "required": [
+ "appId",
+ "inviteTime",
+ "userId"
+ ],
+ "properties": {
+ "appId": {
+ "type": "integer"
+ },
+ "inviteTime": {
+ "type": "string"
+ },
+ "parentPromCode": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserRechargeMsg": {
+ "type": "object",
+ "required": [
+ "appId",
+ "money",
+ "orderId",
+ "payMoney",
+ "sysType",
+ "userId"
+ ],
+ "properties": {
+ "appId": {
+ "type": "integer"
+ },
+ "chanShareMod": {
+ "type": "integer"
+ },
+ "channelName": {
+ "type": "string"
+ },
+ "cid": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "devType": {
+ "type": "string"
+ },
+ "districtCode": {
+ "type": "string"
+ },
+ "isNewOrOldUser": {
+ "type": "boolean"
+ },
+ "money": {
+ "type": "integer"
+ },
+ "oid": {
+ "type": "string"
+ },
+ "orderId": {
+ "type": "string"
+ },
+ "payMoney": {
+ "type": "integer"
+ },
+ "paymentAt": {
+ "type": "string"
+ },
+ "platformId": {
+ "type": "string"
+ },
+ "productType": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "string"
+ },
+ "registerTime": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer"
+ },
+ "successAt": {
+ "type": "string"
+ },
+ "sysType": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserRegisterMsg": {
+ "type": "object",
+ "required": [
+ "appId",
+ "isDirect",
+ "registerTime",
+ "sysType",
+ "userId"
+ ],
+ "properties": {
+ "appId": {
+ "description": "appID",
+ "type": "integer"
+ },
+ "devType": {
+ "description": "设备类型",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "ip": {
+ "description": "IP",
+ "type": "string"
+ },
+ "isDirect": {
+ "description": "true:是直推用户",
+ "type": "boolean"
+ },
+ "mobile": {
+ "description": "手机号",
+ "type": "string"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "platformId": {
+ "description": "原始平台流水Id",
+ "type": "string"
+ },
+ "promCode": {
+ "description": "推广序列",
+ "type": "string"
+ },
+ "promSeqe": {
+ "description": "全名代理",
+ "type": "string"
+ },
+ "puc": {
+ "description": "应用中心推广",
+ "type": "string"
+ },
+ "registerTime": {
+ "description": "注册时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "操作系统类型 安卓 IOS",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "statcenter.UserVisitMsg": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appID",
+ "type": "integer"
+ },
+ "devID": {
+ "description": "设备ID",
+ "type": "string"
+ },
+ "devType": {
+ "description": "设备型号",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "ip": {
+ "description": "IP",
+ "type": "string"
+ },
+ "isDirect": {
+ "description": "true:是直推用户",
+ "type": "boolean"
+ },
+ "platformId": {
+ "description": "原始平台流水Id",
+ "type": "string"
+ },
+ "registerTime": {
+ "description": "用户注册时间",
+ "type": "string"
+ },
+ "sysType": {
+ "description": "操作系统类型 安卓 IOS",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "version": {
+ "description": "APP版本号",
+ "type": "string"
+ },
+ "visitAt": {
+ "description": "访问时间",
+ "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"
+ }
+ }
+ },
+ "vipordermod.OrderType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "OrderTypeCoin": "金币购买",
+ "OrderTypeVip": "会员购买"
+ },
+ "x-enum-varnames": [
+ "OrderTypeVip",
+ "OrderTypeCoin"
+ ]
+ },
+ "vipordermod.VipOrder": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "支付金额(分)",
+ "type": "integer"
+ },
+ "cardId": {
+ "description": "── VIP 专用 ──",
+ "type": "integer"
+ },
+ "cardName": {
+ "description": "套餐名快照",
+ "type": "string"
+ },
+ "coin": {
+ "description": "到账金币数(快照)",
+ "type": "integer"
+ },
+ "coinCfgId": {
+ "description": "── 金币专用 ──",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "下单时用户渠道码",
+ "type": "string"
+ },
+ "finishedAt": {
+ "description": "完成时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "唯一ID",
+ "type": "integer"
+ },
+ "ipAddr": {
+ "description": "客户端 IP",
+ "type": "string"
+ },
+ "oid": {
+ "description": "第三方订单号",
+ "type": "string"
+ },
+ "orderType": {
+ "description": "订单类型:1会员 2金币",
+ "allOf": [
+ {
+ "$ref": "#/definitions/vipordermod.OrderType"
+ }
+ ]
+ },
+ "payChannel": {
+ "description": "支付渠道",
+ "type": "string"
+ },
+ "payMode": {
+ "description": "支付方式:overage/alipay/wechat",
+ "type": "string"
+ },
+ "payRate": {
+ "description": "费率",
+ "type": "string"
+ },
+ "payUrl": {
+ "description": "支付链接",
+ "type": "string"
+ },
+ "payUrlMode": {
+ "description": "链接打开方式",
+ "type": "string"
+ },
+ "status": {
+ "description": "订单状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipOrderStatus"
+ }
+ ]
+ },
+ "sysType": {
+ "description": "ios/android",
+ "type": "string"
+ },
+ "tradeNo": {
+ "description": "本地订单号",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "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..54d8154
--- /dev/null
+++ b/app/docs/swagger.yaml
@@ -0,0 +1,4587 @@
+basePath: /
+definitions:
+ authapi.loginReq:
+ properties:
+ account:
+ description: 账号或邮箱
+ type: string
+ channel:
+ description: 渠道码(可选,用户无商区码时写入)
+ type: string
+ devID:
+ description: 设备唯一标示(可选)
+ type: string
+ password:
+ description: 明文密码
+ type: string
+ required:
+ - account
+ - password
+ type: object
+ authapi.registerReq:
+ properties:
+ accountNo:
+ description: 账号
+ type: string
+ agentInviteCode:
+ description: 邀请人的邀请码(可选)
+ type: string
+ channel:
+ description: 渠道码(可选,用户无商区码时写入)
+ type: string
+ devID:
+ description: 设备唯一标示(可选)
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ password:
+ description: 明文密码
+ type: string
+ required:
+ - accountNo
+ - email
+ - password
+ type: object
+ authapi.resetPasswordReq:
+ properties:
+ accountNo:
+ description: 账号
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ newPassword:
+ description: 新密码
+ type: string
+ required:
+ - accountNo
+ - email
+ - newPassword
+ type: object
+ authapi.updateProfileReq:
+ properties:
+ avatar:
+ description: 新头像 URL(可选)
+ type: string
+ introduction:
+ description: 简介(可选)
+ type: string
+ newPassword:
+ description: 新密码明文(修改密码时必填)
+ type: string
+ nickName:
+ description: 新昵称(可选)
+ type: string
+ oldPassword:
+ description: 旧密码明文(修改密码时必填)
+ type: string
+ privacyMode:
+ description: 隐私模式:true-开启,false-关闭(可选)
+ type: boolean
+ type: object
+ authser.GuestLoginReq:
+ properties:
+ channel:
+ description: 渠道码(可选,用户无商区码时写入 DistrictCode)
+ type: string
+ devID:
+ description: 设备唯一标示(必填)
+ type: string
+ deviceId:
+ description: 备用设备ID(可选,写入 DevV2)
+ type: string
+ mac:
+ description: 设备 Mac(可选)
+ type: string
+ type: object
+ authser.LoginResp:
+ properties:
+ accountNo:
+ type: string
+ avatar:
+ description: 头像
+ type: string
+ currentVipExpire:
+ description: VIP 过期时间,客户端需结合 vipType 判断是否真正有效
+ type: string
+ email:
+ type: string
+ introduction:
+ description: 简介
+ type: string
+ inviteCode:
+ description: 邀请码
+ type: string
+ isNewUser:
+ description: 本次是否新建游客
+ type: boolean
+ isRegister:
+ description: 是否已注册
+ type: boolean
+ loginType:
+ allOf:
+ - $ref: '#/definitions/pubdef.LoginType'
+ description: 登陆方式
+ nickName:
+ type: string
+ privacyMode:
+ description: 隐私模式
+ type: boolean
+ recharge:
+ description: 金币余额
+ type: integer
+ score:
+ description: 积分
+ type: integer
+ showCardId:
+ description: 最新会员卡id
+ type: integer
+ showCardName:
+ description: 最新会员卡名称
+ type: string
+ status:
+ $ref: '#/definitions/pubdef.UserStatus'
+ token:
+ type: string
+ type:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserType'
+ description: 身份类型:0游客 1普通用户
+ userId:
+ type: integer
+ vipType:
+ $ref: '#/definitions/pubdef.VipType'
+ type: object
+ coinapi.buyCoinReq:
+ properties:
+ packageId:
+ description: 购买套餐ID(金币充值)
+ type: integer
+ payMode:
+ description: 支付方式:alipay/wechat
+ type: string
+ required:
+ - packageId
+ - payMode
+ type: object
+ coinser.BuyCoinResp:
+ properties:
+ isOpenNewBrowser:
+ type: boolean
+ payUrl:
+ type: string
+ payUrlMode:
+ type: string
+ tradeNo:
+ type: string
+ type: object
+ cointransmod.FlowDir:
+ enum:
+ - 1
+ - -1
+ type: integer
+ x-enum-comments:
+ FlowIn: 收入(充值、赠送)
+ FlowOut: 支出(消费、解锁)
+ x-enum-varnames:
+ - FlowIn
+ - FlowOut
+ cointransmod.TranType:
+ enum:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ type: integer
+ x-enum-comments:
+ TranTypeAdminAdjust: 管理员手动调整
+ TranTypeCoinRecharge: 金币充值(购买金币)
+ TranTypeDarkAlley: 历史遗留交易类型(已下线)
+ TranTypeVideoUnlock: 视频解锁
+ TranTypeVipGiftCoin: 购买会员赠送金币
+ x-enum-varnames:
+ - TranTypeCoinRecharge
+ - TranTypeVipGiftCoin
+ - TranTypeDarkAlley
+ - TranTypeVideoUnlock
+ - TranTypeAdminAdjust
+ proto.AdvitiseData:
+ properties:
+ AList:
+ description: 固定12广告位
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ BList:
+ description: 固定13广告位
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ bottomBanner:
+ description: 底部banner
+ 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
+ downList:
+ 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
+ nineGridList:
+ description: 9宫格
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ pauseyList:
+ description: 暂停广告
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ playList:
+ description: 播放强制跳转
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ titleList:
+ description: 标题广告
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ videoInfoList:
+ description: 视频详情
+ items:
+ $ref: '#/definitions/proxy.AdvitiseInfo'
+ type: array
+ type: object
+ proto.AnnouncementItem:
+ properties:
+ content:
+ description: 内容
+ type: string
+ id:
+ description: 公告ID
+ type: integer
+ title:
+ description: 标题
+ type: string
+ 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.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: 广告
+ 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
+ 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.CoinTransListItemResp:
+ properties:
+ amount:
+ description: 本次交易金币数,始终为正数
+ type: integer
+ balanceAfter:
+ description: 交易后余额
+ type: integer
+ balanceBefore:
+ description: 交易前余额
+ type: integer
+ changeAmount:
+ description: 带方向的金币变动数,收入为正数,支出为负数
+ type: integer
+ createdAt:
+ description: 交易时间,格式:2006-01-02 15:04:05
+ type: string
+ flowDir:
+ allOf:
+ - $ref: '#/definitions/cointransmod.FlowDir'
+ description: 收支方向:1-收入,-1-支出
+ flowDirName:
+ description: 收支方向名称:收入/支出
+ type: string
+ id:
+ description: 流水ID
+ type: integer
+ orderNo:
+ description: 关联订单号
+ type: string
+ relatedId:
+ description: 关联内容ID
+ type: integer
+ remark:
+ description: 备注
+ type: string
+ title:
+ description: 标题,优先使用备注,备注为空时使用交易类型名称
+ type: string
+ tranType:
+ allOf:
+ - $ref: '#/definitions/cointransmod.TranType'
+ description: 交易类型 1-金币充值(购买金币)2-购买会员赠送金币 3-历史遗留 4-视频解锁 5-管理员手动调整
+ tranTypeName:
+ description: 交易类型名称
+ type: string
+ type: object
+ proto.CommentInfo:
+ properties:
+ children:
+ description: 二级评论列表
+ items:
+ $ref: '#/definitions/proto.CommentInfo'
+ type: array
+ commentCount:
+ description: 二级评论数量
+ type: integer
+ countLike:
+ description: 评论点赞数
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ description: 评论ID
+ type: integer
+ isLike:
+ description: 当前用户点赞状态 1-已点赞 2-未点赞
+ type: integer
+ level:
+ description: 评论级别 1-一级评论 2-二级评论
+ type: integer
+ objectId:
+ description: 对象ID
+ type: integer
+ objectType:
+ description: 对象类型 10-短剧 11-短剧剧集
+ type: integer
+ parentsId:
+ description: 父评论ID,一级评论为0
+ type: integer
+ replyCommentId:
+ description: 被回复评论ID,一级评论为0
+ type: integer
+ replyUserName:
+ description: 被回复用户昵称
+ type: string
+ text:
+ description: 评论内容
+ type: string
+ userAvatar:
+ description: 用户头像,已拼接图片域名
+ type: string
+ userId:
+ description: 评论用户ID
+ type: integer
+ userName:
+ description: 用户名称
+ type: string
+ type: object
+ proto.CommentLikeSetReq:
+ properties:
+ commentId:
+ description: 评论ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞,false-取消点赞
+ type: boolean
+ required:
+ - commentId
+ - isLike
+ type: object
+ proto.CommentLikeSetResp:
+ properties:
+ commentId:
+ description: 评论ID
+ type: integer
+ countLike:
+ description: 评论点赞数
+ type: integer
+ isLike:
+ description: 点赞状态 1-已点赞 2-未点赞
+ type: integer
+ type: object
+ proto.CommentSendReq:
+ properties:
+ objectId:
+ description: 对象ID,短剧评论传短剧ID,剧集评论传剧集ID
+ type: integer
+ objectType:
+ description: 对象类型 10-短剧 11-短剧剧集
+ type: integer
+ parentsId:
+ description: 一级评论ID,0-发布一级评论,大于0-发布二级评论
+ type: integer
+ replyCommentId:
+ description: 被回复评论ID,0时默认回复parentsId
+ type: integer
+ text:
+ description: 评论内容
+ type: string
+ type: object
+ proto.CommonData:
+ properties:
+ anchor:
+ description: 作者/演员
+ type: string
+ banner:
+ description: banner图,已拼接图片域名
+ type: string
+ countBrowse:
+ description: 观看量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 点赞数,包含真实点赞数和后台虚拟点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ description:
+ description: SEO Description
+ type: string
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧id
+ type: integer
+ isFeatured:
+ description: 是否爆剧(true-是,false-否)
+ 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
+ operateType:
+ description: 运营类型 1-短剧 2-漫剧
+ type: integer
+ playName:
+ description: 剧名
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ showOnBanner:
+ description: 是否显示在首页banner
+ type: boolean
+ slug:
+ description: slug
+ type: string
+ status:
+ description: '状态: 0下架 1上架'
+ type: integer
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ tagSlugs:
+ description: 标签slug集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ themeSlugs:
+ description: 题材slug集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ type: object
+ proto.ConfigInfo:
+ properties:
+ actressBackGround:
+ description: 女优通用背景图
+ type: string
+ advitiesTopicNum:
+ description: 广告间隔
+ type: integer
+ appAddr:
+ description: APP下载地址(落地页)
+ type: string
+ channelCode:
+ description: 渠道码
+ type: string
+ customerServiceHost:
+ 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
+ mediaAppUsCdn:
+ description: 视频CDN(海外app使用)
+ type: string
+ newUrlAddr:
+ description: 最新地址
+ type: string
+ newUrlAddr1:
+ description: 最新地址(备用地址1)
+ type: string
+ newUrlAddr2:
+ description: 最新地址(备用地址2)
+ type: string
+ officialAppAddr:
+ description: 官网app地址
+ 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
+ watermarkImage:
+ 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
+ proto.ConfigInfoResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告配置
+ config:
+ allOf:
+ - $ref: '#/definitions/proto.ConfigInfo'
+ description: 完整系统配置(与首页等接口的 config 一致,含 potaoGroup 等其余字段)
+ navPageList:
+ description: 导航页线路集合
+ items:
+ $ref: '#/definitions/proto.NavPageItem'
+ type: array
+ newAddressInfo:
+ allOf:
+ - $ref: '#/definitions/proto.NewAddressInfo'
+ description: 最新地址信息
+ reportSdkConfig:
+ description: 数据上报SDK域名配置
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 全站SEO信息
+ seoNavInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoNavInfo'
+ description: 导航SEO信息
+ type: object
+ proto.ContactUsAddReq:
+ properties:
+ attachments:
+ description: 附件地址,可存放多张图片或文件地址
+ items:
+ type: string
+ type: array
+ content:
+ description: 内容
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ title:
+ description: 标题
+ type: string
+ type:
+ description: 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ type: integer
+ required:
+ - content
+ - email
+ - title
+ - type
+ type: object
+ proto.ContactUsAddResp:
+ properties:
+ message:
+ description: 提示信息
+ type: string
+ type: object
+ proto.FeedbackReq:
+ properties:
+ contact:
+ description: 联系方式,可为空
+ type: string
+ content:
+ description: 描述内容,最多500字
+ type: string
+ screenshots:
+ description: 问题截图,可存放多张图片地址
+ items:
+ type: string
+ type: array
+ type:
+ description: 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
+ type: integer
+ required:
+ - content
+ - type
+ type: object
+ proto.FeedbackResp:
+ properties:
+ message:
+ description: 提示信息
+ type: string
+ 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.GetCommentListResp:
+ properties:
+ list:
+ description: 数据
+ items:
+ $ref: '#/definitions/proto.CommentInfo'
+ type: array
+ objectId:
+ description: 对象ID
+ type: integer
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总数量
+ type: integer
+ type: object
+ proto.HomeBannerData:
+ 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.HomeCategoryData:
+ properties:
+ desc:
+ description: 描述
+ type: string
+ icon:
+ description: 分类图标
+ type: string
+ id:
+ description: 分类ID
+ type: integer
+ list:
+ description: 分类下短剧集合,最多16条
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ name:
+ description: 分类名称
+ type: string
+ rank:
+ description: 分类排序,越大越靠前
+ type: integer
+ slug:
+ description: slug
+ type: string
+ type: object
+ proto.HomeHtmlData:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ 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.HomePlayletResponse:
+ properties:
+ announcements:
+ description: 启用中的公告,来自独立缓存
+ items:
+ $ref: '#/definitions/proto.AnnouncementItem'
+ type: array
+ homeBannerData:
+ description: 首页banner信息列表
+ items:
+ $ref: '#/definitions/proto.HomeBannerData'
+ type: array
+ homeCategoryData:
+ description: 启用分类短剧集合,按分类排序,每个分类最多16条
+ items:
+ $ref: '#/definitions/proto.HomeCategoryData'
+ type: array
+ homeRecommendThemes:
+ description: 推荐主题,最多4条
+ items:
+ $ref: '#/definitions/proto.HomeRecommendTheme'
+ type: array
+ homeTodayUpdateData:
+ description: 最近更新集合,最多16条
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息(短剧列表页SEO)
+ type: object
+ proto.HomeRecommendTheme:
+ properties:
+ icon:
+ description: 题材图标,已拼接图片域名
+ type: string
+ id:
+ description: 题材ID
+ type: integer
+ name:
+ description: 题材名称
+ type: string
+ playletCount:
+ description: 该题材绑定的上架短剧数量
+ type: integer
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ slug:
+ description: slug
+ type: string
+ type: object
+ proto.HotSearchItem:
+ properties:
+ addedTime:
+ description: 上架shi时间
+ type: string
+ desc:
+ description: 描述
+ type: string
+ hotValue:
+ description: 热度值
+ type: integer
+ id:
+ description: ID
+ type: integer
+ images:
+ description: 封面
+ type: string
+ title:
+ description: 标题
+ type: string
+ works:
+ description: 作品数量
+ type: integer
+ type: object
+ proto.NavPageItem:
+ properties:
+ line:
+ description: 线路序号
+ type: integer
+ url:
+ description: 线路地址
+ type: string
+ type: object
+ proto.NewAddressInfo:
+ properties:
+ appAddr:
+ description: APP下载地址(落地页)
+ type: string
+ customerServiceHost:
+ description: 在线客服
+ type: string
+ foreverUrlAddr:
+ description: 永久海外地址(翻墙访问 永不迷路)
+ type: string
+ gmailAddr:
+ description: 邮箱地址
+ type: string
+ newUrlAddr:
+ description: 新国内地址
+ type: string
+ newUrlAddr1:
+ description: 备用地址1
+ type: string
+ newUrlAddr2:
+ description: 备用地址1
+ type: string
+ officialAppAddr:
+ description: 官网app地址
+ type: string
+ pdfAddr:
+ description: 回家的路PDF
+ type: string
+ telegram:
+ description: 商务tg
+ type: string
+ tgGroup:
+ description: tg群
+ type: string
+ twitterAddr:
+ description: 推特地址
+ type: string
+ type: object
+ proto.PayMethodInfo:
+ properties:
+ type:
+ description: alipay / wechat / union
+ type: string
+ typeName:
+ description: 支付宝 / 微信 / 银联
+ type: string
+ type: object
+ proto.PlayletChapterCollectData:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ chapterTitle:
+ description: 剧集标题
+ type: string
+ collectedAt:
+ description: 收藏时间
+ type: string
+ countBrowse:
+ description: 剧集观看数
+ type: integer
+ countCollect:
+ description: 剧集收藏数
+ type: integer
+ currentEpisode:
+ description: 第几集
+ type: integer
+ isCollect:
+ description: 收藏状态,收藏列表固定返回1-已收藏
+ type: integer
+ mediaTime:
+ description: 影片长度(秒)
+ type: integer
+ payType:
+ description: 支付方式 0-免费 1-金币 2-VIP
+ type: integer
+ playletCover:
+ description: 短剧封面,已拼接图片域名
+ type: string
+ playletId:
+ description: 短剧ID
+ type: integer
+ playletTitle:
+ description: 短剧标题
+ type: string
+ singleCoins:
+ description: 单集金币
+ type: integer
+ totalEpisode:
+ description: 短剧总集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ videoUrl:
+ description: 播放地址
+ type: string
+ type: object
+ proto.PlayletChapterCollectListResp:
+ properties:
+ list:
+ description: 收藏剧集集合
+ items:
+ $ref: '#/definitions/proto.PlayletChapterCollectData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总收藏数量
+ type: integer
+ type: object
+ proto.PlayletChapterCollectSetReq:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ isCollect:
+ description: 是否收藏,true-收藏,false-取消收藏
+ type: boolean
+ required:
+ - chapterId
+ - isCollect
+ type: object
+ proto.PlayletChapterCollectSetResp:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ countCollect:
+ description: 短剧剧集收藏数
+ type: integer
+ isCollect:
+ description: 收藏状态 1-已收藏 2-未收藏
+ type: integer
+ type: object
+ proto.PlayletChapterData:
+ properties:
+ balance:
+ description: 当前金币余额
+ type: integer
+ canWatch:
+ description: 是否可观看
+ type: boolean
+ costCoins:
+ description: 本次解锁消耗金币
+ type: integer
+ currentEpisode:
+ description: 第几集
+ type: integer
+ hash:
+ description: 媒体hash
+ type: string
+ height:
+ description: 高
+ type: integer
+ id:
+ description: 短剧剧集ID
+ type: integer
+ isUnlocked:
+ description: 当前用户是否已解锁本集
+ type: boolean
+ md5:
+ description: 媒体md5
+ type: string
+ mediaSize:
+ description: 资源大小
+ type: integer
+ mediaTime:
+ description: 影片长度(秒)
+ type: integer
+ needUnlock:
+ description: 是否需要金币解锁
+ type: boolean
+ needVip:
+ description: 是否需要开通VIP
+ type: boolean
+ payType:
+ description: 支付方式 0-免费 1-金币 2-VIP
+ type: integer
+ playletId:
+ description: 短剧ID
+ type: integer
+ ratio:
+ description: 宽高比
+ type: number
+ singleCoins:
+ description: 单集金币(支付方式为1-金币时候生效)
+ type: integer
+ title:
+ description: 章节名
+ type: string
+ videoUrl:
+ description: 视频地址
+ type: string
+ vipValid:
+ description: 当前用户VIP是否有效
+ type: boolean
+ width:
+ description: 宽
+ type: integer
+ type: object
+ proto.PlayletChapterInfoResp:
+ properties:
+ balance:
+ description: 当前金币余额
+ type: integer
+ canWatch:
+ description: 是否可观看
+ type: boolean
+ costCoins:
+ description: 本次解锁消耗金币
+ type: integer
+ countBrowse:
+ description: 短剧剧集观看数
+ type: integer
+ countCollect:
+ description: 短剧剧集收藏数
+ type: integer
+ countComment:
+ description: 短剧剧集评论数
+ type: integer
+ countLike:
+ description: 短剧剧集点赞数
+ type: integer
+ currentEpisode:
+ description: 第几集
+ type: integer
+ hash:
+ description: 媒体hash
+ type: string
+ height:
+ description: 高
+ type: integer
+ id:
+ description: 短剧剧集ID
+ type: integer
+ isCollect:
+ description: 收藏状态 1-已收藏 2-未收藏
+ type: integer
+ isLike:
+ description: 点赞状态 1-已点赞 2-未点赞
+ type: integer
+ isUnlocked:
+ description: 当前用户是否已解锁本集
+ type: boolean
+ md5:
+ description: 媒体md5
+ type: string
+ mediaSize:
+ description: 资源大小
+ type: integer
+ mediaTime:
+ description: 影片长度(秒)
+ type: integer
+ needUnlock:
+ description: 是否需要金币解锁
+ type: boolean
+ needVip:
+ description: 是否需要开通VIP
+ type: boolean
+ payType:
+ description: 支付方式 0-免费 1-金币 2-VIP
+ type: integer
+ playletId:
+ description: 短剧ID
+ type: integer
+ ratio:
+ description: 宽高比
+ type: number
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息
+ singleCoins:
+ description: 单集金币(支付方式为1-金币时候生效)
+ type: integer
+ title:
+ description: 章节名
+ type: string
+ videoUrl:
+ description: 视频地址
+ type: string
+ vipValid:
+ description: 当前用户VIP是否有效
+ type: boolean
+ width:
+ description: 宽
+ type: integer
+ type: object
+ proto.PlayletChapterLikeSetReq:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞,false-取消点赞
+ type: boolean
+ required:
+ - chapterId
+ - isLike
+ type: object
+ proto.PlayletChapterLikeSetResp:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ countLike:
+ description: 短剧剧集点赞数
+ type: integer
+ isLike:
+ description: 点赞状态 1-已点赞 2-未点赞
+ type: integer
+ type: object
+ proto.PlayletChapterListResp:
+ properties:
+ list:
+ description: 短剧剧集集合
+ items:
+ $ref: '#/definitions/proto.PlayletChapterData'
+ type: array
+ playletId:
+ description: 短剧ID
+ type: integer
+ type: object
+ proto.PlayletChapterUnlockReq:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ playletId:
+ description: 短剧ID
+ type: integer
+ required:
+ - chapterId
+ - playletId
+ type: object
+ proto.PlayletChapterUnlockResp:
+ properties:
+ balance:
+ description: 当前金币余额
+ type: integer
+ canWatch:
+ description: 是否可观看
+ type: boolean
+ changeCoin:
+ description: 本次实际扣减金币,幂等命中时为0
+ type: integer
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ costCoins:
+ description: 本次解锁消耗金币
+ type: integer
+ playletId:
+ description: 短剧ID
+ type: integer
+ unlocked:
+ description: 是否已解锁
+ type: boolean
+ type: object
+ proto.PlayletChapterWatchReportReq:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ playletId:
+ description: 短剧ID,传入时用于校验剧集归属
+ type: integer
+ required:
+ - chapterId
+ type: object
+ proto.PlayletChapterWatchReportResp:
+ properties:
+ chapterId:
+ description: 短剧剧集ID
+ type: integer
+ currentEpisode:
+ description: 当前集数
+ type: integer
+ lastChapterId:
+ description: 最近观看剧集ID
+ type: integer
+ lastEpisode:
+ description: 最近观看集数
+ type: integer
+ playletId:
+ description: 短剧ID
+ type: integer
+ unwatchedCount:
+ description: 当前用户未看集数
+ type: integer
+ watchedCount:
+ description: 当前用户已看集数
+ type: integer
+ type: object
+ proto.PlayletCollectData:
+ properties:
+ collectedAt:
+ description: 收藏时间
+ type: string
+ countBrowse:
+ description: 观看量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧ID
+ type: integer
+ isCollect:
+ description: 收藏状态,收藏列表固定返回1-已收藏
+ type: integer
+ lastChapterId:
+ description: 最近观看剧集ID,无观看进度时为0
+ type: integer
+ lastEpisode:
+ description: 最近观看集数,无观看进度时为0
+ type: integer
+ playName:
+ description: 剧名
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ slug:
+ description: slug
+ type: string
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ tagSlugs:
+ description: 标签slug集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ themeSlugs:
+ description: 题材slug集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ unwatchedCount:
+ description: 当前用户未看集数,按totalEpisode-watchedCount计算
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ watchedCount:
+ description: 当前用户已看集数
+ type: integer
+ type: object
+ proto.PlayletCollectListResp:
+ properties:
+ list:
+ description: 收藏短剧集合
+ items:
+ $ref: '#/definitions/proto.PlayletCollectData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总收藏数量
+ type: integer
+ type: object
+ proto.PlayletCollectSetReq:
+ properties:
+ id:
+ description: 短剧ID
+ type: integer
+ isCollect:
+ description: 是否收藏,true-收藏,false-取消收藏
+ type: boolean
+ required:
+ - id
+ - isCollect
+ type: object
+ proto.PlayletCollectSetResp:
+ properties:
+ countCollect:
+ description: 短剧收藏数
+ type: integer
+ id:
+ description: 短剧ID
+ type: integer
+ isCollect:
+ description: 收藏状态 1-已收藏 2-未收藏
+ type: integer
+ type: object
+ proto.PlayletForwardHeatReq:
+ properties:
+ id:
+ description: 短剧ID
+ type: integer
+ required:
+ - id
+ type: object
+ proto.PlayletForwardHeatResp:
+ properties:
+ heatValue:
+ description: 加热后的热度值
+ type: integer
+ id:
+ description: 短剧ID
+ type: integer
+ type: object
+ proto.PlayletHistoryClearResp:
+ properties:
+ deletedCount:
+ description: 删除记录数量
+ type: integer
+ type: object
+ proto.PlayletHistoryData:
+ properties:
+ anchor:
+ description: 作者/演员
+ type: string
+ banner:
+ description: banner图,已拼接图片域名
+ type: string
+ countBrowse:
+ description: 观看量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 点赞数,包含真实点赞数和后台虚拟点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ description:
+ description: SEO Description
+ type: string
+ heatValue:
+ description: 热度值
+ type: integer
+ historyAt:
+ description: 最近观看时间
+ type: string
+ id:
+ description: 短剧id
+ type: integer
+ isFeatured:
+ description: 是否爆剧(true-是,false-否)
+ type: boolean
+ isFinished:
+ description: 是否已看完
+ type: boolean
+ isNew:
+ description: 是否上新,本周内上架为true
+ type: boolean
+ isRecommend:
+ description: 是否推荐 true-推荐,false-不推荐
+ type: boolean
+ lastChapterId:
+ description: 最近观看剧集ID
+ type: integer
+ lastEpisode:
+ description: 最近观看集数
+ type: integer
+ mediaCreatedAt:
+ description: 媒资中心创建时间
+ type: string
+ mediaUpdatedAt:
+ description: 媒资中心更新时间
+ type: string
+ onlineAt:
+ description: 上架时间
+ type: string
+ operateType:
+ description: 运营类型 1-短剧 2-漫剧
+ type: integer
+ playName:
+ description: 剧名
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ showOnBanner:
+ description: 是否显示在首页banner
+ type: boolean
+ slug:
+ description: slug
+ type: string
+ status:
+ description: '状态: 0下架 1上架'
+ type: integer
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ tagSlugs:
+ description: 标签slug集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ themeSlugs:
+ description: 题材slug集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ unwatchedCount:
+ description: 当前用户未看集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ watchedCount:
+ description: 当前用户已看集数
+ type: integer
+ type: object
+ proto.PlayletHistoryListResp:
+ properties:
+ list:
+ description: 观看历史集合
+ items:
+ $ref: '#/definitions/proto.PlayletHistoryData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总历史数量
+ type: integer
+ type: object
+ proto.PlayletHotLikeResp:
+ properties:
+ guessLike:
+ description: 按收藏量最多的15部上架短剧
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ hotSearch:
+ description: 前10个热搜词
+ items:
+ $ref: '#/definitions/proto.PlayletHotSearchItem'
+ type: array
+ type: object
+ proto.PlayletHotSearchItem:
+ properties:
+ count:
+ description: 搜索次数
+ type: integer
+ keyword:
+ description: 搜索词
+ type: string
+ type: object
+ proto.PlayletInfoResp:
+ properties:
+ anchor:
+ description: 作者/演员
+ type: string
+ banner:
+ description: banner图,已拼接图片域名
+ type: string
+ countBrowse:
+ description: 观看量
+ type: integer
+ countCollect:
+ description: 收藏数
+ type: integer
+ countLike:
+ description: 点赞数,包含真实点赞数和后台虚拟点赞数
+ type: integer
+ cover:
+ description: 封面,已拼接图片域名
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 简介
+ type: string
+ description:
+ description: SEO Description
+ type: string
+ heatValue:
+ description: 热度值
+ type: integer
+ id:
+ description: 短剧id
+ type: integer
+ isCollect:
+ description: 是否收藏(1-已收藏,2-未收藏)
+ type: integer
+ isFeatured:
+ description: 是否爆剧(true-是,false-否)
+ type: boolean
+ isLike:
+ description: 是否点赞(1-已点赞,2-未点赞)
+ 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
+ operateType:
+ description: 运营类型 1-短剧 2-漫剧
+ type: integer
+ playName:
+ description: 剧名
+ type: string
+ playletChapterInfo:
+ description: 短剧章节集合
+ items:
+ $ref: '#/definitions/proto.PlayletChapterData'
+ type: array
+ relatedRecommendData:
+ description: 相关推荐集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息
+ showOnBanner:
+ description: 是否显示在首页banner
+ type: boolean
+ slug:
+ description: slug
+ type: string
+ status:
+ description: '状态: 0下架 1上架'
+ type: integer
+ tagIds:
+ description: 标签ID集合
+ items:
+ type: integer
+ type: array
+ tagNames:
+ description: 标签名称集合
+ items:
+ type: string
+ type: array
+ tagSlugs:
+ description: 标签slug集合
+ items:
+ type: string
+ type: array
+ themeIds:
+ description: 题材ID集合
+ items:
+ type: integer
+ type: array
+ themeNames:
+ description: 题材名称集合
+ items:
+ type: string
+ type: array
+ themeSlugs:
+ description: 题材slug集合
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ totalEpisode:
+ description: 总集数
+ type: integer
+ updateStatus:
+ description: 更新状态 1-连载中 2-已完结
+ type: integer
+ updatedAt:
+ description: 修改时间
+ type: string
+ type: object
+ proto.PlayletLikeSetReq:
+ properties:
+ id:
+ description: 短剧ID
+ type: integer
+ isLike:
+ description: 是否点赞,true-点赞,false-取消点赞
+ type: boolean
+ required:
+ - id
+ - isLike
+ type: object
+ proto.PlayletLikeSetResp:
+ properties:
+ countLike:
+ description: 短剧点赞数
+ type: integer
+ id:
+ description: 短剧ID
+ type: integer
+ isLike:
+ description: 点赞状态 1-已点赞 2-未点赞
+ type: integer
+ type: object
+ proto.PlayletOperateListResp:
+ properties:
+ filterType:
+ description: 筛选类型 1-最近更新 2-当前热播 3-独家原创
+ type: integer
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ operateType:
+ description: 运营类型 1-短剧 2-漫剧
+ type: integer
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletRankResp:
+ properties:
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息(短剧列表页SEO)
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletRecommendListResp:
+ properties:
+ categoryDesc:
+ description: 分类描述
+ type: string
+ categoryId:
+ description: 分类ID
+ type: integer
+ categoryName:
+ description: 分类名称
+ type: string
+ categorySlug:
+ description: 分类slug
+ type: string
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息(分类SEO)
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletSearchResp:
+ properties:
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletTagVideoResp:
+ properties:
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息(标签SEO)
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletThemeItem:
+ properties:
+ icon:
+ description: 题材图标,已拼接图片域名
+ type: string
+ id:
+ description: 题材ID
+ type: integer
+ isRecommend:
+ description: 是否推荐
+ type: boolean
+ name:
+ description: 题材名称
+ type: string
+ playletCount:
+ description: 该题材绑定的上架短剧数量
+ type: integer
+ rank:
+ description: 排序,越大越靠前
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 题材对应页面 SEO 信息
+ slug:
+ description: slug
+ type: string
+ status:
+ description: 上架状态
+ type: boolean
+ type: object
+ proto.PlayletThemeListResponse:
+ properties:
+ list:
+ description: 题材列表
+ items:
+ $ref: '#/definitions/proto.PlayletThemeItem'
+ type: array
+ type: object
+ proto.PlayletTodayUpdateResp:
+ properties:
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息(短剧列表页SEO)
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletVideoResp:
+ properties:
+ list:
+ description: 短剧集合
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息:有 themeId 时返回题材 SEO,否则返回短剧列表页 SEO
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.RechargePackageItem:
+ properties:
+ baseCoin:
+ description: 基础金币数
+ type: integer
+ durationDays:
+ description: 会员有效天数
+ type: integer
+ durationHours:
+ description: 会员有效小时数,由durationDays计算
+ type: integer
+ giftCoin:
+ description: 赠送金币数
+ type: integer
+ id:
+ description: 套餐ID
+ type: integer
+ name:
+ description: 展示标题
+ type: string
+ originalPrice:
+ description: 原价,单位分
+ type: integer
+ packageType:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.PackageType'
+ description: 套餐类型:1-会员畅享 2-金币充值
+ payMethods:
+ description: 该套餐支持的支付方式
+ items:
+ $ref: '#/definitions/proto.PayMethodInfo'
+ type: array
+ price:
+ description: 售价,单位分
+ type: integer
+ sort:
+ description: 排序,越小越靠前
+ type: integer
+ subTitle:
+ description: 展示副标题
+ type: string
+ totalCoin:
+ description: 到账总金币数,由baseCoin+giftCoin计算
+ type: integer
+ type: object
+ proto.RechargePackageListResp:
+ properties:
+ coinList:
+ description: 金币充值列表
+ items:
+ $ref: '#/definitions/proto.RechargePackageItem'
+ type: array
+ vipList:
+ description: 会员畅享列表
+ items:
+ $ref: '#/definitions/proto.RechargePackageItem'
+ type: array
+ type: object
+ proto.SeoInfo:
+ properties:
+ canonicalUrl:
+ description: 当前内容的标准URL
+ type: string
+ description:
+ description: 页面描述
+ type: string
+ keywords:
+ description: 页面关键词
+ items:
+ type: string
+ type: array
+ title:
+ description: 页面标题
+ type: string
+ webSiteName:
+ description: 网站名称
+ type: string
+ type: object
+ proto.SeoNavInfo:
+ properties:
+ dhDescription:
+ description: 导航描述
+ type: string
+ dhKeyworld:
+ description: 关键词
+ items:
+ type: string
+ type: array
+ dhSiteName:
+ description: 导航名称
+ type: string
+ dhTitle:
+ description: 导航标题
+ type: string
+ type: object
+ proto.TranslationCategory:
+ properties:
+ categoryType:
+ allOf:
+ - $ref: '#/definitions/pubdef.TranslationType'
+ description: 导航类型
+ icon:
+ description: Icon
+ type: string
+ id:
+ type: integer
+ name:
+ description: 名称
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ translationList:
+ items:
+ $ref: '#/definitions/proto.TranslationInfo'
+ type: array
+ urlValue:
+ description: URl路径
+ type: string
+ type: object
+ proto.TranslationInfo:
+ properties:
+ isFriendAds:
+ description: 是否是友链广告位
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ urlValue:
+ description: URl路径
+ type: string
+ type: object
+ proto.VideoData:
+ properties:
+ cover:
+ description: 视频封面
+ type: string
+ fileId:
+ description: 媒资库ID
+ type: string
+ height:
+ description: 分辨率高(视频才会存在)
+ type: integer
+ preMp4FileNameFs:
+ description: 预览MP4文件
+ type: string
+ taskId:
+ description: 视频ID(视频才会存在)
+ type: string
+ unencryptedPreMp4:
+ description: 不加密的预览MP4
+ type: string
+ url:
+ description: 视频URl
+ type: string
+ videoTime:
+ description: 影片长度
+ type: integer
+ width:
+ description: 分辨率宽(视频才会存在)
+ type: integer
+ type: object
+ proto.VideoInfoResp:
+ properties:
+ advitise:
+ allOf:
+ - $ref: '#/definitions/proto.AdvitiseData'
+ description: 广告
+ coverImg:
+ description: 封面
+ type: string
+ id:
+ description: id
+ type: integer
+ keyworld:
+ description: 关键字
+ items:
+ type: string
+ type: array
+ recommendVideos:
+ description: 相关推荐,按当前视频标签匹配并按上架时间倒序返回最多20条
+ items:
+ $ref: '#/definitions/proto.VideoItemResp'
+ type: array
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/proto.SeoInfo'
+ description: 页面SEO信息
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ tagsName:
+ description: 标签名称
+ items:
+ type: string
+ type: array
+ title:
+ description: 标题
+ type: string
+ video:
+ allOf:
+ - $ref: '#/definitions/proto.VideoData'
+ description: 视频
+ watchTimes:
+ description: 观看数(真假之和)
+ type: integer
+ type: object
+ proto.VideoItemResp:
+ properties:
+ addedTime:
+ description: 上架时间
+ type: string
+ coverImg:
+ description: 封面
+ type: string
+ creatorId:
+ description: 作者Id
+ type: integer
+ creatorName:
+ description: 作者名称
+ type: string
+ id:
+ description: id
+ type: integer
+ keyworld:
+ description: 关键字
+ items:
+ type: string
+ type: array
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: 标题
+ type: string
+ videoTime:
+ description: 影片长度
+ type: integer
+ watchTimes:
+ description: 观看数(真假之和)
+ type: integer
+ type: object
+ proto.VideoResp:
+ properties:
+ list:
+ description: 视频列表
+ items:
+ $ref: '#/definitions/proto.VideoItemResp'
+ type: array
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ 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.LoginType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ LoginTypeAccount: 账号登陆
+ LoginTypeDevice: 设备登陆
+ LoginTypeEmail: 邮箱登陆
+ x-enum-varnames:
+ - LoginTypeDevice
+ - LoginTypeAccount
+ - LoginTypeEmail
+ 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
+ pubdef.UserType:
+ enum:
+ - 0
+ - 1
+ - 0
+ type: integer
+ x-enum-comments:
+ UserTypeGuest: 游客用户
+ UserTypeNormal: 普通用户(已注册)
+ x-enum-varnames:
+ - UserTypeGuest
+ - UserTypeNormal
+ - UserTypeNone
+ pubdef.VipOrderStatus:
+ enum:
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ VipOrderFailed: 支付失败
+ VipOrderPending: 待支付
+ VipOrderSuccess: 支付成功
+ x-enum-varnames:
+ - VipOrderPending
+ - VipOrderSuccess
+ - VipOrderFailed
+ pubdef.VipType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ VipTypeMonth: 月卡
+ VipTypeNone: 没卡
+ VipTypeYear: 年卡
+ x-enum-varnames:
+ - VipTypeNone
+ - VipTypeMonth
+ - VipTypeYear
+ rechargeapi.submitOrderReq:
+ properties:
+ packageId:
+ description: 购买套餐ID(会员畅享)
+ type: integer
+ payMode:
+ description: 支付方式:alipay/wechat
+ type: string
+ required:
+ - packageId
+ - payMode
+ type: object
+ rechargepackagemod.PackageType:
+ enum:
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ PackageTypeCoin: 金币充值
+ PackageTypeVip: 会员畅享
+ x-enum-varnames:
+ - PackageTypeVip
+ - PackageTypeCoin
+ rechargeser.SubmitOrderResp:
+ properties:
+ isOpenNewBrowser:
+ description: 是否需要外部浏览器打开
+ type: boolean
+ payUrl:
+ description: 支付链接
+ type: string
+ payUrlMode:
+ description: 链接打开方式
+ type: string
+ tradeNo:
+ description: 本地订单号(可用于查询)
+ type: string
+ type: object
+ response.BasePageResp:
+ properties:
+ list:
+ description: 集合列表
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ statcenter.AiSellMsg:
+ properties:
+ amount:
+ type: integer
+ appID:
+ type: integer
+ currencyType:
+ type: string
+ devType:
+ type: string
+ isRepurchase:
+ type: string
+ sysType:
+ type: string
+ tranCreatedAt:
+ type: string
+ tranType:
+ type: string
+ uid:
+ type: integer
+ uniqID:
+ type: string
+ type: object
+ statcenter.BuyVIPType:
+ enum:
+ - 0
+ - 1
+ type: integer
+ x-enum-comments:
+ BuyVIPTypeGold: 金币
+ BuyVIPTypeMoney: 现金
+ x-enum-varnames:
+ - BuyVIPTypeMoney
+ - BuyVIPTypeGold
+ statcenter.CardSellMsg:
+ properties:
+ amount:
+ type: integer
+ appID:
+ type: integer
+ currencyType:
+ description: 0金币 1现金
+ type: integer
+ product:
+ $ref: '#/definitions/statcenter.Product'
+ sysType:
+ type: string
+ tranCreatedAt:
+ type: string
+ tranType:
+ type: string
+ tranTypeInt:
+ type: integer
+ uid:
+ type: integer
+ uniqID:
+ type: string
+ type: object
+ statcenter.ConsumeRecordMsg:
+ properties:
+ amount:
+ description: 金币
+ type: number
+ appID:
+ type: integer
+ createdAt:
+ type: string
+ currencyType:
+ description: 0金币 1人民币
+ type: integer
+ money:
+ description: 人民币 元
+ type: number
+ type:
+ description: 消费类型
+ type: integer
+ uid:
+ type: integer
+ uniq:
+ type: string
+ type: object
+ statcenter.Product:
+ properties:
+ discountedPrice:
+ type: integer
+ id:
+ type: string
+ name:
+ type: string
+ type: object
+ statcenter.StatcenterSyncReq:
+ properties:
+ job:
+ description: 同步任务类型
+ type: string
+ maxSize:
+ description: 最大条数
+ type: integer
+ platformId:
+ description: 原始平台Id(访问/流水)
+ type: string
+ successTime:
+ description: 游标时间(购买会员、提现、充值)
+ type: string
+ userId:
+ description: 用户(注册)
+ type: integer
+ type: object
+ statcenter.StatcenterSyncResp:
+ properties:
+ accessList:
+ description: 日活记录
+ items:
+ $ref: '#/definitions/statcenter.UserVisitMsg'
+ type: array
+ aiSellList:
+ description: AI销售流水
+ items:
+ $ref: '#/definitions/statcenter.AiSellMsg'
+ type: array
+ allRechargeList:
+ description: 全部支付订单
+ items:
+ $ref: '#/definitions/statcenter.UserRechargeMsg'
+ type: array
+ bindingList:
+ description: 用户绑定记录
+ items:
+ $ref: '#/definitions/statcenter.UserBindingMsg'
+ type: array
+ buyVipList:
+ description: VIP购买记录
+ items:
+ $ref: '#/definitions/statcenter.UserBuyVIPMsg'
+ type: array
+ cardSellList:
+ description: 会员卡销售流水
+ items:
+ $ref: '#/definitions/statcenter.CardSellMsg'
+ type: array
+ code:
+ description: 200正常 其他异常
+ type: integer
+ consumeList:
+ description: 消费流水
+ items:
+ $ref: '#/definitions/statcenter.ConsumeRecordMsg'
+ type: array
+ inviteList:
+ description: 邀请绑定
+ items:
+ $ref: '#/definitions/statcenter.UserInviteBindMsg'
+ type: array
+ job:
+ description: 类型
+ type: string
+ msg:
+ description: 错误消息
+ type: string
+ rechargeList:
+ description: 充值记录
+ items:
+ $ref: '#/definitions/statcenter.UserRechargeMsg'
+ type: array
+ registerList:
+ description: 注册记录
+ items:
+ $ref: '#/definitions/statcenter.UserRegisterMsg'
+ type: array
+ visitList:
+ description: 日活记录(旧字段,兼容)
+ items:
+ $ref: '#/definitions/statcenter.UserVisitMsg'
+ type: array
+ type: object
+ statcenter.UserBindingMsg:
+ properties:
+ appId:
+ description: appID
+ type: integer
+ bindingTime:
+ description: 绑定时间
+ type: string
+ devType:
+ description: 设备类型
+ type: string
+ mobile:
+ description: 手机号/绑定账号(本项目填邮箱)
+ type: string
+ platformId:
+ description: 原始平台流水Id
+ type: string
+ sysType:
+ description: 操作系统类型
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ type: object
+ statcenter.UserBuyVIPMsg:
+ properties:
+ amount:
+ type: integer
+ appId:
+ type: integer
+ buyVIPType:
+ $ref: '#/definitions/statcenter.BuyVIPType'
+ deviceType:
+ type: string
+ districtCode:
+ type: string
+ isDirect:
+ type: boolean
+ money:
+ type: integer
+ name:
+ type: string
+ oid:
+ type: string
+ platformId:
+ type: string
+ promCode:
+ type: string
+ status:
+ type: integer
+ successTime:
+ type: string
+ uid:
+ type: integer
+ type: object
+ statcenter.UserInviteBindMsg:
+ properties:
+ appId:
+ type: integer
+ inviteTime:
+ type: string
+ parentPromCode:
+ type: string
+ userId:
+ type: integer
+ required:
+ - appId
+ - inviteTime
+ - userId
+ type: object
+ statcenter.UserRechargeMsg:
+ properties:
+ appId:
+ type: integer
+ chanShareMod:
+ type: integer
+ channelName:
+ type: string
+ cid:
+ type: string
+ createdAt:
+ type: string
+ devType:
+ type: string
+ districtCode:
+ type: string
+ isNewOrOldUser:
+ type: boolean
+ money:
+ type: integer
+ oid:
+ type: string
+ orderId:
+ type: string
+ payMoney:
+ type: integer
+ paymentAt:
+ type: string
+ platformId:
+ type: string
+ productType:
+ type: integer
+ rate:
+ type: string
+ registerTime:
+ type: string
+ status:
+ type: integer
+ successAt:
+ type: string
+ sysType:
+ type: string
+ type:
+ type: string
+ userId:
+ type: integer
+ required:
+ - appId
+ - money
+ - orderId
+ - payMoney
+ - sysType
+ - userId
+ type: object
+ statcenter.UserRegisterMsg:
+ properties:
+ appId:
+ description: appID
+ type: integer
+ devType:
+ description: 设备类型
+ type: string
+ districtCode:
+ description: 渠道码
+ type: string
+ ip:
+ description: IP
+ type: string
+ isDirect:
+ description: true:是直推用户
+ type: boolean
+ mobile:
+ description: 手机号
+ type: string
+ name:
+ description: 名称
+ type: string
+ platformId:
+ description: 原始平台流水Id
+ type: string
+ promCode:
+ description: 推广序列
+ type: string
+ promSeqe:
+ description: 全名代理
+ type: string
+ puc:
+ description: 应用中心推广
+ type: string
+ registerTime:
+ description: 注册时间
+ type: string
+ sysType:
+ description: 操作系统类型 安卓 IOS
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ required:
+ - appId
+ - isDirect
+ - registerTime
+ - sysType
+ - userId
+ type: object
+ statcenter.UserVisitMsg:
+ properties:
+ appId:
+ description: appID
+ type: integer
+ devID:
+ description: 设备ID
+ type: string
+ devType:
+ description: 设备型号
+ type: string
+ districtCode:
+ description: 渠道码
+ type: string
+ ip:
+ description: IP
+ type: string
+ isDirect:
+ description: true:是直推用户
+ type: boolean
+ platformId:
+ description: 原始平台流水Id
+ type: string
+ registerTime:
+ description: 用户注册时间
+ type: string
+ sysType:
+ description: 操作系统类型 安卓 IOS
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ version:
+ description: APP版本号
+ type: string
+ visitAt:
+ description: 访问时间
+ 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
+ vipordermod.OrderType:
+ enum:
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ OrderTypeCoin: 金币购买
+ OrderTypeVip: 会员购买
+ x-enum-varnames:
+ - OrderTypeVip
+ - OrderTypeCoin
+ vipordermod.VipOrder:
+ properties:
+ amount:
+ description: 支付金额(分)
+ type: integer
+ cardId:
+ description: ── VIP 专用 ──
+ type: integer
+ cardName:
+ description: 套餐名快照
+ type: string
+ coin:
+ description: 到账金币数(快照)
+ type: integer
+ coinCfgId:
+ description: ── 金币专用 ──
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ districtCode:
+ description: 下单时用户渠道码
+ type: string
+ finishedAt:
+ description: 完成时间
+ type: string
+ id:
+ description: 唯一ID
+ type: integer
+ ipAddr:
+ description: 客户端 IP
+ type: string
+ oid:
+ description: 第三方订单号
+ type: string
+ orderType:
+ allOf:
+ - $ref: '#/definitions/vipordermod.OrderType'
+ description: 订单类型:1会员 2金币
+ payChannel:
+ description: 支付渠道
+ type: string
+ payMode:
+ description: 支付方式:overage/alipay/wechat
+ type: string
+ payRate:
+ description: 费率
+ type: string
+ payUrl:
+ description: 支付链接
+ type: string
+ payUrlMode:
+ description: 链接打开方式
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.VipOrderStatus'
+ description: 订单状态
+ sysType:
+ description: ios/android
+ type: string
+ tradeNo:
+ description: 本地订单号
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ type: object
+host: https://视频站-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/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:
+ description: 通过 token 获取最新用户信息,包含金币余额、VIP状态等
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 用户信息
+ schema:
+ $ref: '#/definitions/authser.LoginResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 获取当前用户信息
+ tags:
+ - 用户认证
+ /api/app/auth/guest:
+ post:
+ consumes:
+ - application/json
+ description: 按设备ID创建或找回游客账号;请求头可带 Authorization 复用已有游客会话。需传 X-User-Agent
+ parameters:
+ - description: 游客登录参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/authser.GuestLoginReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/authser.LoginResp'
+ "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: 用户信息通过 token 获取,无需传账号;nickName、avatar、introduction、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: 传入账号、邮箱、明文密码。请求头带游客 Authorization 时升级该游客为普通用户,否则新建用户
+ 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/auth/resetPassword:
+ post:
+ consumes:
+ - application/json
+ description: 通过账号和邮箱双重校验后重置密码,两者必须同时匹配同一用户
+ parameters:
+ - description: 找回密码参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/authapi.resetPasswordReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 找回密码
+ tags:
+ - 用户认证
+ /api/app/coin/buy:
+ post:
+ consumes:
+ - application/json
+ description: 根据指定金币套餐发起第三方支付,返回支付链接
+ parameters:
+ - description: 购买参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/coinapi.buyCoinReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 支付链接
+ schema:
+ $ref: '#/definitions/coinser.BuyCoinResp'
+ security:
+ - ApiKeyAuth: []
+ summary: 购买金币
+ tags:
+ - 金币
+ /api/app/coin/orders:
+ get:
+ parameters:
+ - description: 页码(默认1)
+ in: query
+ name: page
+ type: integer
+ - description: 每页数量(默认10)
+ in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 订单列表
+ schema:
+ type: object
+ security:
+ - ApiKeyAuth: []
+ summary: 查询金币购买记录
+ tags:
+ - 金币
+ /api/app/coin/trans/list:
+ get:
+ description: 返回当前登录用户的金币收入、支出流水,按交易时间倒序
+ parameters:
+ - description: 页码(默认1)
+ in: query
+ name: page
+ type: integer
+ - description: 每页数量(默认10,最大100)
+ in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/response.BasePageResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CoinTransListItemResp'
+ type: array
+ type: object
+ security:
+ - ApiKeyAuth: []
+ summary: 查询金币余额明细 -- by 旺鸣
+ tags:
+ - 金币
+ /api/app/comment/like/set:
+ post:
+ consumes:
+ - application/json
+ description: 设置当前用户对短剧评论或短剧剧集评论的点赞状态
+ parameters:
+ - description: 评论点赞设置请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.CommentLikeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.CommentLikeSetResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 评论点赞/取消点赞 --by 王鸣
+ tags:
+ - 评论APP
+ /api/app/comment/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧或短剧剧集评论树,一级评论分页返回,二级评论挂在children中,objectType 10-短剧 11-短剧剧集
+ parameters:
+ - description: 对象ID(短剧id 短剧剧集id)
+ in: query
+ name: objectId
+ required: true
+ type: integer
+ - description: 对象类型 10-短剧 11-短剧剧集
+ in: query
+ name: objectType
+ required: true
+ type: integer
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 排序 1-热门,按点赞数倒序 2-最新,按创建时间倒序
+ in: query
+ name: sortOrder
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.GetCommentListResp'
+ - 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: 发布短剧或短剧剧集评论,parentsId为0发布一级评论,大于0发布二级评论,replyCommentId表示实际回复的评论,objectType
+ 10-短剧 11-短剧剧集
+ 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/info:
+ get:
+ consumes:
+ - application/json
+ description: 返回最新地址信息、完整系统配置、广告配置和全站SEO配置
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.ConfigInfoResp'
+ "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/contactus/add:
+ post:
+ consumes:
+ - application/json
+ description: 用户提交联系我们表单,问题类型:1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ parameters:
+ - description: 联系我们请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.ContactUsAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.ContactUsAddResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 提交联系我们 -- by 旺鸣
+ tags:
+ - 联系我们相关接口
+ /api/app/feedback/add:
+ post:
+ consumes:
+ - application/json
+ description: 用户提交意见反馈,问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他
+ parameters:
+ - description: 意见反馈请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.FeedbackReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.FeedbackResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 提交意见反馈 -- by 旺鸣
+ tags:
+ - 意见反馈相关接口
+ /api/app/playlet/chapter/collect/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:
+ $ref: '#/definitions/proto.PlayletChapterCollectListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端我的短剧剧集收藏列表 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/chapter/collect/set:
+ post:
+ consumes:
+ - application/json
+ description: 设置当前用户短剧剧集收藏状态,收藏数只在状态真实变化时增减
+ parameters:
+ - description: 短剧剧集收藏设置请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterCollectSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterCollectSetResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端短剧剧集收藏/取消收藏 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/chapter/detail:
+ get:
+ description: 根据短剧剧集ID查询播放页当前剧集详情;可传playletId或playletSlug校验剧集归属,slug非空时优先解析短剧ID
+ parameters:
+ - description: 短剧剧集ID
+ in: query
+ name: chapterId
+ required: true
+ type: integer
+ - description: 短剧ID,传入时用于校验剧集归属
+ in: query
+ name: playletId
+ type: integer
+ - description: 短剧slug,非空时优先按slug解析短剧ID
+ in: query
+ name: playletSlug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterInfoResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端根据ID查询短剧剧集详情 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/chapter/like/set:
+ post:
+ consumes:
+ - application/json
+ description: 设置当前用户短剧剧集点赞状态,点赞数只在状态真实变化时增减
+ parameters:
+ - description: 短剧剧集点赞设置请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterLikeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterLikeSetResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端短剧剧集点赞/取消点赞 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/chapter/list:
+ get:
+ description: 根据短剧ID或slug查询右侧选集列表;slug非空时优先按slug解析短剧ID
+ parameters:
+ - description: 短剧ID
+ in: query
+ name: playletId
+ type: integer
+ - description: 短剧slug,非空时优先按slug解析短剧ID
+ in: query
+ name: playletSlug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端根据短剧ID查询剧集列表 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/chapter/unlock:
+ post:
+ consumes:
+ - application/json
+ description: 仅金币付费剧集可调用;接口具备幂等性,已解锁剧集重复调用不会重复扣金币
+ parameters:
+ - description: 短剧剧集金币解锁请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterUnlockReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterUnlockResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端使用金币逐集解锁短剧剧集 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/chapter/watch/report:
+ post:
+ consumes:
+ - application/json
+ description: 上报当前用户短剧剧集播放进度,达到已看条件时用于我的收藏未看集数计算
+ parameters:
+ - description: 短剧剧集观看上报请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterWatchReportReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterWatchReportResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端短剧剧集观看上报 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/collect/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:
+ $ref: '#/definitions/proto.PlayletCollectListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端我的短剧收藏列表 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/collect/set:
+ post:
+ consumes:
+ - application/json
+ description: 设置当前用户短剧收藏状态,收藏数只在状态真实变化时增减
+ parameters:
+ - description: 短剧收藏设置请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletCollectSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletCollectSetResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端短剧收藏/取消收藏 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/detail:
+ get:
+ description: 根据短剧ID或slug查询已上架短剧详情;slug非空时优先按slug解析短剧ID
+ 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:
+ $ref: '#/definitions/proto.PlayletInfoResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端根据ID查询短剧详情 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/forward/heat:
+ post:
+ consumes:
+ - application/json
+ description: 请求一次短剧热度值增加5,仅支持已上架短剧
+ parameters:
+ - description: 短剧转发加热度请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletForwardHeatReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletForwardHeatResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端短剧转发增加热度 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/history/clear:
+ post:
+ consumes:
+ - application/json
+ description: 仅清空观看历史,不影响收藏列表和观看进度
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletHistoryClearResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端清空我的短剧观看历史 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/history/list:
+ get:
+ consumes:
+ - application/json
+ description: 按当前用户最近观看时间倒序分页返回短剧观看历史;pageSize默认20、最大100
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletHistoryListResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端我的短剧观看历史列表 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/home:
+ get:
+ consumes:
+ - application/json
+ description: 无请求参数,返回首页banner、最近更新和启用短剧分类数据;homeCategoryData按分类rank倒序返回全部启用分类,每个分类最多16条短剧
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.HomePlayletResponse'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端首页短剧接口 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/hot-like:
+ get:
+ consumes:
+ - application/json
+ description: 返回缓存的前10个热搜词,以及按收藏量最多的15部猜你喜欢短剧
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletHotLikeResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端热搜词和猜你喜欢
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/like/set:
+ post:
+ consumes:
+ - application/json
+ description: 设置当前用户短剧点赞状态,点赞数只在状态真实变化时增减
+ parameters:
+ - description: 短剧点赞设置请求
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/proto.PlayletLikeSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletLikeSetResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: app端短剧点赞/取消点赞 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询已上架短剧;可按题材ID或题材slug筛选,slug非空时优先按slug解析题材ID
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 排序 1-热度优先,按收藏量倒序 2-今日更新,按上架时间倒序
+ in: query
+ name: sortOrder
+ type: integer
+ - description: 题材ID
+ in: query
+ name: themeId
+ type: integer
+ - description: 题材slug,非空时优先按slug解析题材ID
+ in: query
+ name: themeSlug
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletVideoResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 短剧 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/operate/list:
+ get:
+ consumes:
+ - application/json
+ description: 按运营类型查询已上架短剧;operateType=1短剧 2漫剧;filterType=1最近更新(按上架时间) 2当前热播(按收藏数)
+ 3独家原创(原创且按推荐排序)
+ parameters:
+ - description: 筛选类型 1-最近更新 2-当前热播 3-独家原创
+ in: query
+ name: filterType
+ type: integer
+ - description: 运营类型 1-短剧 2-漫剧
+ in: query
+ name: operateType
+ type: integer
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletOperateListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端按运营类型分页查询短剧
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/rank:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询已上架短剧排行榜;sortOrder=1按热度值倒序,2按上架时间倒序,3按收藏数倒序
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 排序 1-热播榜,按热度值倒序 2-新剧榜,按上架时间倒序 3-追剧榜,按收藏数倒序
+ in: query
+ name: sortOrder
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.PlayletRankResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 短剧排行榜 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/recommend/list:
+ get:
+ consumes:
+ - application/json
+ description: 根据短剧分类ID或分类slug分页查询已上架短剧;slug非空时优先按slug解析分类ID;pageSize默认10、最大100
+ 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.PlayletRecommendListResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端分页查询分类短剧(更多) --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/search:
+ get:
+ consumes:
+ - application/json
+ description: 使用ES分页搜索已上架短剧,支持剧名、题材名、简介和标签名检索
+ parameters:
+ - description: 搜索关键词,支持剧名、题材名、简介和标签名
+ in: query
+ name: keyword
+ 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.PlayletSearchResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端ES分页搜索短剧 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/tag/list:
+ get:
+ consumes:
+ - application/json
+ description: 根据标签ID或标签slug分页查询已上架短剧;slug非空时优先按slug解析标签ID
+ parameters:
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ 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.PlayletTagVideoResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端根据标签ID分页查询短剧 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/playlet/today-update:
+ 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.PlayletTodayUpdateResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.CommonData'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端分页查询今日更新短剧 --by 王鸣
+ tags:
+ - app短剧相关接口
+ /api/app/recharge/package/list:
+ get:
+ description: 返回会员畅享和金币充值两个列表,并附带各套餐可用支付方式
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.RechargePackageListResp'
+ summary: 查询购买套餐列表 -- by 王鸣
+ tags:
+ - 金币与会员
+ /api/app/statcenter/sync:
+ post:
+ consumes:
+ - application/json
+ description: 按 job 拉取注册/日活/充值/绑定/消费/售卡等增量数据
+ parameters:
+ - description: 参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/statcenter.StatcenterSyncReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/statcenter.StatcenterSyncResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 统计中心增量同步
+ tags:
+ - 数据同步
+ /api/app/theme/list:
+ get:
+ consumes:
+ - application/json
+ description: 无请求参数,不分页,返回已上架题材列表
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletThemeListResponse'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端短剧题材列表 --by 王鸣
+ tags:
+ - app短剧题材相关接口
+ /api/app/translation/category/list:
+ get:
+ consumes:
+ - application/json
+ description: 无请求参数,不分页,返回启用的顶部导航分类
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.AppTranslationInfo'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: app端导航分类列表 --by 王鸣
+ tags:
+ - APP导航相关接口
+ /api/app/upload/image:
+ post:
+ consumes:
+ - multipart/form-data
+ description: 将图片转发至存储服务 /media/web/autopublish/image
+ parameters:
+ - description: 图片文件
+ in: formData
+ name: file
+ required: true
+ type: file
+ - description: 是否加水印:1加,其他情况不加(头像不加)
+ in: formData
+ name: watermark
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/uploadser.ImageUploadResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ summary: 图片上传
+ tags:
+ - 上传
+ /api/app/video/detail:
+ get:
+ consumes:
+ - application/json
+ description: 查询已上架视频详情和该视频相关推荐列表
+ parameters:
+ - description: 视频的id必填
+ in: query
+ name: id
+ required: true
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.VideoInfoResp'
+ - properties:
+ recommendVideos:
+ items:
+ $ref: '#/definitions/proto.VideoItemResp'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 查询视频详情 --by 旺鸣
+ tags:
+ - 视频相关接口
+ /api/app/video/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.VideoResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.VideoItemResp'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ summary: 普通分页查询视频 --by 旺鸣
+ tags:
+ - 视频相关接口
+ /api/app/vip/orders:
+ get:
+ consumes:
+ - application/json
+ parameters:
+ - description: 页码(默认1)
+ in: query
+ name: page
+ type: integer
+ - description: 每页数量(默认10)
+ in: query
+ name: pageSize
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 订单列表
+ schema:
+ items:
+ $ref: '#/definitions/vipordermod.VipOrder'
+ type: array
+ security:
+ - ApiKeyAuth: []
+ summary: 查询 VIP 充值记录
+ tags:
+ - 充值
+ /api/app/vip/pay:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 下单参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/rechargeapi.submitOrderReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 支付链接
+ schema:
+ $ref: '#/definitions/rechargeser.SubmitOrderResp'
+ security:
+ - ApiKeyAuth: []
+ summary: 创建 VIP 充值订单(第三方支付)
+ tags:
+ - 充值
+ /richtext/: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:
+ - 标签搜索
+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..553f3d9
--- /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 https://视频站-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..636c8e3
--- /dev/null
+++ b/app/middleware/auth/http.go
@@ -0,0 +1,131 @@
+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"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+)
+
+// gin 验证中间件
+
+var (
+ // 白名单,不需要验证token的api
+ whiteList = []string{
+ "/swagger",
+ "/api/app/auth/guest",
+ "/api/app/auth/register",
+ "/api/app/auth/login",
+ "/api/app/auth/resetPassword",
+ "/api/app/video/list",
+ "/api/app/video/detail",
+ "/api/app/playlet/home",
+ "/api/app/playlet/recommend/list",
+ "/api/app/playlet/list",
+ "/api/app/playlet/operate/list",
+ "/api/app/playlet/rank",
+ "/api/app/playlet/today-update",
+ "/api/app/playlet/tag/list",
+ "/api/app/playlet/search",
+ "/api/app/playlet/hot-like",
+ "/api/app/playlet/detail",
+ "/api/app/playlet/chapter/detail",
+ "/api/app/recharge/package/list",
+ "/api/app/comment/list",
+ "/api/app/theme/list",
+ "/api/app/config/info",
+ "/api/app/config/footer/info",
+ "/api/app/config/richtext/list",
+ "/api/app/applist",
+ "/api/app/translation/category/list",
+ "/api/app/health/ping",
+ "/api/app/statcenter/sync",
+ "/h5/m3u8",
+ "/enkey",
+ "/newenkey",
+ }
+)
+
+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
+ }
+
+ if pubdef.IsCrawlerUser(userId) {
+ if len(newToken) > 0 {
+ ctx.Writer.Header().Set("Refresh-Authorization", newToken)
+ }
+ ctx.Set(consts.CtxUserID, userId)
+ ctx.Set(consts.CtxToken, token)
+ return srvdef.Success
+ }
+
+ 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..c249400
--- /dev/null
+++ b/app/middleware/ipblacklist/ipblacklist.go
@@ -0,0 +1,18 @@
+package ipblacklist
+
+import (
+ "errors"
+ "net/http"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/ipaddr"
+
+ "github.com/gin-gonic/gin"
+)
+
+func IpBlackListMiddleware(ctx *gin.Context) {
+ ip := ipaddr.ResolveClientIP(ctx)
+ if configser.CheckIpBlack(ip) {
+ ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
+ return
+ }
+}
diff --git a/app/middleware/ipstat/ipstat.go b/app/middleware/ipstat/ipstat.go
new file mode 100644
index 0000000..7d7892c
--- /dev/null
+++ b/app/middleware/ipstat/ipstat.go
@@ -0,0 +1,78 @@
+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"
+)
+
+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)
+
+ added, err := redis.SAddCount(key, normalized)
+ if err != nil {
+ log.Warn("IpStatMiddleware SAdd failed", log.Any("key", key), log.E(err))
+ return
+ }
+ if added > 0 {
+ if err := redis.Expire(key, redisconst.IpStatExpire); err != nil {
+ log.Warn("IpStatMiddleware Expire failed", log.Any("key", key), log.E(err))
+ }
+ }
+}
+
+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..416dce6
--- /dev/null
+++ b/app/middleware/monitor/monitor.go
@@ -0,0 +1,266 @@
+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/ipaddr"
+ "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 := ipaddr.ResolveClientIP(ctx)
+ 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)
+ if path == "/api/app/feedback/add" {
+ rawString = ""
+ }
+ 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..fae98e6
--- /dev/null
+++ b/app/middleware/reptilerecord/reptilerecord.go
@@ -0,0 +1,50 @@
+package reptilerecord
+
+import (
+ "strings"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+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", ipaddr.ResolveClientIP(c)))
+ }
+}
+
+// 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..fae3b7e
--- /dev/null
+++ b/app/middleware/templatecache/cache.go
@@ -0,0 +1,72 @@
+package templatecache
+
+import (
+ "bytes"
+ "github.com/gin-gonic/gin"
+ "github.com/patrickmn/go-cache"
+ "net/http"
+ "time"
+)
+
+var (
+ Cache *cache.Cache
+)
+
+func init() {
+ Cache = cache.New(5*time.Minute, 5*time.Minute)
+}
+
+// 自定义 ResponseWriter 用于捕获 HTML 输出
+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) {
+ // 只缓存 GET 请求(HEAD 由 _web 层处理,_server 不会收到 HEAD)
+ if ctx.Request.Method != "GET" {
+ ctx.Next()
+ return
+ }
+
+ // 生成缓存键(包含路径和查询参数)
+ cacheKey := ctx.Request.URL.String()
+ var cacheJson []byte
+ if cacheData, exists := Cache.Get(cacheKey); exists {
+ if item, ok := cacheData.([]byte); ok {
+ cacheJson = item
+ }
+ }
+ // 检查缓存
+ if cacheJson != nil {
+ ctx.Data(http.StatusOK, "application/json; charset=utf-8", cacheJson)
+ ctx.Abort()
+ return
+ }
+
+ writer := &templateResponseWriter{
+ body: bytes.NewBuffer(nil),
+ ResponseWriter: ctx.Writer,
+ }
+ ctx.Writer = writer
+
+ ctx.Next()
+
+ // 只有成功的响应才缓存
+ status := ctx.Writer.Status()
+ if status == 200 {
+ cacheJsonData := writer.body.Bytes()
+ cacheDuration := duration
+ if cacheDuration == 0 {
+ cacheDuration = 1 * time.Minute
+ }
+ Cache.Set(cacheKey, cacheJsonData, 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..d87f1c3
--- /dev/null
+++ b/app/middleware/visit/visit.go
@@ -0,0 +1,114 @@
+package visit
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/app/middleware/reptilerecord"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/uservisitlog"
+ "videoweb/pub/redis"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// Visit 用户日活访问中间件:每天首次访问写入 user_visit_log
+func Visit(ctx *gin.Context) {
+ if reptilerecord.IsBot(ctx) {
+ return
+ }
+ val, ok := ctx.Get(consts.CtxUA)
+ if !ok {
+ return
+ }
+ ua, ok := val.(useragent.UA)
+ if !ok {
+ return
+ }
+ RecordDailyVisit(ctx, ctx.GetInt64(consts.CtxUserID), ua, ipaddr.ResolveClientIP(ctx))
+}
+
+// RecordDailyVisit 当天首次访问写入 user_visit_log,游客新建成功后也会立刻记一笔。
+func RecordDailyVisit(ctx context.Context, userId int64, ua useragent.UA, ip string) {
+ if userId <= 0 {
+ return
+ }
+ now := time.Now()
+ userVisitLogKey := redisconst.UserVisitLogKey(userId, now)
+ exist, err := redis.Exists(userVisitLogKey)
+ if err != nil || exist {
+ return
+ }
+ visitInfo := &uservisitlog.UserVisitLog{
+ Id: primitive.NewObjectID(),
+ UserId: userId,
+ SysType: ua.SysType,
+ DevType: ua.DevType,
+ DevID: ua.DevID,
+ Version: ua.Ver,
+ IP: ip,
+ VisitAt: now,
+ }
+ if serr := uservisitlog.InsertOneVisitLog(ctx, visitInfo); serr != nil {
+ return
+ }
+ if serr := userVisitStatus(ctx, userId, ua, visitInfo.IP, visitInfo.VisitAt); serr != nil {
+ return
+ }
+ _ = redis.Set(userVisitLogKey, "1", utils.BeginOfToday().Add(24*time.Hour).Sub(now)+5*time.Minute)
+}
+
+func userVisitStatus(ctx context.Context, uId int64, ua useragent.UA, ip string, visitTime time.Time) *srvdef.SrvErr {
+ uFilter := primitive.M{usermod.FN.User.Id: uId}
+ user, serr := usermod.FindOneUser(ctx, uFilter)
+ if serr != nil {
+ return serr
+ }
+
+ todayZero := utils.BeginOfToday()
+ yesterdayZero := todayZero.AddDate(0, 0, -1)
+ setDoc := primitive.M{
+ usermod.FN.User.AppVer: ua.Ver,
+ usermod.FN.User.SysType: ua.SysType,
+ usermod.FN.User.Device: ua.DevType,
+ usermod.FN.User.LastIp: ip,
+ usermod.FN.User.LastLoginAt: visitTime,
+ }
+ mergeUAExtFields(setDoc, user, ua)
+ var updateDoc primitive.M
+ if user.LastLoginAt.Before(todayZero) && user.LastLoginAt.After(yesterdayZero) {
+ updateDoc = primitive.M{
+ "$inc": primitive.M{usermod.FN.User.ConsecutiveLogin: 1},
+ "$set": setDoc,
+ }
+ } else {
+ setDoc[usermod.FN.User.ConsecutiveLogin] = int64(1)
+ updateDoc = primitive.M{"$set": setDoc}
+ }
+ return usermod.UpdateOneUser(ctx, uFilter, updateDoc)
+}
+
+func mergeUAExtFields(setDoc primitive.M, user *usermod.User, ua useragent.UA) {
+ if v := strings.TrimSpace(ua.SessionID); v != "" && v != user.SessionID {
+ setDoc[usermod.FN.User.SessionID] = v
+ }
+ if v := strings.TrimSpace(ua.DeviceBrand); v != "" && v != user.DeviceBrand {
+ setDoc[usermod.FN.User.DeviceBrand] = v
+ }
+ if v := strings.TrimSpace(ua.DeviceModel); v != "" && v != user.DeviceModel {
+ setDoc[usermod.FN.User.DeviceModel] = v
+ }
+ if v := strings.TrimSpace(ua.SystemName); v != "" && v != user.SystemName {
+ setDoc[usermod.FN.User.SystemName] = v
+ }
+ if v := strings.TrimSpace(ua.SystemVersion); v != "" && v != user.SystemVersion {
+ setDoc[usermod.FN.User.SystemVersion] = v
+ }
+}
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..79dff04
--- /dev/null
+++ b/app/router/auth.go
@@ -0,0 +1,19 @@
+package router
+
+import (
+ "videoweb/app/api/authapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func authRouter(r *gin.Engine) {
+ auth := r.Group("/api/app/auth")
+ {
+ auth.POST("/guest", authapi.GuestLogin)
+ auth.POST("/register", authapi.Register)
+ auth.POST("/login", authapi.Login)
+ auth.GET("/getInfo", authapi.GetInfo)
+ auth.POST("/profile", authapi.UpdateProfile)
+ auth.POST("/resetPassword", authapi.ResetPassword)
+ }
+}
diff --git a/app/router/coin.go b/app/router/coin.go
new file mode 100644
index 0000000..72a327a
--- /dev/null
+++ b/app/router/coin.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "videoweb/app/api/coinapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func coinRouter(r *gin.Engine) {
+ coinGroup := r.Group("/api/app/coin")
+ {
+ coinGroup.POST("/buy", coinapi.BuyCoin) // 发起购买金币(第三方支付)
+ coinGroup.GET("/orders", coinapi.GetCoinOrderHistory) // 查询购买记录
+ coinGroup.GET("/trans/list", coinapi.GetCoinTransList) // 查询金币余额明细
+ }
+}
diff --git a/app/router/comment.go b/app/router/comment.go
new file mode 100644
index 0000000..f0729fc
--- /dev/null
+++ b/app/router/comment.go
@@ -0,0 +1,18 @@
+package router
+
+import (
+ "videoweb/app/api/commentapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func commentRouter(r *gin.Engine) {
+ r.POST("/comment/send/", commentapi.SendComment)
+ group := r.Group("/api/app/comment")
+ {
+ group.POST("/send", commentapi.SendComment)
+ group.POST("/send/", commentapi.SendComment)
+ group.GET("/list", commentapi.List)
+ group.POST("/like/set", commentapi.SetLike)
+ }
+}
diff --git a/app/router/config.go b/app/router/config.go
new file mode 100644
index 0000000..d3b6f7e
--- /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/info", configapi.Info)
+ r.GET("/api/app/config/footer/info", configapi.FooterInfo)
+ r.GET("/api/app/config/richtext/list", configapi.RichTextList)
+}
diff --git a/app/router/contactus.go b/app/router/contactus.go
new file mode 100644
index 0000000..06a8ba5
--- /dev/null
+++ b/app/router/contactus.go
@@ -0,0 +1,11 @@
+package router
+
+import (
+ "videoweb/app/api/contactusapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func contactUsRouter(r *gin.Engine) {
+ r.POST("/api/app/contactus/add", contactusapi.AddContactUs)
+}
diff --git a/app/router/feedback.go b/app/router/feedback.go
new file mode 100644
index 0000000..31ce427
--- /dev/null
+++ b/app/router/feedback.go
@@ -0,0 +1,11 @@
+package router
+
+import (
+ "videoweb/app/api/feedbackapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func feedbackRouter(r *gin.Engine) {
+ r.POST("/api/app/feedback/add", feedbackapi.AddFeedback)
+}
diff --git a/app/router/gin.go b/app/router/gin.go
new file mode 100644
index 0000000..5bc3d0c
--- /dev/null
+++ b/app/router/gin.go
@@ -0,0 +1,130 @@
+package router
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+ "videoweb/app/api/home"
+ "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/middleware/templatecache"
+ "videoweb/app/middleware/visit"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "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()
+ if appg.Conf.TrustedProxies != "" {
+ proxies := strings.Split(appg.Conf.TrustedProxies, ",")
+ for i, p := range proxies {
+ proxies[i] = strings.TrimSpace(p)
+ }
+ _ = engine.SetTrustedProxies(proxies)
+ log.Infof("SetTrustedProxies: %v", proxies)
+ } else {
+ _ = engine.SetTrustedProxies([]string{})
+ log.Infof("SetTrustedProxies: none (direct TCP IP mode)")
+ }
+ engine.HandleMethodNotAllowed = true
+ engine.MaxMultipartMemory = 60 << 20
+
+ common.InitGinSecret(appg.Conf.ResKey, consts.RunMod(appg.Conf.RunMod))
+ // 注册中间件
+ 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",
+ "sid",
+ "device_brand", "device-brand",
+ "device_model", "device-model",
+ "system_name", "system-name",
+ "system_version", "system-version",
+ },
+ 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) {
+ videoRouter(r)
+ playletRouter(r)
+ playletThemeRouter(r)
+ commentRouter(r)
+ coinRouter(r)
+ feedbackRouter(r)
+ contactUsRouter(r)
+ authRouter(r)
+ rechargeRouter(r)
+ rechargePackageRouter(r)
+ uploadRouter(r)
+ healthRouter(r)
+ configRouter(r)
+ appInfoRouter(r)
+ translationRouter(r)
+ statcenterRouter(r)
+ r.GET("/notpage", templatecache.WithCache(time.Minute*1), home.PageNotFound)
+
+}
+
+// 设置swag 接口信息
+func setGinSwagger(router *gin.Engine) {
+ url := ginSwagger.URL(fmt.Sprintf("%s/swagger/doc.json", appg.Conf.Host))
+ 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/playlet.go b/app/router/playlet.go
new file mode 100644
index 0000000..6240fa8
--- /dev/null
+++ b/app/router/playlet.go
@@ -0,0 +1,36 @@
+package router
+
+import (
+ "videoweb/app/api/playletapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func playletRouter(r *gin.Engine) {
+ group := r.Group("/api/app/playlet")
+ {
+ group.GET("/home", playletapi.HomePlaylet)
+ group.GET("/recommend/list", playletapi.RecommendList)
+ group.GET("/list", playletapi.List)
+ group.GET("/operate/list", playletapi.OperateList)
+ group.GET("/rank", playletapi.Rank)
+ group.GET("/today-update", playletapi.TodayUpdate)
+ group.GET("/tag/list", playletapi.TagVideoList)
+ group.GET("/search", playletapi.Search)
+ group.GET("/hot-like", playletapi.HotLike)
+ group.POST("/like/set", playletapi.SetLike)
+ group.POST("/collect/set", playletapi.SetCollect)
+ group.POST("/forward/heat", playletapi.ForwardHeat)
+ group.GET("/collect/list", playletapi.CollectList)
+ group.GET("/history/list", playletapi.HistoryList)
+ group.POST("/history/clear", playletapi.ClearHistory)
+ group.POST("/chapter/like/set", playletapi.SetChapterLike)
+ group.POST("/chapter/collect/set", playletapi.SetChapterCollect)
+ group.GET("/chapter/collect/list", playletapi.ChapterCollectList)
+ group.POST("/chapter/watch/report", playletapi.ReportChapterWatch)
+ group.POST("/chapter/unlock", playletapi.UnlockChapter)
+ group.GET("/detail", playletapi.Detail)
+ group.GET("/chapter/detail", playletapi.ChapterDetail)
+ group.GET("/chapter/list", playletapi.ChapterList)
+ }
+}
diff --git a/app/router/playlettheme.go b/app/router/playlettheme.go
new file mode 100644
index 0000000..c7e3258
--- /dev/null
+++ b/app/router/playlettheme.go
@@ -0,0 +1,14 @@
+package router
+
+import (
+ "videoweb/app/api/playletthemeapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func playletThemeRouter(r *gin.Engine) {
+ group := r.Group("/api/app/theme")
+ {
+ group.GET("/list", playletthemeapi.List)
+ }
+}
diff --git a/app/router/recharge.go b/app/router/recharge.go
new file mode 100644
index 0000000..3025e86
--- /dev/null
+++ b/app/router/recharge.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/app/api/rechargeapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func rechargeRouter(r *gin.Engine) {
+ vipGroup := r.Group("/api/app/vip")
+ {
+ vipGroup.POST("/pay", rechargeapi.SubmitOrder)
+ vipGroup.GET("/orders", rechargeapi.GetOrderHistory)
+ }
+}
diff --git a/app/router/rechargepackage.go b/app/router/rechargepackage.go
new file mode 100644
index 0000000..c141868
--- /dev/null
+++ b/app/router/rechargepackage.go
@@ -0,0 +1,14 @@
+package router
+
+import (
+ "videoweb/app/api/rechargepackageapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func rechargePackageRouter(r *gin.Engine) {
+ group := r.Group("/api/app/recharge/package")
+ {
+ group.GET("/list", rechargepackageapi.List)
+ }
+}
diff --git a/app/router/statcenter.go b/app/router/statcenter.go
new file mode 100644
index 0000000..069ea4e
--- /dev/null
+++ b/app/router/statcenter.go
@@ -0,0 +1,14 @@
+package router
+
+import (
+ "videoweb/app/api/statcenterapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func statcenterRouter(r *gin.Engine) {
+ group := r.Group("/api/app/statcenter")
+ {
+ group.POST("/sync", statcenterapi.StatcenterSyncList)
+ }
+}
diff --git a/app/router/translation.go b/app/router/translation.go
new file mode 100644
index 0000000..ddd8eb5
--- /dev/null
+++ b/app/router/translation.go
@@ -0,0 +1,14 @@
+package router
+
+import (
+ "videoweb/app/api/translationapi"
+
+ "github.com/gin-gonic/gin"
+)
+
+func translationRouter(r *gin.Engine) {
+ group := r.Group("/api/app/translation")
+ {
+ group.GET("/category/list", translationapi.CategoryList)
+ }
+}
diff --git a/app/router/upload.go b/app/router/upload.go
new file mode 100644
index 0000000..98c7c67
--- /dev/null
+++ b/app/router/upload.go
@@ -0,0 +1,14 @@
+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("/image", uploadapi.UploadImage)
+ }
+}
diff --git a/app/router/video.go b/app/router/video.go
new file mode 100644
index 0000000..f00c0c4
--- /dev/null
+++ b/app/router/video.go
@@ -0,0 +1,33 @@
+package router
+
+import (
+ "time"
+ "videoweb/app/api/videoapi"
+ "videoweb/app/middleware/templatecache"
+ "videoweb/app/service/videoser"
+
+ "github.com/gin-gonic/gin"
+
+ "videoweb/app/api/home"
+ _ "videoweb/app/docs"
+)
+
+func videoRouter(r *gin.Engine) {
+ r.GET("/api/app/video/list", videoapi.List)
+ r.GET("/api/app/video/detail", videoapi.Detail)
+
+ r.GET("/tagcategory/cache", templatecache.WithCache(time.Minute*5), home.GetTagCategoryCache)
+
+ r.GET("/richtext/:id/", templatecache.WithCache(time.Minute*1), home.RichtextApi) // 富文本
+
+ r.GET("/h5/m3u8/*key", videoser.GetM3U8Ex)
+ r.GET("/h5/vtt/*key", videoser.GetVTTEx)
+ r.GET("/enkey/", videoser.Enkey)
+ r.GET("/newenkey/", videoser.NewEnkey)
+ r.GET("/lsjenkey", videoser.LsjEnkey)
+
+ r.GET("/urlRedirect/", templatecache.WithCache(time.Minute*1), home.UrlRedirect)
+
+ r.GET("/videoList", home.VideoListPage)
+ r.GET("/videoDetail", home.VideoDetailPage)
+}
diff --git a/app/server/server.go b/app/server/server.go
new file mode 100644
index 0000000..c7f9b03
--- /dev/null
+++ b/app/server/server.go
@@ -0,0 +1,212 @@
+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/playletcache"
+ "videoweb/app/service/translationser"
+ ipstatsync "videoweb/app/skd/ipstat"
+ "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.BucketVideoAddressV2)
+ mongos.StartLid()
+ initRedis()
+ if err := initElasticSearch(); err != nil {
+ log.Error("init ES failed", log.E(err))
+ os.Exit(1)
+ }
+ initData()
+ ipstatsync.StartSync()
+ dataReport.Init(dataReport.Config{
+ ApiUrl: appg.Conf.DataReport.Url,
+ RunMod: appg.Conf.RunMod,
+ })
+
+ initHttpServer()
+ //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()
+ appser.Start()
+ playletcache.Start()
+ seo.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() {
+ mongos.Shop.DisConnect()
+ mongos.ShopLog.DisConnect()
+ redis.Disconnect()
+ configser.Stop()
+ 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/appser/appinfo.go b/app/service/appser/appinfo.go
new file mode 100644
index 0000000..1d85740
--- /dev/null
+++ b/app/service/appser/appinfo.go
@@ -0,0 +1,12 @@
+package appser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/srvdef"
+)
+
+// AppCategoryData 查询应用分类列表及当前分类数据
+func AppCategoryData(ctx context.Context, categoryId int64) ([]proto.AppCategoryInfo, proto.AppCategoryInfo, *srvdef.SrvErr) {
+ return GetAppListCache(), GetCategoryCache(categoryId), nil
+}
diff --git a/app/service/appser/cache.go b/app/service/appser/cache.go
new file mode 100644
index 0000000..d21ae5f
--- /dev/null
+++ b/app/service/appser/cache.go
@@ -0,0 +1,196 @@
+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
+ selfAppName := strings.TrimSpace(config.WebSiteName)
+
+ 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 selfAppName != "" && strings.HasPrefix(app.Name, selfAppName) {
+ 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/appser/cache_test.go b/app/service/appser/cache_test.go
new file mode 100644
index 0000000..dc3b553
--- /dev/null
+++ b/app/service/appser/cache_test.go
@@ -0,0 +1,52 @@
+package appser
+
+import (
+ "strings"
+ "testing"
+
+ "videoweb/pub/pubdef"
+)
+
+func TestAppendChannelCode(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ rawURL string
+ channelCode string
+ wantContain string
+ wantSkip bool
+ }{
+ {
+ name: "append custom channel",
+ rawURL: "https://example.com/app",
+ channelCode: "custom",
+ wantContain: "dc=custom",
+ },
+ {
+ name: "fallback default channel",
+ rawURL: "https://example.com/app",
+ channelCode: "",
+ wantContain: "dc=" + pubdef.DefaultChannelCode,
+ },
+ {
+ name: "skip existing dc",
+ rawURL: "https://example.com/app?dc=exists",
+ channelCode: "custom",
+ wantContain: "dc=exists",
+ wantSkip: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := appendChannelCode(tt.rawURL, tt.channelCode)
+ if !strings.Contains(got, tt.wantContain) {
+ t.Fatalf("appendChannelCode() = %q, want contain %q", got, tt.wantContain)
+ }
+ if tt.wantSkip && strings.Contains(got, "dc=custom") {
+ t.Fatalf("appendChannelCode() should keep existing dc: %q", got)
+ }
+ })
+ }
+}
diff --git a/app/service/authser/auth.go b/app/service/authser/auth.go
new file mode 100644
index 0000000..b07b7ae
--- /dev/null
+++ b/app/service/authser/auth.go
@@ -0,0 +1,478 @@
+package authser
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/app/service/reportser"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "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 := strings.ToUpper(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 strings.ToUpper(utils.GetRandomLetters(12))
+}
+
+// LoginResp 登录/注册成功后的响应数据
+type LoginResp struct {
+ Token string `json:"token"`
+ UserId int64 `json:"userId"`
+ AccountNo string `json:"accountNo"`
+ Email string `json:"email"`
+ NickName string `json:"nickName"`
+ Introduction string `json:"introduction"` // 简介
+ VipType pubdef.VipType `json:"vipType"`
+ CurrentVipExpire time.Time `json:"currentVipExpire"` // VIP 过期时间,客户端需结合 vipType 判断是否真正有效
+ ShowCardId int64 `json:"showCardId"` // 最新会员卡id
+ ShowCardName string `json:"showCardName"` // 最新会员卡名称
+ Status pubdef.UserStatus `json:"status"`
+ Avatar string `json:"avatar"` // 头像
+ InviteCode string `json:"inviteCode"` // 邀请码
+ Type pubdef.UserType `json:"type"` // 身份类型:0游客 1普通用户
+ LoginType pubdef.LoginType `json:"loginType"` // 登陆方式
+ IsRegister bool `json:"isRegister"` // 是否已注册
+ IsNewUser bool `json:"isNewUser"` // 本次是否新建游客
+ PrivacyMode bool `json:"privacyMode"` // 隐私模式
+ Score int64 `json:"score"` // 积分
+ Recharge int64 `json:"recharge"` // 金币余额
+}
+
+// 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
+}
+
+// generateCrawlerToken 签发爬虫观看 JWT,不写入 Redis,避免占用真实用户槽位。
+func generateCrawlerToken() (string, *srvdef.SrvErr) {
+ token, err := utils.GenerateUserToken(utils.UserClaims{
+ UserID: pubdef.CrawlerUserId,
+ Expire: consts.UserJwtTokenExpire,
+ }, appg.Conf.JwtKey)
+ if err != nil {
+ return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
+ }
+ return token, nil
+}
+
+// CrawlerLoginResp 爬虫虚拟游客资料,不对应任何数据库用户。
+func CrawlerLoginResp(token string) *LoginResp {
+ return &LoginResp{
+ Token: token,
+ UserId: pubdef.CrawlerUserId,
+ NickName: "游客",
+ Status: pubdef.UserStatusNormal,
+ Type: pubdef.UserTypeGuest,
+ LoginType: pubdef.LoginTypeDevice,
+ IsRegister: false,
+ IsNewUser: false,
+ }
+}
+
+// Register 用户注册
+// guestUserId:请求头 Authorization 解析出的用户ID;若为游客则升级该用户,否则新建
+// agentInviteCode:邀请人的邀请码,可为空
+func Register(ctx context.Context, guestUserId int64, accountNo, email, password, agentInviteCode string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) {
+ accountNo = strings.TrimSpace(accountNo)
+ email = strings.TrimSpace(email)
+ password = strings.TrimSpace(password)
+
+ // 检查账号是否已存在
+ exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo})
+ if exist != nil {
+ return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "")
+ }
+ // 检查邮箱是否已存在
+ exist, _ = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Email: email})
+ if exist != nil {
+ return nil, srvdef.New(srvdef.ErrDbInputExist, "邮箱已被注册", "")
+ }
+
+ hashed, err := hashPassword(password)
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, "密码处理失败", "")
+ }
+
+ now := time.Now()
+
+ // 请求头带游客 token:升级当前游客为普通用户
+ if guestUserId > 0 {
+ guest, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: guestUserId})
+ if serr == nil && isGuestUser(guest) {
+ setDoc := bson.M{
+ usermod.FN.User.AccountNo: accountNo,
+ usermod.FN.User.Email: email,
+ usermod.FN.User.Password: hashed,
+ usermod.FN.User.Type: pubdef.UserTypeNormal,
+ usermod.FN.User.IsRegister: true,
+ usermod.FN.User.LoginType: pubdef.LoginTypeAccount,
+ usermod.FN.User.RegisterTime: now,
+ usermod.FN.User.LastLoginAt: now,
+ usermod.FN.User.UpdatedAt: now,
+ usermod.FN.User.LastIp: ip,
+ }
+ if agentInviteCode != "" && guest.AgentInviteCode == "" {
+ setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode
+ setDoc[usermod.FN.User.IsDirect] = false
+ guest.IsDirect = false
+ }
+ if v := strings.TrimSpace(ua.DevID); v != "" {
+ setDoc[usermod.FN.User.DevID] = v
+ }
+ if v := strings.TrimSpace(ua.DevType); v != "" {
+ setDoc[usermod.FN.User.Device] = v
+ }
+ if v := strings.TrimSpace(ua.SysType); v != "" {
+ setDoc[usermod.FN.User.SysType] = v
+ }
+ if v := strings.TrimSpace(ua.Ver); v != "" {
+ setDoc[usermod.FN.User.AppVer] = v
+ }
+ mergeUAExtFieldsToSet(setDoc, guest, ua)
+ applyChannelIfMissing(setDoc, guest, channel)
+ if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: guest.Id}, bson.M{"$set": setDoc}); serr != nil {
+ return nil, serr
+ }
+ if agentInviteCode != "" && guest.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: now},
+ },
+ )
+ }
+ guest.AccountNo = accountNo
+ guest.Email = email
+ guest.Type = pubdef.UserTypeNormal
+ guest.IsRegister = true
+ guest.LoginType = pubdef.LoginTypeAccount
+ guest.RegisterTime = now
+ token, serr := generateAndSaveToken(guest.Id)
+ if serr != nil {
+ return nil, serr
+ }
+ reportser.ReportUserRegister(guest, ua, ip, "username")
+ reportser.ReportUserLogin(guest, ua, ip, pubdef.LoginTypeAccount)
+ return toLoginResp(ctx, guest, token, true), nil
+ }
+ }
+
+ user := &usermod.User{
+ Id: mongos.Lid.User.Get(),
+ AccountNo: accountNo,
+ Email: email,
+ Password: hashed,
+ NickName: appg.GenerateNickname(),
+ Avatar: randomAvatar(),
+ InviteCode: generateUniqueInviteCode(ctx),
+ AgentInviteCode: agentInviteCode,
+ Type: pubdef.UserTypeNormal,
+ Status: pubdef.UserStatusNormal,
+ IsRegister: true,
+ LoginType: pubdef.LoginTypeAccount,
+ RegIp: ip,
+ LastIp: ip,
+ DevID: ua.DevID,
+ Device: ua.DevType,
+ SysType: ua.SysType,
+ AppVer: ua.Ver,
+ OriVer: ua.Ver,
+ SessionID: ua.SessionID,
+ DeviceBrand: ua.DeviceBrand,
+ DeviceModel: ua.DeviceModel,
+ SystemName: ua.SystemName,
+ SystemVersion: ua.SystemVersion,
+ DistrictCode: normalizeChannel(channel),
+ IsDirect: resolveUserDistrict(channel, agentInviteCode, ip, ua.SysType).IsDirect,
+ LastLoginAt: now,
+ RegisterTime: now,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if serr := usermod.InsertOneUser(ctx, user); serr != nil {
+ return nil, serr
+ }
+
+ // 若填写了邀请码,给对应用户的 invites +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
+ }
+ reportser.ReportUserRegister(user, ua, ip, "username")
+ reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeAccount)
+ return toLoginResp(ctx, user, token, false), nil
+}
+
+// Login 用户登录(账号或邮箱均可)
+func Login(ctx context.Context, account, password string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) {
+ // 账号或邮箱均可登录
+ user, _ := usermod.FindOneUser(ctx, bson.M{
+ "$or": []bson.M{
+ {usermod.FN.User.AccountNo: account},
+ {usermod.FN.User.Email: 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
+ }
+
+ now := time.Now()
+ setDoc := bson.M{
+ usermod.FN.User.LastLoginAt: now,
+ usermod.FN.User.UpdatedAt: now,
+ }
+ if ip != "" {
+ setDoc[usermod.FN.User.LastIp] = ip
+ }
+ if v := strings.TrimSpace(ua.DevID); v != "" {
+ setDoc[usermod.FN.User.DevID] = v
+ }
+ if v := strings.TrimSpace(ua.DevType); v != "" {
+ setDoc[usermod.FN.User.Device] = v
+ }
+ if v := strings.TrimSpace(ua.SysType); v != "" {
+ setDoc[usermod.FN.User.SysType] = v
+ }
+ if v := strings.TrimSpace(ua.Ver); v != "" {
+ setDoc[usermod.FN.User.AppVer] = v
+ }
+ // 已有账号的历史用户统一修正为普通用户
+ if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal {
+ setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal
+ setDoc[usermod.FN.User.IsRegister] = true
+ user.Type = pubdef.UserTypeNormal
+ user.IsRegister = true
+ }
+ mergeUAExtFieldsToSet(setDoc, user, ua)
+ applyChannelIfMissing(setDoc, user, channel)
+ _ = usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: user.Id},
+ bson.M{"$set": setDoc},
+ )
+ reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeAccount)
+ return toLoginResp(ctx, user, token, true), nil
+}
+
+// GetInfo 获取当前登录用户的最新信息(不刷新 token)
+func GetInfo(ctx context.Context, userId int64, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) {
+ if pubdef.IsCrawlerUser(userId) {
+ return CrawlerLoginResp(""), nil
+ }
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ if user == nil {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
+ }
+ now := time.Now()
+ setDoc := bson.M{}
+ if v := strings.TrimSpace(ua.DevID); v != "" && v != user.DevID {
+ setDoc[usermod.FN.User.DevID] = v
+ }
+ if v := strings.TrimSpace(ua.DevType); v != "" && v != user.Device {
+ setDoc[usermod.FN.User.Device] = v
+ }
+ if v := strings.TrimSpace(ua.SysType); v != "" && v != user.SysType {
+ setDoc[usermod.FN.User.SysType] = v
+ }
+ if v := strings.TrimSpace(ua.Ver); v != "" && v != user.AppVer {
+ setDoc[usermod.FN.User.AppVer] = v
+ }
+ if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal {
+ setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal
+ setDoc[usermod.FN.User.IsRegister] = true
+ user.Type = pubdef.UserTypeNormal
+ user.IsRegister = true
+ }
+ mergeUAExtFieldsToSet(setDoc, user, ua)
+ if len(setDoc) > 0 {
+ setDoc[usermod.FN.User.UpdatedAt] = now
+ _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
+ }
+ return toLoginResp(ctx, user, "", true), nil
+}
+
+func mergeUAExtFieldsToSet(setDoc bson.M, user *usermod.User, ua useragent.UA) {
+ if user == nil {
+ return
+ }
+ if v := strings.TrimSpace(ua.SessionID); v != "" && v != user.SessionID {
+ setDoc[usermod.FN.User.SessionID] = v
+ }
+ if v := strings.TrimSpace(ua.DeviceBrand); v != "" && v != user.DeviceBrand {
+ setDoc[usermod.FN.User.DeviceBrand] = v
+ }
+ if v := strings.TrimSpace(ua.DeviceModel); v != "" && v != user.DeviceModel {
+ setDoc[usermod.FN.User.DeviceModel] = v
+ }
+ if v := strings.TrimSpace(ua.SystemName); v != "" && v != user.SystemName {
+ setDoc[usermod.FN.User.SystemName] = v
+ }
+ if v := strings.TrimSpace(ua.SystemVersion); v != "" && v != user.SystemVersion {
+ setDoc[usermod.FN.User.SystemVersion] = v
+ }
+}
+
+// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
+type UpdateProfileReq struct {
+ UserId int64 // 用户ID(从 token 中获取)
+ NickName *string // 新昵称(可选)
+ Avatar *string // 新头像 URL(可选)
+ Introduction *string // 简介(可选)
+ PrivacyMode *bool // 隐私模式:true-开启,false-关闭(可选)
+ OldPassword *string // 旧密码明文(修改密码时必传)
+ NewPassword *string // 新密码明文(修改密码时必传)
+}
+
+// UpdateProfile 修改用户资料:支持修改头像、简介、密码,或组合修改
+func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr {
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId})
+ if user == nil {
+ return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
+ }
+
+ setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()}
+
+ if req.NickName != nil && *req.NickName != "" {
+ setDoc[usermod.FN.User.NickName] = *req.NickName
+ }
+ if req.Avatar != nil && *req.Avatar != "" {
+ setDoc[usermod.FN.User.Avatar] = *req.Avatar
+ }
+ if req.Introduction != nil {
+ setDoc[usermod.FN.User.Introduction] = strings.TrimSpace(*req.Introduction)
+ }
+ if req.PrivacyMode != nil {
+ setDoc[usermod.FN.User.PrivacyMode] = *req.PrivacyMode
+ }
+
+ // 修改密码:两者必须同时提供
+ 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},
+ )
+}
+
+// ResetPassword 通过账号+邮箱双重校验后重置密码
+func ResetPassword(ctx context.Context, accountNo, email, newPassword string) *srvdef.SrvErr {
+ user, _ := usermod.FindOneUser(ctx, bson.M{
+ usermod.FN.User.AccountNo: accountNo,
+ usermod.FN.User.Email: email,
+ })
+ if user == nil {
+ return srvdef.New(srvdef.ErrUserNotExsit, "账号或邮箱不匹配", "")
+ }
+
+ hashed, err := hashPassword(newPassword)
+ if err != nil {
+ return srvdef.New(srvdef.Failure, "密码处理失败", "")
+ }
+
+ return usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: user.Id},
+ bson.M{"$set": bson.M{
+ usermod.FN.User.Password: hashed,
+ usermod.FN.User.UpdatedAt: time.Now(),
+ }},
+ )
+}
diff --git a/app/service/authser/district.go b/app/service/authser/district.go
new file mode 100644
index 0000000..c4a4977
--- /dev/null
+++ b/app/service/authser/district.go
@@ -0,0 +1,68 @@
+package authser
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "videoweb/app/appg"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/statcenter"
+ "videoweb/pub/common/utils"
+)
+
+// UserDistrictInfo 统计中心返回的渠道/直推信息
+type UserDistrictInfo struct {
+ IsDirect bool `json:"isDirect"`
+ DistrictCode string `json:"districtCode"`
+ ParentPromCode string `json:"parentPromCode"`
+ ParentId uint64 `json:"parentId"`
+ ParentInviteCount int64 `json:"parentInviteCount"`
+}
+
+type userDistrictReq struct {
+ AppId int32 `json:"appId"`
+ DistrictCode string `json:"dc"`
+ PromSeqe string `json:"pc"`
+ IP string `json:"ip"`
+ SysType string `json:"sysType"`
+}
+
+// resolveUserDistrict 有邀请码视为非直推;否则向统计中心查询补量/直推。
+func resolveUserDistrict(channel, inviteCode, ip, sysType string) UserDistrictInfo {
+ inviteCode = strings.TrimSpace(inviteCode)
+ if inviteCode != "" {
+ return UserDistrictInfo{IsDirect: false, ParentPromCode: inviteCode}
+ }
+ return GetUserDistictInfo(channel, inviteCode, ip, sysType)
+}
+
+// GetUserDistictInfo 查询统计中心用户渠道信息,对齐 ksx_server。
+func GetUserDistictInfo(dc, pc, ip, sysType string) (districtInfo UserDistrictInfo) {
+ base := strings.TrimSpace(appg.Conf.NewProductUrl)
+ if base == "" {
+ return
+ }
+ request := userDistrictReq{
+ AppId: statcenter.STAT_APPID,
+ SysType: sysType,
+ DistrictCode: dc,
+ PromSeqe: pc,
+ IP: ip,
+ }
+ bodyStr, err := json.Marshal(request)
+ if err != nil {
+ log.Info(fmt.Sprintf("GetUserDistictInfo json.Marshal is fail error:%+v/data:%+v", err, request))
+ return
+ }
+ url := strings.TrimRight(base, "/") + "/api/stat/userInvite/district"
+ code, err := utils.POSTWithJResp(&districtInfo, url, nil, bodyStr)
+ if err != nil {
+ log.Error("GetUserDistictInfo POSTWithJResp", log.Any("url", url), log.E(err))
+ return
+ }
+ if code != http.StatusOK {
+ log.Error("GetUserDistictInfo response status", log.Any("code", code))
+ }
+ return
+}
diff --git a/app/service/authser/guest.go b/app/service/authser/guest.go
new file mode 100644
index 0000000..c45523e
--- /dev/null
+++ b/app/service/authser/guest.go
@@ -0,0 +1,276 @@
+package authser
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/app/middleware/reptilerecord"
+ "videoweb/app/middleware/visit"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/reportser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// GuestLoginReq 游客登录请求
+type GuestLoginReq struct {
+ DevID string `json:"devID"` // 设备唯一标示(必填)
+ Mac string `json:"mac"` // 设备 Mac(可选)
+ DeviceId string `json:"deviceId"` // 备用设备ID(可选,写入 DevV2)
+ Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入 DistrictCode)
+}
+
+// isGuestUser 是否为可升级的游客身份
+func isGuestUser(user *usermod.User) bool {
+ if user == nil {
+ return false
+ }
+ if user.Type == pubdef.UserTypeNormal {
+ return false
+ }
+ return !user.IsRegister && strings.TrimSpace(user.AccountNo) == ""
+}
+
+// GuestLogin 游客登录:按 token / DevID / Mac / DeviceId 查找,不存在则创建游客
+func GuestLogin(ctx context.Context, userId int64, ip string, req *GuestLoginReq, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) {
+ if isBotRequest(ctx) {
+ return crawlerGuestLogin()
+ }
+ if req == nil || strings.TrimSpace(req.DevID) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "devID不能为空", "")
+ }
+ ua.DevID = strings.TrimSpace(req.DevID)
+
+ var user *usermod.User
+ var serr *srvdef.SrvErr
+ reuseToken := false
+
+ if userId > 0 {
+ user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ if serr != nil {
+ if serr.Code != srvdef.ErrUserNotExsit {
+ return nil, serr
+ }
+ userId = 0
+ user = nil
+ } else {
+ reuseToken = true
+ }
+ }
+
+ isNewUser := false
+ if userId == 0 {
+ user, serr = findGuestUser(ctx, req)
+ if serr != nil {
+ if serr.Code != srvdef.ErrUserNotExsit {
+ return nil, serr
+ }
+ user, serr = createGuestUser(ctx, ua, req, ip)
+ if serr != nil {
+ return nil, serr
+ }
+ isNewUser = true
+ }
+ }
+
+ if user.Status == pubdef.UserStatusDisable {
+ return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
+ }
+
+ now := time.Now()
+ setDoc := bson.M{
+ usermod.FN.User.LastLoginAt: now,
+ usermod.FN.User.UpdatedAt: now,
+ usermod.FN.User.LastIp: ip,
+ usermod.FN.User.LoginType: pubdef.LoginTypeDevice,
+ }
+ if v := strings.TrimSpace(ua.DevID); v != "" {
+ setDoc[usermod.FN.User.DevID] = v
+ }
+ if v := strings.TrimSpace(ua.DevType); v != "" {
+ setDoc[usermod.FN.User.Device] = v
+ }
+ if v := strings.TrimSpace(ua.SysType); v != "" {
+ setDoc[usermod.FN.User.SysType] = v
+ }
+ if v := strings.TrimSpace(ua.Ver); v != "" {
+ setDoc[usermod.FN.User.AppVer] = v
+ }
+ if v := strings.TrimSpace(req.Mac); v != "" {
+ setDoc[usermod.FN.User.Mac] = v
+ }
+ if v := strings.TrimSpace(req.DeviceId); v != "" {
+ setDoc[usermod.FN.User.DevV2] = v
+ }
+ // 历史用户 Type 为空/旧值时,未注册则归为游客
+ if isGuestUser(user) && user.Type != pubdef.UserTypeGuest {
+ setDoc[usermod.FN.User.Type] = pubdef.UserTypeGuest
+ user.Type = pubdef.UserTypeGuest
+ }
+ mergeUAExtFieldsToSet(setDoc, user, ua)
+ applyChannelIfMissing(setDoc, user, req.Channel)
+ _ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
+
+ var token string
+ if reuseToken {
+ if old, err := tokener.Get(user.Id); err == nil && len(old) > 0 {
+ token = string(old)
+ }
+ }
+ if token == "" {
+ token, serr = generateAndSaveToken(user.Id)
+ if serr != nil {
+ return nil, serr
+ }
+ }
+
+ resp := toLoginResp(ctx, user, token, true)
+ resp.IsNewUser = isNewUser
+ if isNewUser {
+ visit.RecordDailyVisit(ctx, user.Id, ua, ip)
+ reportser.ReportUserRegister(user, ua, ip, "deviceid")
+ }
+ reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeDevice)
+ return resp, nil
+}
+
+func findGuestUser(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) {
+ user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: req.DevID})
+ if serr == nil {
+ return user, nil
+ }
+ if serr.Code != srvdef.ErrUserNotExsit {
+ return nil, serr
+ }
+
+ mac := strings.TrimSpace(req.Mac)
+ if mac != "" && !strings.Contains(mac, "00:00:00:00") {
+ user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac})
+ if serr == nil {
+ return user, nil
+ }
+ if serr.Code != srvdef.ErrUserNotExsit {
+ return nil, serr
+ }
+ }
+
+ deviceId := strings.TrimSpace(req.DeviceId)
+ if deviceId != "" && !strings.Contains(deviceId, "00:00:00:00") {
+ return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId})
+ }
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
+}
+
+func createGuestUser(ctx context.Context, ua useragent.UA, req *GuestLoginReq, ip string) (*usermod.User, *srvdef.SrvErr) {
+ now := time.Now()
+ uid := mongos.Lid.User.Get()
+ districtInfo := resolveUserDistrict(req.Channel, "", ip, ua.SysType)
+ user := &usermod.User{
+ Id: uid,
+ Type: pubdef.UserTypeGuest,
+ Status: pubdef.UserStatusNormal,
+ LoginType: pubdef.LoginTypeDevice,
+ IsRegister: false,
+ NickName: fmt.Sprintf("游客%d", uid),
+ Avatar: randomAvatar(),
+ InviteCode: generateUniqueInviteCode(ctx),
+ DistrictCode: normalizeChannel(req.Channel),
+ IsDirect: districtInfo.IsDirect,
+ RegIp: ip,
+ LastIp: ip,
+ DevID: ua.DevID,
+ Mac: strings.TrimSpace(req.Mac),
+ DevV2: strings.TrimSpace(req.DeviceId),
+ Device: ua.DevType,
+ SysType: ua.SysType,
+ AppVer: ua.Ver,
+ OriVer: ua.Ver,
+ SessionID: ua.SessionID,
+ DeviceBrand: ua.DeviceBrand,
+ DeviceModel: ua.DeviceModel,
+ SystemName: ua.SystemName,
+ SystemVersion: ua.SystemVersion,
+ LastLoginAt: now,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if nick := appg.GenerateNickname(); nick != "" {
+ user.NickName = nick
+ }
+ if serr := usermod.InsertOneUser(ctx, user); serr != nil {
+ log.Error("authser:createGuestUser InsertOneUser failed", log.E(serr), log.Any("devID", ua.DevID))
+ return nil, serr
+ }
+ return user, nil
+}
+
+func toLoginResp(ctx context.Context, user *usermod.User, token string, withCDN bool) *LoginResp {
+ avatar := user.Avatar
+ if withCDN {
+ cdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
+ avatar = netutil.MergeURL(cdn, user.Avatar)
+ }
+ return &LoginResp{
+ Token: token,
+ UserId: user.Id,
+ AccountNo: user.AccountNo,
+ Email: user.Email,
+ NickName: user.NickName,
+ Introduction: user.Introduction,
+ VipType: user.VipType,
+ CurrentVipExpire: user.CurrentVipExpire,
+ ShowCardId: user.ShowCardId,
+ ShowCardName: user.ShowCardName,
+ Status: user.Status,
+ Avatar: avatar,
+ InviteCode: user.InviteCode,
+ Type: user.Type,
+ LoginType: user.LoginType,
+ IsRegister: user.IsRegister,
+ PrivacyMode: user.PrivacyMode,
+ Score: user.Score,
+ Recharge: user.Recharge,
+ }
+}
+
+func normalizeChannel(channel string) string {
+ return strings.TrimSpace(channel)
+}
+
+func hasDistrictCode(code string) bool {
+ return strings.TrimSpace(code) != ""
+}
+
+func isBotRequest(ctx context.Context) bool {
+ gc, ok := ctx.(*gin.Context)
+ return ok && reptilerecord.IsBot(gc)
+}
+
+// crawlerGuestLogin 给爬虫签发可观看的虚拟游客 token,不建用户、不写日活、不上报。
+func crawlerGuestLogin() (*LoginResp, *srvdef.SrvErr) {
+ token, serr := generateCrawlerToken()
+ if serr != nil {
+ return nil, serr
+ }
+ return CrawlerLoginResp(token), nil
+}
+
+func applyChannelIfMissing(setDoc bson.M, user *usermod.User, channel string) {
+ if user == nil || hasDistrictCode(user.DistrictCode) {
+ return
+ }
+ if ch := normalizeChannel(channel); ch != "" {
+ setDoc[usermod.FN.User.DistrictCode] = ch
+ user.DistrictCode = ch
+ }
+}
diff --git a/app/service/coinser/coin_buy.go b/app/service/coinser/coin_buy.go
new file mode 100644
index 0000000..3ff4a02
--- /dev/null
+++ b/app/service/coinser/coin_buy.go
@@ -0,0 +1,133 @@
+package coinser
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/app/service/rechargeser/jiahua"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/vipordermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// BuyCoinResp 购买金币下单响应
+type BuyCoinResp struct {
+ TradeNo string `json:"tradeNo"`
+ PayUrl string `json:"payUrl"`
+ PayUrlMode string `json:"payUrlMode"`
+ IsOpenNewBrowser bool `json:"isOpenNewBrowser"`
+}
+
+// GetUserInfo 获取用户信息
+func GetUserInfo(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
+ return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+}
+
+// CreateCoinOrder 创建金币购买订单(统一写入 vip_order 表,OrderType=2)。
+// 防重复:15 秒内同一用户+同一套餐的 pending 订单直接返回原支付链接。
+func CreateCoinOrder(ctx context.Context,
+ userId int64, userName string,
+ packageId int64, payMode string,
+ sysType string, devType string, ip string,
+ districtCode string,
+) (*BuyCoinResp, *srvdef.SrvErr) {
+
+ pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
+ rechargepackagemod.FN.RechargePackage.Id: packageId,
+ rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeCoin,
+ rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
+ })
+ if serr != nil || pkg == nil {
+ return nil, srvdef.New(srvdef.ErrNoData, "金币套餐不存在或已下架", "")
+ }
+ coinAmount := pkg.BaseCoin + pkg.GiftCoin
+ if coinAmount <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "金币套餐到账数量无效", "")
+ }
+
+ now := time.Now()
+ dupFilter := bson.M{
+ vipordermod.FN.VipOrder.UserId: userId,
+ vipordermod.FN.VipOrder.CoinCfgId: packageId,
+ vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
+ vipordermod.FN.VipOrder.PayMode: payMode,
+ vipordermod.FN.VipOrder.Status: pubdef.VipOrderPending,
+ vipordermod.FN.VipOrder.CreatedAt: bson.M{"$gte": now.Add(-15 * time.Second)},
+ }
+ existing, _ := vipordermod.FindOneVipOrder(ctx, dupFilter)
+ if existing != nil && existing.PayUrl != "" {
+ return &BuyCoinResp{
+ TradeNo: existing.TradeNo,
+ PayUrl: existing.PayUrl,
+ PayUrlMode: existing.PayUrlMode,
+ IsOpenNewBrowser: true,
+ }, nil
+ }
+
+ tradeNo := pubdef.GetTrandNoWithUUID()
+
+ payInfo, serr := jiahua.SubmitOrder(ctx, userId, userName, tradeNo, pkg.Price, payMode, sysType, devType, ip)
+ if serr != nil {
+ return nil, serr
+ }
+
+ order := &vipordermod.VipOrder{
+ Id: mongos.Lid.VipOrder.Get(),
+ OrderType: vipordermod.OrderTypeCoin,
+ UserId: userId,
+ CoinCfgId: packageId, // 兼容历史字段,存购买套餐ID
+ Coin: coinAmount,
+ Amount: pkg.Price,
+ PayMode: payMode,
+ TradeNo: tradeNo,
+ Oid: payInfo.Oid,
+ Status: pubdef.VipOrderPending,
+ PayUrl: payInfo.PayUrl,
+ PayUrlMode: payInfo.PayUrlMode,
+ PayChannel: payInfo.PayChannel,
+ PayRate: payInfo.PayRate,
+ IPAddr: ip,
+ SysType: sysType,
+ DistrictCode: strings.TrimSpace(districtCode),
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if serr = vipordermod.InsertOneVipOrder(ctx, order); serr != nil {
+ log.Error("coinser:CreateCoinOrder InsertOneVipOrder failed",
+ log.Any("tradeNo", tradeNo), log.E(serr))
+ return nil, serr
+ }
+
+ return &BuyCoinResp{
+ TradeNo: tradeNo,
+ PayUrl: payInfo.PayUrl,
+ PayUrlMode: payInfo.PayUrlMode,
+ IsOpenNewBrowser: true,
+ }, nil
+}
+
+// GetCoinOrderHistory 查询用户金币购买记录
+func GetCoinOrderHistory(ctx context.Context, userId int64, page, pageSize int64) (vipordermod.VipOrderList, int64, *srvdef.SrvErr) {
+ filter := bson.M{
+ vipordermod.FN.VipOrder.UserId: userId,
+ vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
+ }
+ total, serr := vipordermod.CountVipOrder(ctx, filter)
+ if serr != nil {
+ return nil, 0, serr
+ }
+ skip := (page - 1) * pageSize
+ opts := options.Find().
+ SetSort(bson.M{vipordermod.FN.VipOrder.CreatedAt: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize)
+ list, serr := vipordermod.FindManyVipOrder(ctx, filter, opts)
+ return list, total, serr
+}
diff --git a/app/service/coinser/coin_trans.go b/app/service/coinser/coin_trans.go
new file mode 100644
index 0000000..344cb14
--- /dev/null
+++ b/app/service/coinser/coin_trans.go
@@ -0,0 +1,146 @@
+package coinser
+
+import (
+ "context"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ // coinTransDefaultPageSize 金币余额明细默认每页数量
+ coinTransDefaultPageSize int64 = 10
+ // coinTransMaxPageSize 金币余额明细最大每页数量,避免单次请求拉取过多流水
+ coinTransMaxPageSize int64 = 100
+)
+
+var (
+ // coinTransListProjection 余额明细只读取列表展示需要的字段,降低Mongo读取成本
+ coinTransListProjection = bson.D{
+ {Key: cointransmod.FN.CoinTrans.Id, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.FlowDir, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.TranType, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.Amount, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.BalanceBefore, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.BalanceAfter, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.OrderNo, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.RelatedId, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.Remark, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.CreatedAt, Value: 1},
+ }
+
+ // coinTransListSort 按交易时间倒序展示余额明细
+ coinTransListSort = bson.D{
+ {Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1},
+ }
+
+ // coinTransListHint 匹配 coin_trans 的 userId+createdAt 复合索引
+ coinTransListHint = bson.D{
+ {Key: cointransmod.FN.CoinTrans.UserId, Value: 1},
+ {Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1},
+ }
+)
+
+// GetCoinTransList 查询当前用户金币余额明细
+//
+// 只查询当前登录用户的流水,按交易时间倒序分页,pageSize 最大限制为100
+func GetCoinTransList(ctx context.Context, userId int64, page, pageSize int64) (*response.BasePageResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ page, pageSize = normalizeCoinTransPage(page, pageSize)
+ filter := bson.M{
+ cointransmod.FN.CoinTrans.UserId: userId,
+ }
+ total, err := cointransmod.CountCoinTrans(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ if total <= 0 {
+ return &response.BasePageResp{
+ TotalCount: 0,
+ PageSize: int(pageSize),
+ PageNum: int(page),
+ List: make([]*proto.CoinTransListItemResp, 0),
+ }, nil
+ }
+ list, err1 := cointransmod.FindManyCoinTrans(ctx, filter, options.Find().
+ SetProjection(coinTransListProjection).
+ SetSort(coinTransListSort).
+ SetHint(coinTransListHint).
+ SetSkip((page-1)*pageSize).
+ SetLimit(pageSize))
+ if err1 != nil {
+ return nil, err1
+ }
+ resp := make([]*proto.CoinTransListItemResp, 0, len(list))
+ for _, item := range list {
+ resp = append(resp, coinTransToListItemResp(item))
+ }
+ return &response.BasePageResp{
+ TotalCount: total,
+ PageSize: int(pageSize),
+ PageNum: int(page),
+ List: resp,
+ }, nil
+}
+
+// normalizeCoinTransPage 规范化金币流水分页参数
+//
+// page 小于等于0时按第1页处理;pageSize 未传时使用默认值,超过上限时截断
+func normalizeCoinTransPage(page, pageSize int64) (int64, int64) {
+ if page <= 0 {
+ page = 1
+ }
+ if pageSize <= 0 {
+ pageSize = coinTransDefaultPageSize
+ }
+ if pageSize > coinTransMaxPageSize {
+ pageSize = coinTransMaxPageSize
+ }
+ return page, pageSize
+}
+
+// coinTransToListItemResp 转换金币流水响应项
+//
+// amount 保持数据库中的正数金额,changeAmount 根据收支方向转换为带正负号的变动金额
+func coinTransToListItemResp(item *cointransmod.CoinTrans) *proto.CoinTransListItemResp {
+ title := item.Remark
+ if title == "" {
+ title = item.TranType.Desc()
+ }
+ changeAmount := item.Amount
+ if item.FlowDir == cointransmod.FlowOut {
+ changeAmount = -item.Amount
+ }
+ return &proto.CoinTransListItemResp{
+ Id: item.Id,
+ Title: title,
+ FlowDir: item.FlowDir,
+ FlowDirName: item.FlowDir.Desc(),
+ TranType: item.TranType,
+ TranTypeName: item.TranType.Desc(),
+ Amount: item.Amount,
+ ChangeAmount: changeAmount,
+ BalanceBefore: item.BalanceBefore,
+ BalanceAfter: item.BalanceAfter,
+ OrderNo: item.OrderNo,
+ RelatedId: item.RelatedId,
+ Remark: item.Remark,
+ CreatedAt: formatCoinTransTime(item.CreatedAt),
+ }
+}
+
+func formatCoinTransTime(t time.Time) string {
+ if t.IsZero() {
+ return ""
+ }
+ return t.In(utils.LocBJ).Format(pubdef.TimeISO)
+}
diff --git a/app/service/commentser/comment.go b/app/service/commentser/comment.go
new file mode 100644
index 0000000..d7a1079
--- /dev/null
+++ b/app/service/commentser/comment.go
@@ -0,0 +1,664 @@
+package commentser
+
+import (
+ "context"
+ "strconv"
+ "strings"
+ "time"
+ "unicode/utf8"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/reportser"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/redis"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ ObjectTypePlaylet int64 = 10 // 短剧评论
+ ObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
+ commentLevelFirst int64 = 1 // 一级评论
+ commentLevelSecond int64 = 2 // 二级评论
+ commentReviewPending int = 0 // 待审核
+ commentReviewPass int = 1 // 审核通过
+ commentMaxTextLen int = 200 // 评论最大字数
+ commentDefaultPageSize int64 = 20 // 默认分页条数
+ commentMaxPageSize int64 = 100 // 最大分页条数
+ commentSortHot int64 = 1 // 热门
+ commentSortNewest int64 = 2 // 最新
+ commentPlayletHeatInc int64 = 3 // 发布评论增加的短剧热度值
+ operateStatusYes int64 = 1 // 已操作
+ operateStatusNo int64 = 2 // 未操作
+)
+
+var commentListProjection = bson.D{
+ {Key: commentmod.FN.Comment.Id, Value: 1},
+ {Key: commentmod.FN.Comment.ObjectId, Value: 1},
+ {Key: commentmod.FN.Comment.ObjectType, Value: 1},
+ {Key: commentmod.FN.Comment.UserId, Value: 1},
+ {Key: commentmod.FN.Comment.UserName, Value: 1},
+ {Key: commentmod.FN.Comment.UserAvatar, Value: 1},
+ {Key: commentmod.FN.Comment.Text, Value: 1},
+ {Key: commentmod.FN.Comment.Level, Value: 1},
+ {Key: commentmod.FN.Comment.ParentsId, Value: 1},
+ {Key: commentmod.FN.Comment.ReplyCommentId, Value: 1},
+ {Key: commentmod.FN.Comment.ReplyUserName, Value: 1},
+ {Key: commentmod.FN.Comment.CommentCount, Value: 1},
+ {Key: commentmod.FN.Comment.CountLike, Value: 1},
+ {Key: commentmod.FN.Comment.CreatedAt, Value: 1},
+}
+
+// SendComment 发布评论
+func SendComment(ctx context.Context, userId int64, ip string, req *proto.CommentSendReq) *srvdef.SrvErr {
+ // 评论入口先做频控和基础参数校验,再校验短剧/剧集是否可评论,避免无效请求进入写库链路。
+ if userId <= 0 {
+ return srvdef.New(srvdef.InvalidToken, "请先登录", "")
+ }
+ if err := checkCommentLimit(ip, req); err != nil {
+ return err
+ }
+ text, err := normalizeCommentText(req)
+ if err != nil {
+ return err
+ }
+ objectName, playletId, err := validateCommentTarget(ctx, req.ObjectType, req.ObjectId)
+ if err != nil {
+ return err
+ }
+ level := commentLevelFirst
+ parentsId := int64(0)
+ replyCommentId := int64(0)
+ replyUserName := ""
+ if req.ParentsId > 0 {
+ replyComment, err := findReplyTargetComment(ctx, req)
+ if err != nil {
+ return err
+ }
+ level = commentLevelSecond
+ parentsId = req.ParentsId
+ replyCommentId = replyComment.Id
+ replyUserName = replyComment.UserName
+ }
+ user, err := findCommentUser(ctx, userId)
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ info := commentmod.CommentModel{
+ Id: mongos.Lid.Comment.Get(),
+ Ip: ip,
+ ObjectId: req.ObjectId,
+ ObjectName: objectName,
+ ObjectType: req.ObjectType,
+ UserId: user.Id,
+ UserName: commentUserName(user),
+ UserAvatar: user.Avatar,
+ Level: level,
+ ParentsId: parentsId,
+ ReplyCommentId: replyCommentId,
+ ReplyUserName: replyUserName,
+ Text: text,
+ CommentCount: 0,
+ CountLike: 0,
+ ReviewStatus: commentReviewPending,
+ UpdatedAt: now,
+ CreatedAt: now,
+ }
+ if err := commentmod.InsertOneComment(ctx, &info); err != nil {
+ log.Error("插入短剧评论失败", log.Any("评论信息", info), log.E(err))
+ return err
+ }
+ if err := incrementCommentPlayletHeatValue(ctx, playletId, commentPlayletHeatInc); err != nil {
+ log.Error("评论发布后更新短剧热度失败", log.Any("短剧ID", playletId), log.Any("评论信息", info), log.E(err))
+ return err
+ }
+ chapterId := int64(0)
+ if req.ObjectType == ObjectTypePlayletChapter {
+ chapterId = req.ObjectId
+ }
+ reportser.ReportPlayletComment(ctx, user, playletId, chapterId, text)
+ // app端发布评论先进入待审核状态,不立即增加短剧/剧集评论数;后台审核通过时再统一同步 countComment。
+ return nil
+}
+
+func findCommentUser(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
+ user, err := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ if err != nil {
+ log.Error("查询评论用户失败", log.Any("用户ID", userId), log.E(err))
+ return nil, err
+ }
+ return user, nil
+}
+
+func commentUserName(user *usermod.User) string {
+ if user == nil {
+ return ""
+ }
+ if name := strings.TrimSpace(user.NickName); name != "" {
+ return name
+ }
+ if name := strings.TrimSpace(user.AccountNo); name != "" {
+ return name
+ }
+ return "用户" + strconv.FormatInt(user.Id, 10)
+}
+
+// GetCommentPage 分页查询评论列表
+func GetCommentPage(ctx context.Context, userId int64, req *proto.GetCommentListReq) (*proto.GetCommentListResp, *srvdef.SrvErr) {
+ // 列表只查一级且已审核评论,并通过一次批量查询补齐当前用户点赞状态,避免列表 N+1 查询。
+ if req == nil || req.ObjectId <= 0 || !isPlayletCommentType(req.ObjectType) {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "")
+ }
+ pageNum, pageSize := normalizeCommentPage(req.PageNum, req.PageSize)
+ return getCommentPageData(ctx, userId, req.ObjectId, req.ObjectType, req.SortOrder, pageNum, pageSize)
+}
+
+// GetCommentList 兼容旧详情接口的评论查询
+func GetCommentList(ctx context.Context, objectId, objectType, page int64) (*proto.GetCommentListResp, *srvdef.SrvErr) {
+ pageNum, _ := normalizeCommentPage(page, commentDefaultPageSize)
+ return getCommentPageData(ctx, 0, objectId, objectType, commentSortNewest, pageNum, commentDefaultPageSize)
+}
+
+// SetCommentLike 设置当前用户评论点赞状态
+func SetCommentLike(ctx context.Context, userId int64, req *proto.CommentLikeSetReq) (*proto.CommentLikeSetResp, *srvdef.SrvErr) {
+ // 使用评论点赞关系表做去重;只有关系真实变化时才更新 _comment.countLike,重复点赞/取消不会重复增减计数。
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "")
+ }
+ if req == nil || req.CommentId <= 0 || req.IsLike == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "")
+ }
+ comment, err := findLikeTargetComment(ctx, req.CommentId)
+ if err != nil {
+ return nil, err
+ }
+ filter := bson.M{
+ commentlikemod.FN.CommentLike.UserId: userId,
+ commentlikemod.FN.CommentLike.CommentId: req.CommentId,
+ }
+ changed := false
+ if *req.IsLike {
+ inserted, err := commentlikemod.InsertOneCommentLike(ctx, &commentlikemod.CommentLike{
+ UserId: userId,
+ CommentId: req.CommentId,
+ ObjectId: comment.ObjectId,
+ ObjectType: comment.ObjectType,
+ })
+ if err != nil {
+ log.Error("新增评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", req.CommentId), log.E(err))
+ return nil, err
+ }
+ changed = inserted
+ } else {
+ deleted, err := commentlikemod.DeleteOneCommentLike(ctx, filter)
+ if err != nil {
+ log.Error("删除评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", req.CommentId), log.E(err))
+ return nil, err
+ }
+ changed = deleted > 0
+ }
+ if changed {
+ delta := int64(1)
+ if !*req.IsLike {
+ delta = -1
+ }
+ if err := incrementCommentLikeCount(ctx, req.CommentId, delta); err != nil {
+ rollbackCommentLike(ctx, userId, comment, *req.IsLike)
+ return nil, err
+ }
+ }
+ return &proto.CommentLikeSetResp{
+ CommentId: req.CommentId,
+ IsLike: operateStatusByFlag(*req.IsLike),
+ CountLike: applyLocalCounter(comment.CountLike, *req.IsLike, changed),
+ }, nil
+}
+
+func checkCommentLimit(ip string, req *proto.CommentSendReq) *srvdef.SrvErr {
+ curValue, err := redis.Incr(redisconst.CommentLimitKey(ip), redisconst.CommentLimitKeyExpire)
+ if err != nil {
+ log.Error("评论频率限制写入Redis失败", log.E(err), log.Any("IP", ip))
+ }
+ if curValue > 1 {
+ log.Error("评论操作过于频繁", log.Any("IP", ip), log.Any("请求参数", req))
+ return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
+ }
+ dayCurValue, err := redis.Incr(redisconst.CommentOneDayLimitKey(ip), redisconst.CommentDayLimitKeyExpire)
+ if err != nil {
+ log.Error("评论每日限制写入Redis失败", log.E(err), log.Any("IP", ip))
+ }
+ if dayCurValue > 50 {
+ log.Error("评论每日次数超限", log.Any("IP", ip), log.Any("请求参数", req))
+ return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
+ }
+ return nil
+}
+
+func normalizeCommentText(req *proto.CommentSendReq) (string, *srvdef.SrvErr) {
+ if req == nil || req.ObjectId <= 0 || !isPlayletCommentType(req.ObjectType) {
+ return "", srvdef.New(srvdef.ErrParamError, "参数错误", "")
+ }
+ text := strings.TrimSpace(req.Text)
+ if text == "" {
+ return "", srvdef.New(srvdef.CommentTextIsEmpty, "评论内容不能为空", "")
+ }
+ if utf8.RuneCountInString(text) > commentMaxTextLen {
+ return "", srvdef.New(srvdef.ErrParamError, "评论内容不能超过200字", "")
+ }
+ return text, nil
+}
+
+func validateCommentTarget(ctx context.Context, objectType, objectId int64) (string, int64, *srvdef.SrvErr) {
+ switch objectType {
+ case ObjectTypePlaylet:
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: objectId,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询评论短剧目标失败", log.Any("短剧ID", objectId), log.E(err))
+ return "", 0, srvdef.New(srvdef.ErrParamError, "短剧不存在或未上架", "")
+ }
+ return playlet.Title, playlet.Id, nil
+ case ObjectTypePlayletChapter:
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: objectId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询评论剧集目标失败", log.Any("剧集ID", objectId), log.E(err))
+ return "", 0, srvdef.New(srvdef.ErrParamError, "剧集不存在", "")
+ }
+ if _, err = playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ })); err != nil {
+ log.Error("查询剧集所属短剧失败", log.Any("剧集ID", objectId), log.Any("短剧ID", chapter.PlayletId), log.E(err))
+ return "", 0, srvdef.New(srvdef.ErrParamError, "短剧不存在或未上架", "")
+ }
+ if chapter.Title != "" {
+ return chapter.Title, chapter.PlayletId, nil
+ }
+ return "第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集", chapter.PlayletId, nil
+ default:
+ return "", 0, srvdef.New(srvdef.ErrParamError, "评论类型有误", "")
+ }
+}
+
+func findReplyTargetComment(ctx context.Context, req *proto.CommentSendReq) (*commentmod.CommentModel, *srvdef.SrvErr) {
+ parentComment, err := 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.Level: commentLevelFirst,
+ commentmod.FN.Comment.ParentsId: 0,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: commentmod.FN.Comment.Id, Value: 1},
+ {Key: commentmod.FN.Comment.UserName, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询回复父评论失败", log.Any("父评论ID", req.ParentsId), log.Any("请求参数", req), log.E(err))
+ return nil, srvdef.New(srvdef.ErrParamError, "父评论不存在或不可回复", "")
+ }
+ replyCommentId := req.ReplyCommentId
+ if replyCommentId <= 0 {
+ replyCommentId = req.ParentsId
+ }
+ if replyCommentId == parentComment.Id {
+ return parentComment, nil
+ }
+ replyComment, err := commentmod.FindOneComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: replyCommentId,
+ commentmod.FN.Comment.ObjectId: req.ObjectId,
+ commentmod.FN.Comment.ObjectType: req.ObjectType,
+ commentmod.FN.Comment.Level: commentLevelSecond,
+ commentmod.FN.Comment.ParentsId: req.ParentsId,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: commentmod.FN.Comment.Id, Value: 1},
+ {Key: commentmod.FN.Comment.UserName, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询被回复评论失败", log.Any("被回复评论ID", replyCommentId), log.Any("请求参数", req), log.E(err))
+ return nil, srvdef.New(srvdef.ErrParamError, "被回复评论不存在或不可回复", "")
+ }
+ return replyComment, nil
+}
+
+func incrementTargetCommentCount(ctx context.Context, objectType, objectId int64, delta int64) *srvdef.SrvErr {
+ switch objectType {
+ case ObjectTypePlaylet:
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: objectId,
+ }, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountComment: delta}}); err != nil {
+ log.Error("更新短剧评论数失败", log.Any("短剧ID", objectId), log.Any("增量", delta), log.E(err))
+ return err
+ }
+ case ObjectTypePlayletChapter:
+ if err := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: objectId,
+ }, bson.M{"$inc": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: delta}}); err != nil {
+ log.Error("更新剧集评论数失败", log.Any("剧集ID", objectId), log.Any("增量", delta), log.E(err))
+ return err
+ }
+ }
+ return nil
+}
+
+func incrementCommentPlayletHeatValue(ctx context.Context, playletId int64, delta int64) *srvdef.SrvErr {
+ if playletId <= 0 || delta == 0 {
+ return nil
+ }
+ return playletmod.UpdateOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: playletId,
+ }, bson.M{
+ "$inc": bson.M{
+ playletmod.FN.Playlet.HeatValue: delta,
+ },
+ })
+}
+
+func getCommentPageData(ctx context.Context, userId int64, objectId, objectType, sortOrder, pageNum, pageSize int64) (*proto.GetCommentListResp, *srvdef.SrvErr) {
+ resp := &proto.GetCommentListResp{
+ ObjectId: objectId,
+ TotalCount: 0,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommentInfo, 0),
+ }
+ if objectId <= 0 || !isPlayletCommentType(objectType) {
+ return resp, nil
+ }
+ filter := bson.M{
+ commentmod.FN.Comment.ObjectId: objectId,
+ commentmod.FN.Comment.ObjectType: objectType,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ commentmod.FN.Comment.Level: commentLevelFirst,
+ commentmod.FN.Comment.ParentsId: 0,
+ }
+ total, err := commentmod.CountComments(ctx, filter)
+ if err != nil {
+ log.Warn("统计评论列表失败", log.Any("filter", filter), log.E(err))
+ return resp, err
+ }
+ resp.TotalCount = total
+ if total == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= total {
+ return resp, nil
+ }
+ opts := options.Find().
+ SetProjection(commentListProjection).
+ SetSkip(skip).
+ SetLimit(pageSize).
+ SetSort(commentListSort(sortOrder))
+ list, err := commentmod.FindManyComment(ctx, filter, opts)
+ if err != nil {
+ log.Warn("查询评论列表失败", log.Any("filter", filter), log.E(err))
+ return resp, err
+ }
+ parentIds := commentIdsFromList(list)
+ children, err := findChildComments(ctx, parentIds)
+ if err != nil {
+ return resp, err
+ }
+ allComments := make(commentmod.CommentModelList, 0, len(list)+len(children))
+ allComments = append(allComments, list...)
+ allComments = append(allComments, children...)
+ likedCommentIds, err := findLikedCommentIdMap(ctx, userId, allComments)
+ if err != nil {
+ return resp, err
+ }
+ childrenByParent := make(map[int64][]proto.CommentInfo)
+ for _, item := range children {
+ if item == nil {
+ continue
+ }
+ _, liked := likedCommentIds[item.Id]
+ childrenByParent[item.ParentsId] = append(childrenByParent[item.ParentsId], buildCommentInfo(ctx, item, liked))
+ }
+ resp.List = make([]proto.CommentInfo, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ _, liked := likedCommentIds[item.Id]
+ info := buildCommentInfo(ctx, item, liked)
+ info.Children = childrenByParent[item.Id]
+ resp.List = append(resp.List, info)
+ }
+ return resp, nil
+}
+
+func commentListSort(sortOrder int64) bson.D {
+ switch sortOrder {
+ case commentSortHot:
+ return bson.D{
+ {Key: commentmod.FN.Comment.CountLike, Value: -1},
+ {Key: commentmod.FN.Comment.Id, Value: -1},
+ }
+ case commentSortNewest:
+ fallthrough
+ default:
+ return bson.D{
+ {Key: commentmod.FN.Comment.CreatedAt, Value: -1},
+ {Key: commentmod.FN.Comment.Id, Value: -1},
+ }
+ }
+}
+
+func commentIdsFromList(list commentmod.CommentModelList) []int64 {
+ commentIds := make([]int64, 0, len(list))
+ for _, item := range list {
+ if item != nil {
+ commentIds = append(commentIds, item.Id)
+ }
+ }
+ return commentIds
+}
+
+func findChildComments(ctx context.Context, parentIds []int64) (commentmod.CommentModelList, *srvdef.SrvErr) {
+ if len(parentIds) == 0 {
+ return make(commentmod.CommentModelList, 0), nil
+ }
+ filter := bson.M{
+ commentmod.FN.Comment.ParentsId: bson.M{"$in": parentIds},
+ commentmod.FN.Comment.Level: commentLevelSecond,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ }
+ opts := options.Find().
+ SetProjection(commentListProjection).
+ SetSort(bson.D{
+ {Key: commentmod.FN.Comment.ParentsId, Value: 1},
+ {Key: commentmod.FN.Comment.CreatedAt, Value: 1},
+ {Key: commentmod.FN.Comment.Id, Value: 1},
+ })
+ children, err := commentmod.FindManyComment(ctx, filter, opts)
+ if err != nil {
+ log.Warn("查询二级评论列表失败", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ return children, nil
+}
+
+func findLikedCommentIdMap(ctx context.Context, userId int64, list commentmod.CommentModelList) (map[int64]struct{}, *srvdef.SrvErr) {
+ likedMap := make(map[int64]struct{})
+ if userId <= 0 || len(list) == 0 {
+ return likedMap, nil
+ }
+ commentIds := commentIdsFromList(list)
+ if len(commentIds) == 0 {
+ return likedMap, nil
+ }
+ likes, err := commentlikemod.FindManyCommentLike(ctx, bson.M{
+ commentlikemod.FN.CommentLike.UserId: userId,
+ commentlikemod.FN.CommentLike.CommentId: bson.M{"$in": commentIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: commentlikemod.FN.CommentLike.CommentId, Value: 1},
+ }))
+ if err != nil {
+ log.Error("批量查询评论点赞状态失败", log.Any("用户ID", userId), log.E(err))
+ return nil, err
+ }
+ for _, item := range likes {
+ if item != nil {
+ likedMap[item.CommentId] = struct{}{}
+ }
+ }
+ return likedMap, nil
+}
+
+func buildCommentInfo(ctx context.Context, item *commentmod.CommentModel, liked bool) proto.CommentInfo {
+ return proto.CommentInfo{
+ Id: item.Id,
+ ObjectId: item.ObjectId,
+ ObjectType: item.ObjectType,
+ UserId: item.UserId,
+ UserName: item.UserName,
+ UserAvatar: withCommentAvatarDomain(ctx, item.UserAvatar),
+ Text: item.Text,
+ Level: item.Level,
+ ParentsId: item.ParentsId,
+ ReplyCommentId: item.ReplyCommentId,
+ ReplyUserName: item.ReplyUserName,
+ CommentCount: item.CommentCount,
+ CountLike: item.CountLike,
+ IsLike: operateStatusByFlag(liked),
+ CreatedAt: item.CreatedAt,
+ Children: make([]proto.CommentInfo, 0),
+ }
+}
+
+func withCommentAvatarDomain(ctx context.Context, path string) string {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return ""
+ }
+ imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
+ if imgCdn == "" {
+ return path
+ }
+ return netutil.MergeURL(imgCdn, path)
+}
+
+func findLikeTargetComment(ctx context.Context, commentId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
+ comment, err := commentmod.FindOneComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: commentId,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ commentmod.FN.Comment.ObjectType: bson.M{
+ "$in": []int64{ObjectTypePlaylet, ObjectTypePlayletChapter},
+ },
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: commentmod.FN.Comment.Id, Value: 1},
+ {Key: commentmod.FN.Comment.ObjectId, Value: 1},
+ {Key: commentmod.FN.Comment.ObjectType, Value: 1},
+ {Key: commentmod.FN.Comment.CountLike, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询评论点赞目标失败", log.Any("评论ID", commentId), log.E(err))
+ return nil, srvdef.New(srvdef.ErrParamError, "评论不存在", "")
+ }
+ return comment, nil
+}
+
+func incrementCommentLikeCount(ctx context.Context, commentId int64, delta int64) *srvdef.SrvErr {
+ filter := bson.M{
+ commentmod.FN.Comment.Id: commentId,
+ }
+ if delta < 0 {
+ filter[commentmod.FN.Comment.CountLike] = bson.M{"$gt": 0}
+ }
+ err := commentmod.UpdateOneComment(ctx, filter, bson.M{
+ "$inc": bson.M{
+ commentmod.FN.Comment.CountLike: delta,
+ },
+ })
+ if err != nil {
+ if delta < 0 && err.Code == srvdef.ErrNoData {
+ log.Warn("评论点赞数已为0,跳过扣减", log.Any("评论ID", commentId))
+ return nil
+ }
+ log.Error("更新评论点赞数失败", log.Any("评论ID", commentId), log.Any("增量", delta), log.E(err))
+ return err
+ }
+ return nil
+}
+
+func rollbackCommentLike(ctx context.Context, userId int64, comment *commentmod.CommentModel, enabled bool) {
+ filter := bson.M{
+ commentlikemod.FN.CommentLike.UserId: userId,
+ commentlikemod.FN.CommentLike.CommentId: comment.Id,
+ }
+ if enabled {
+ if _, err := commentlikemod.DeleteOneCommentLike(ctx, filter); err != nil {
+ log.Error("回滚评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", comment.Id), log.E(err))
+ }
+ return
+ }
+ if _, err := commentlikemod.InsertOneCommentLike(ctx, &commentlikemod.CommentLike{
+ UserId: userId,
+ CommentId: comment.Id,
+ ObjectId: comment.ObjectId,
+ ObjectType: comment.ObjectType,
+ }); err != nil {
+ log.Error("恢复评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", comment.Id), log.E(err))
+ }
+}
+
+func normalizeCommentPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = commentDefaultPageSize
+ }
+ if pageSize > commentMaxPageSize {
+ pageSize = commentMaxPageSize
+ }
+ return pageNum, pageSize
+}
+
+func isPlayletCommentType(objectType int64) bool {
+ return objectType == ObjectTypePlaylet || objectType == ObjectTypePlayletChapter
+}
+
+func applyLocalCounter(current int64, enabled bool, changed bool) int64 {
+ if !changed {
+ return current
+ }
+ if enabled {
+ return current + 1
+ }
+ if current <= 0 {
+ return 0
+ }
+ return current - 1
+}
+
+func operateStatusByFlag(enabled bool) int64 {
+ if enabled {
+ return operateStatusYes
+ }
+ return operateStatusNo
+}
diff --git a/app/service/configser/announcement.go b/app/service/configser/announcement.go
new file mode 100644
index 0000000..f37e2ca
--- /dev/null
+++ b/app/service/configser/announcement.go
@@ -0,0 +1,97 @@
+package configser
+
+import (
+ "strconv"
+ "videoweb/app/api/proto"
+ "videoweb/app/appg"
+ "videoweb/pub/adcenter"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/storage/proxy"
+)
+
+// 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
+ }
+ adMap := make(map[string]adcenter.AdCacheInfo)
+ for _, loc := 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(),
+ })
+ adMap[ad.AdvertiseCode] = adcenter.AdCacheInfo{
+ AdvertiseCode: ad.AdvertiseCode,
+ CustomerCode: ad.CustomerCode,
+ AdvertiseName: ad.AdvertiseName,
+ AdvertiseUrl: ad.AdvertiseUrl,
+ AdvertiseIcon: ad.AdvertiseIcon,
+ AdvertiseType: ad.AdvertiseType,
+ AdMode: ad.AdMode,
+ AppCode: ad.AppCode,
+ LocationType: ad.LocationType,
+ AdvertiseDesc: ad.AdvertiseDesc,
+ PageType: ad.PageType,
+ StartTimeStamp: ad.StartTimeStamp,
+ EndTimeStamp: ad.EndTimeStamp,
+ Sort: ad.Sort,
+ AdExtData: ad.AdExtData,
+ DeptCode: loc.DeptCode,
+ MerchantCode: loc.MerchantCode,
+ AdvertiseLocationCode: loc.AdvertiseLocationCode,
+ AdvertiseLocationName: loc.AdvertiseLocationName,
+ DisplayMode: loc.DisplayMode,
+ AdvertiseHeight: loc.AdvertiseHeight,
+ AdvertiseWidth: loc.AdvertiseWidth,
+ MaterialType: loc.MaterialType,
+ AdLimit: loc.AdLimit,
+ }
+ }
+ switch pos {
+ case 1: //弹窗
+ resp.DialogList = ads
+ case 2: // 应用
+ resp.DetailTopList = ads
+ case 3: // 横幅
+ resp.NineGridList = ads
+ resp.DetailbannerList = ads
+
+ case 12:
+ resp.AList = ads
+ case 13:
+ resp.BList = ads
+ }
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.Advitise = resp
+ CacheMgr.AdvitiseMap = adMap
+}
+
+func GetAdvertiseLocation(adverId string) (res adcenter.AdCacheInfo) {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ if ad, ok := CacheMgr.AdvitiseMap[adverId]; ok {
+ res = ad
+ }
+ return
+}
diff --git a/app/service/configser/catch.go b/app/service/configser/catch.go
new file mode 100644
index 0000000..3e40661
--- /dev/null
+++ b/app/service/configser/catch.go
@@ -0,0 +1,576 @@
+package configser
+
+import (
+ "context"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/adcenter"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/announcementmod"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/ipblackmod"
+ "videoweb/pub/models/richtextcontentmod"
+ "videoweb/pub/models/videotagmod"
+
+ "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 // 系统配置缓存
+ Advitise proto.AdvitiseData
+ AdvitiseMap map[string]adcenter.AdCacheInfo
+ TagCategoryList []proto.VideoTagCategoryData
+ TotalHistory int64
+ IpBlackMaps map[string]struct{}
+ TagMaps map[int64]proto.TagsInfoData
+ NewAddressInfo proto.NewAddressInfo
+ AppRichTexts []proto.AppRichTextItemResp
+ Announcements []*proto.AnnouncementItem
+}
+
+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.IpBlackMaps = make(map[string]struct{})
+}
+
+func Start() {
+ ctx := context.Background()
+ dataReport.StartSDKConfigCron()
+ UpdateConfigInfo(ctx)
+ UpdateAdvitiseDataFromJt()
+ updateIpBlack()
+ updateTagsCache()
+ updateAppRichTexts()
+ updateAnnouncements()
+ _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ UpdateConfigInfo(ctx)
+ UpdateAdvitiseDataFromJt()
+ updateIpBlack()
+ updateTagsCache()
+ updateAppRichTexts()
+ })
+ _, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
+ updateAnnouncements()
+ })
+
+ skdCron.Start()
+}
+
+func updateTagsCache() {
+ filter := bson.M{videotagmod.FN.VideoTagInfo.Status: true}
+ opts := &options.FindOptions{}
+ opts.SetLimit(5000)
+ opts.SetSort(bson.M{videotagmod.FN.VideoTagInfo.Sort: -1})
+ list, err := videotagmod.FindManyVideoTagInfos(context.Background(), filter, opts)
+ if err != nil {
+ log.Warn("updateTaggCache.FFindManyVideoTagInfos", log.Any("err", err))
+ return
+ }
+ tempMaps := make(map[int64]proto.TagsInfoData)
+ for _, v := range list {
+ tempMaps[v.Id] = proto.TagsInfoData{
+ Id: v.Id,
+ Name: v.Title,
+ SeoInfo: v.SeoInfo,
+ }
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.TagMaps = tempMaps
+}
+
+func UpdateConfigInfo(ctx context.Context) {
+ config := proto.ConfigInfo{}
+ newAddressInfo := proto.NewAddressInfo{}
+
+ 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.OfficialAppAddr = sysCfg.OfficialAppAddr
+ 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.AdvitiesTopicNum = sysCfg.AdvitiesTopicNum
+
+ config.DHKeyworld = sysCfg.DHKeyworld
+ config.DHDescription = sysCfg.DHDescription
+ config.DHSiteName = sysCfg.DHSiteName
+ config.DHTitle = sysCfg.DHTitle
+ config.ActressBackGround = netutil.MergeURL(sysCfg.ImgAppCdn, sysCfg.ActressBackGround)
+ config.ReportSDKConfig = dataReport.GetSDKDomainConfig()
+ config.WatermarkImage = sysCfg.WatermarkImage
+ config.CustomerServiceHost = sysCfg.CustomerServiceHost
+ config.ChannelCode = sysCfg.ChannelCode
+ config.EnableH265 = sysCfg.EnableH265
+
+ // NewAddressInfo 来源于系统配置 sys_cfg,用于只返回前端需要展示的最新地址信息。
+ newAddressInfo.NewUrlAddr = sysCfg.NewUrlAddr
+ newAddressInfo.NewUrlAddr1 = sysCfg.NewUrlAddr1
+ newAddressInfo.NewUrlAddr2 = sysCfg.NewUrlAddr2
+ newAddressInfo.ForeverUrlAddr = sysCfg.ForeverUrlAddr
+ newAddressInfo.GmailAddr = sysCfg.GmailAddr
+ newAddressInfo.TgGroup = sysCfg.TgGroup
+ newAddressInfo.Telegram = sysCfg.Telegram
+ newAddressInfo.CustomerServiceHost = sysCfg.CustomerServiceHost
+ newAddressInfo.TwitterAddr = sysCfg.TwitterAddr
+ newAddressInfo.PdfAddr = sysCfg.PdfAddr
+ newAddressInfo.AppAddr = sysCfg.AppAddr
+ newAddressInfo.OfficialAppAddr = sysCfg.OfficialAppAddr
+
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.Config = config
+ CacheMgr.NewAddressInfo = newAddressInfo
+
+}
+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 GetNewAddress() proto.NewAddressInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return CacheMgr.NewAddressInfo
+}
+
+// GetFooterInfo 查询APP底部信息
+func GetFooterInfo() proto.FooterInfo {
+ // 底部富文本和系统配置都来自缓存,接口请求不直接访问Mongo。
+ config := GetConfigInfo()
+ return proto.FooterInfo{
+ Brand: buildFooterBrand(config),
+ NavList: buildFooterNavList(),
+ ContactList: buildFooterContactList(config),
+ }
+}
+
+func getFooterRichTextMap() map[string]proto.AppRichTextItemResp {
+ data := getCachedAppRichTexts()
+ if len(data) == 0 {
+ updateAppRichTexts()
+ data = getCachedAppRichTexts()
+ }
+ result := make(map[string]proto.AppRichTextItemResp, len(data))
+ for _, item := range data {
+ title := strings.TrimSpace(item.Title)
+ if title == "" {
+ continue
+ }
+ result[title] = item
+ }
+ return result
+}
+
+func buildFooterBrand(config proto.ConfigInfo) proto.FooterBrand {
+ return proto.FooterBrand{
+ Title: config.WebSiteName,
+ Content: config.WebDescription,
+ }
+}
+
+func buildFooterNavList() []proto.TranslationCategory {
+ bottom := translationser.GetTranslationBottomList()
+ return append([]proto.TranslationCategory{}, bottom.CategoryList...)
+}
+
+func buildFooterContactList(config proto.ConfigInfo) []proto.FooterContact {
+ // 联系方式名称按页面截图固定返回,右侧只包含 E-mail、商务合作、官方群 三项。
+ return []proto.FooterContact{
+ {Name: "E-mail", Value: config.GmailAddr},
+ {Name: "商务合作:立即联系", Value: config.Telegram},
+ {Name: "官方群:立即加入", Value: firstNotEmpty(config.TgGroup, config.TelegramAdd)},
+ }
+}
+
+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)}
+}
+
+func getCachedAppRichTexts() []proto.AppRichTextItemResp {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return append([]proto.AppRichTextItemResp{}, CacheMgr.AppRichTexts...)
+}
+
+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
+}
+
+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
+}
+
+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
+}
+
+// GetSiteSeoInfo 返回全站SEO配置,供统一渲染 title、meta keywords、description 和 canonical。
+func GetSiteSeoInfo() proto.SeoInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ webAddr := normalizeWebAddr(CacheMgr.Config.WebAddr)
+ return proto.SeoInfo{
+ Title: CacheMgr.Config.WebTitle,
+ Keywords: append([]string{}, CacheMgr.Config.WebKeyworld...),
+ Description: CacheMgr.Config.WebDescription,
+ WebSiteName: CacheMgr.Config.WebSiteName,
+ CanonicalURL: webAddr,
+ }
+}
+
+// GetNavSeoInfo 返回导航页SEO配置
+func GetNavSeoInfo() proto.SeoNavInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return proto.SeoNavInfo{
+ DHTitle: CacheMgr.Config.DHTitle,
+ DHKeyworld: append([]string{}, CacheMgr.Config.DHKeyworld...),
+ DHDescription: CacheMgr.Config.DHDescription,
+ DHSiteName: CacheMgr.Config.DHSiteName,
+ }
+}
+
+// GetNavPageList 返回导航页线路集合。
+func GetNavPageList() []proto.NavPageItem {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ list := make([]proto.NavPageItem, 0, 3)
+ for _, item := range []proto.NavPageItem{
+ {Line: 1, Url: strings.TrimSpace(CacheMgr.Config.DHNewUrlAddr1)},
+ {Line: 2, Url: strings.TrimSpace(CacheMgr.Config.DHNewUrlAddr2)},
+ {Line: 3, Url: strings.TrimSpace(CacheMgr.Config.DHNewUrlAddr3)},
+ } {
+ if item.Url == "" {
+ continue
+ }
+ list = append(list, item)
+ }
+ return list
+}
+
+func normalizeWebAddr(webAddr string) string {
+ webAddr = strings.TrimSpace(webAddr)
+ webAddr = strings.TrimRight(webAddr, "/")
+ if webAddr == "" {
+ return ""
+ }
+ if !strings.HasPrefix(webAddr, "http://") && !strings.HasPrefix(webAddr, "https://") {
+ return "https://" + webAddr
+ }
+ return webAddr
+}
+
+func GetRandCDN() string {
+ config := GetConfigInfo()
+ return config.TsCdn
+}
+
+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 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 updateAnnouncements() {
+ list, err := queryAnnouncements(context.Background())
+ if err != nil {
+ log.Warn("查询公告失败", log.E(err))
+ return
+ }
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.Announcements = list
+}
+
+func queryAnnouncements(ctx context.Context) ([]*proto.AnnouncementItem, *srvdef.SrvErr) {
+ filter := bson.M{announcementmod.FN.Announcement.Status: true}
+ opts := options.Find().
+ SetProjection(bson.M{
+ announcementmod.FN.Announcement.Id: 1,
+ announcementmod.FN.Announcement.Title: 1,
+ announcementmod.FN.Announcement.Content: 1,
+ }).
+ SetSort(bson.D{
+ {Key: announcementmod.FN.Announcement.Sort, Value: -1},
+ {Key: announcementmod.FN.Announcement.UpdatedAt, Value: -1},
+ }).
+ SetLimit(50)
+ list, err := announcementmod.FindManyAnnouncement(ctx, filter, opts)
+ if err != nil {
+ log.Warn("查询公告失败", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ resp := make([]*proto.AnnouncementItem, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ resp = append(resp, &proto.AnnouncementItem{
+ Id: item.Id,
+ Title: item.Title,
+ Content: item.Content,
+ })
+ }
+ return resp, nil
+}
+
+// GetAnnouncements 返回启用中的公告缓存,每10分钟刷新。
+func GetAnnouncements() []*proto.AnnouncementItem {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ if len(CacheMgr.Announcements) == 0 {
+ return make([]*proto.AnnouncementItem, 0)
+ }
+ resp := make([]*proto.AnnouncementItem, len(CacheMgr.Announcements))
+ copy(resp, CacheMgr.Announcements)
+ return resp
+}
+
+func CheckIpBlack(ip string) bool {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ if _, ok := CacheMgr.IpBlackMaps[ip]; ok {
+ return true
+ }
+ return false
+}
+
+func GetVideoTagCategoryDataList() []proto.VideoTagCategoryData {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ rsl := make([]proto.VideoTagCategoryData, 0)
+ for _, v := range CacheMgr.TagCategoryList {
+ rsl = append(rsl, v)
+ }
+ return rsl
+}
+func Stop() {
+ dataReport.StopSDKConfigCron()
+ skdCron.Stop()
+}
+
+func GetTag(tagId int64) *proto.TagsInfoData {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ tag, ok := CacheMgr.TagMaps[tagId]
+ if !ok {
+ return nil
+ }
+ return &tag
+}
diff --git a/app/service/configser/cdn.go b/app/service/configser/cdn.go
new file mode 100644
index 0000000..7a3c875
--- /dev/null
+++ b/app/service/configser/cdn.go
@@ -0,0 +1,47 @@
+package configser
+
+import (
+ "context"
+ "strings"
+ "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)
+}
diff --git a/app/service/configser/h265.go b/app/service/configser/h265.go
new file mode 100644
index 0000000..6bc6669
--- /dev/null
+++ b/app/service/configser/h265.go
@@ -0,0 +1,22 @@
+package configser
+
+import "videoweb/app/api/proto"
+
+// 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
+}
diff --git a/app/service/contactusser/contactus.go b/app/service/contactusser/contactus.go
new file mode 100644
index 0000000..32f3ec7
--- /dev/null
+++ b/app/service/contactusser/contactus.go
@@ -0,0 +1,93 @@
+package contactusser
+
+import (
+ "context"
+ "net/mail"
+ "strings"
+ "time"
+ "unicode/utf8"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/contactusmod"
+)
+
+const (
+ contactUsMaxEmailLen = 100
+ contactUsMaxTitleLen = 100
+ contactUsMaxContentLen = 1000
+ contactUsMaxAttachmentNum = 9
+ contactUsSubmitMessage = "提交成功,我们会在24小时内回复。"
+)
+
+const (
+ ContactUsTypeOrder int32 = 1 // 订单问题
+ ContactUsTypeAccount int32 = 2 // 账号问题
+ ContactUsTypeFeature int32 = 3 // 功能问题
+ ContactUsTypeExp int32 = 4 // 体验问题
+ ContactUsTypeOther int32 = 5 // 其他
+)
+
+// AddContactUs 提交联系我们记录
+func AddContactUs(ctx context.Context, userId int64, req *proto.ContactUsAddReq) (*proto.ContactUsAddResp, *srvdef.SrvErr) {
+ if req == nil || userId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ email := strings.TrimSpace(req.Email)
+ if !isValidEmail(email) || utf8.RuneCountInString(email) > contactUsMaxEmailLen {
+ return nil, srvdef.New(srvdef.ErrParamError, "邮箱格式错误")
+ }
+ if !isValidContactUsType(req.Type) {
+ return nil, srvdef.New(srvdef.ErrParamError, "问题类型无效")
+ }
+ title := strings.TrimSpace(req.Title)
+ if title == "" || utf8.RuneCountInString(title) > contactUsMaxTitleLen {
+ return nil, srvdef.New(srvdef.ErrParamError, "标题不能为空且不能超过100字")
+ }
+ content := strings.TrimSpace(req.Content)
+ if content == "" || utf8.RuneCountInString(content) > contactUsMaxContentLen {
+ return nil, srvdef.New(srvdef.ErrParamError, "内容不能为空且不能超过1000字")
+ }
+ attachments := normalizeAttachments(req.Attachments)
+ if len(attachments) > contactUsMaxAttachmentNum {
+ return nil, srvdef.New(srvdef.ErrParamError, "附件不能超过9个")
+ }
+ now := time.Now()
+ doc := &contactusmod.ContactUs{
+ UserId: userId,
+ Email: email,
+ QuestionType: req.Type,
+ Title: title,
+ Content: content,
+ Attachments: attachments,
+ Status: 0,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if err := contactusmod.InsertOneContactUs(ctx, doc); err != nil {
+ return nil, err
+ }
+ return &proto.ContactUsAddResp{Message: contactUsSubmitMessage}, nil
+}
+
+func isValidEmail(email string) bool {
+ if email == "" {
+ return false
+ }
+ _, err := mail.ParseAddress(email)
+ return err == nil
+}
+
+func isValidContactUsType(questionType int32) bool {
+ return questionType >= ContactUsTypeOrder && questionType <= ContactUsTypeOther
+}
+
+func normalizeAttachments(rawAttachments []string) []string {
+ attachments := make([]string, 0, len(rawAttachments))
+ for _, attachment := range rawAttachments {
+ attachment = strings.TrimSpace(attachment)
+ if attachment != "" {
+ attachments = append(attachments, attachment)
+ }
+ }
+ return attachments
+}
diff --git a/app/service/feedbackser/feedback.go b/app/service/feedbackser/feedback.go
new file mode 100644
index 0000000..bfa28e0
--- /dev/null
+++ b/app/service/feedbackser/feedback.go
@@ -0,0 +1,77 @@
+package feedbackser
+
+import (
+ "context"
+ "strings"
+ "time"
+ "unicode/utf8"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/feedbackmod"
+)
+
+const (
+ feedbackMaxContentLen = 500
+ feedbackMaxContactLen = 100
+ feedbackMaxScreenshotNum = 9
+ feedbackSubmitMessage = "反馈提交成功"
+)
+
+const ( //问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他
+ FeedbackTypeFeature int32 = 1
+ FeedbackTypePlay int32 = 2
+ FeedbackTypeAccount int32 = 3
+ FeedbackTypeOther int32 = 4
+)
+
+// AddFeedback 提交用户意见反馈
+func AddFeedback(ctx context.Context, userId int64, req *proto.FeedbackReq) (*proto.FeedbackResp, *srvdef.SrvErr) {
+ if req == nil || userId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ if !isValidFeedbackType(req.Type) {
+ return nil, srvdef.New(srvdef.ErrParamError, "问题类型无效")
+ }
+ content := strings.TrimSpace(req.Content)
+ if content == "" || utf8.RuneCountInString(content) > feedbackMaxContentLen {
+ return nil, srvdef.New(srvdef.ErrParamError, "描述内容不能为空且不能超过500字")
+ }
+ contact := strings.TrimSpace(req.Contact)
+ if utf8.RuneCountInString(contact) > feedbackMaxContactLen {
+ return nil, srvdef.New(srvdef.ErrParamError, "联系方式不能超过100字")
+ }
+ screenshots := normalizeScreenshots(req.Screenshots)
+ if len(screenshots) > feedbackMaxScreenshotNum {
+ return nil, srvdef.New(srvdef.ErrParamError, "问题截图不能超过9张")
+ }
+ now := time.Now()
+ doc := &feedbackmod.Feedback{
+ UserId: userId,
+ FeedbackType: req.Type,
+ Content: content,
+ Screenshots: screenshots,
+ Contact: contact,
+ Status: 0,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if err := feedbackmod.InsertOneFeedback(ctx, doc); err != nil {
+ return nil, err
+ }
+ return &proto.FeedbackResp{Message: feedbackSubmitMessage}, nil
+}
+
+func isValidFeedbackType(feedbackType int32) bool {
+ return feedbackType >= FeedbackTypeFeature && feedbackType <= FeedbackTypeOther
+}
+
+func normalizeScreenshots(rawScreenshots []string) []string {
+ screenshots := make([]string, 0, len(rawScreenshots))
+ for _, screenshot := range rawScreenshots {
+ screenshot = strings.TrimSpace(screenshot)
+ if screenshot != "" {
+ screenshots = append(screenshots, screenshot)
+ }
+ }
+ return screenshots
+}
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/playletcache/cache.go b/app/service/playletcache/cache.go
new file mode 100644
index 0000000..75754b1
--- /dev/null
+++ b/app/service/playletcache/cache.go
@@ -0,0 +1,575 @@
+package playletcache
+
+import (
+ "context"
+ "sort"
+ "strings"
+ "sync"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/hotsearchmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/pubdef"
+
+ "github.com/robfig/cron/v3"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ skdCron *cron.Cron
+ cache struct {
+ lock sync.RWMutex
+ themeByID map[int64]*ThemeItem
+ themeBySlug map[string]*ThemeItem
+ tagByID map[int64]*TagItem
+ tagBySlug map[string]*TagItem
+ categoryByID map[int64]*CategoryItem
+ categoryBySlug map[string]*CategoryItem
+ hotSearch []HotSearchItem
+ guessLike []*playletmod.Playlet
+ }
+)
+
+// HotSearchItem 热搜词缓存项
+type HotSearchItem struct {
+ Keyword string
+ Count int64
+}
+
+// ThemeItem 题材缓存项(含 SEO)
+type ThemeItem struct {
+ Id int64
+ Name string
+ Slug string
+ Icon string
+ Rank int64
+ Status bool
+ IsRecommend bool
+ PlayletCount int64
+ SeoInfo pubdef.SeoValue
+}
+
+// TagItem 标签缓存项(含 SEO)
+type TagItem struct {
+ Id int64
+ Name string
+ Slug string
+ Rank int64
+ Status bool
+ SeoInfo pubdef.SeoValue
+}
+
+// CategoryItem 分类缓存项(含 SEO)
+type CategoryItem struct {
+ Id int64
+ Name string
+ Slug string
+ Desc string
+ Icon string
+ Rank int64
+ Status bool
+ SeoInfo pubdef.SeoValue
+}
+
+// Start 启动题材/标签/分类/热搜词/猜你喜欢缓存,每 10 分钟刷新一次。
+func Start() {
+ skdCron = cron.New(cron.WithSeconds())
+ UpdateCache(context.Background())
+ _, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
+ UpdateCache(context.Background())
+ })
+ skdCron.Start()
+}
+
+// UpdateCache 从 Mongo 全量刷新题材、标签、分类、热搜词和猜你喜欢缓存。
+func UpdateCache(ctx context.Context) {
+ updateThemeCache(ctx)
+ updateTagCache(ctx)
+ updateCategoryCache(ctx)
+ updateHotSearchCache(ctx)
+ updateGuessLikeCache(ctx)
+}
+
+func updateThemeCache(ctx context.Context) {
+ list, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{}, 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.Status, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.IsRecommend, Value: 1},
+ {Key: "seoInfo", Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Rank, Value: -1},
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: -1},
+ }))
+ if err != nil {
+ log.Warn("playletcache.updateThemeCache failed", log.E(err))
+ return
+ }
+ countMap := countUpPlayletsByTheme(ctx)
+ temp := make(map[int64]*ThemeItem, len(list))
+ tempBySlug := make(map[string]*ThemeItem, len(list))
+ for _, item := range list {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ cached := &ThemeItem{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Icon: item.Icon,
+ Rank: item.Rank,
+ Status: item.Status,
+ IsRecommend: item.IsRecommend,
+ PlayletCount: countMap[item.Id],
+ SeoInfo: item.SeoInfo,
+ }
+ temp[item.Id] = cached
+ if slug := strings.TrimSpace(item.Slug); slug != "" {
+ tempBySlug[slug] = cached
+ }
+ }
+ cache.lock.Lock()
+ cache.themeByID = temp
+ cache.themeBySlug = tempBySlug
+ cache.lock.Unlock()
+}
+
+type themePlayletCount struct {
+ Id int64 `bson:"_id"`
+ Count int64 `bson:"count"`
+}
+
+// countUpPlayletsByTheme 按题材统计已上架短剧数量。
+func countUpPlayletsByTheme(ctx context.Context) map[int64]int64 {
+ result := make(map[int64]int64)
+ list := make([]themePlayletCount, 0)
+ err := playletmod.AggregatePlaylet(ctx, &list, []bson.M{
+ {
+ "$match": bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ },
+ },
+ {
+ "$unwind": "$" + playletmod.FN.Playlet.ThemeIds,
+ },
+ {
+ "$group": bson.M{
+ "_id": "$" + playletmod.FN.Playlet.ThemeIds,
+ "count": bson.M{"$sum": 1},
+ },
+ },
+ })
+ if err != nil {
+ log.Warn("playletcache.countUpPlayletsByTheme failed", log.E(err))
+ return result
+ }
+ for _, item := range list {
+ if item.Id <= 0 || item.Count <= 0 {
+ continue
+ }
+ result[item.Id] = item.Count
+ }
+ return result
+}
+
+func updateTagCache(ctx context.Context) {
+ list, err := playlettagmod.FindManyPlayletTag(ctx, bson.M{}, options.Find().
+ SetProjection(bson.D{
+ {Key: playlettagmod.FN.PlayletTag.Id, Value: 1},
+ {Key: playlettagmod.FN.PlayletTag.Name, Value: 1},
+ {Key: playlettagmod.FN.PlayletTag.Slug, Value: 1},
+ {Key: playlettagmod.FN.PlayletTag.Rank, Value: 1},
+ {Key: playlettagmod.FN.PlayletTag.Status, Value: 1},
+ {Key: "seoInfo", Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playlettagmod.FN.PlayletTag.Rank, Value: -1},
+ {Key: playlettagmod.FN.PlayletTag.Id, Value: -1},
+ }))
+ if err != nil {
+ log.Warn("playletcache.updateTagCache failed", log.E(err))
+ return
+ }
+ temp := make(map[int64]*TagItem, len(list))
+ tempBySlug := make(map[string]*TagItem, len(list))
+ for _, item := range list {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ cached := &TagItem{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Rank: item.Rank,
+ Status: item.Status,
+ SeoInfo: item.SeoInfo,
+ }
+ temp[item.Id] = cached
+ if slug := strings.TrimSpace(item.Slug); slug != "" {
+ tempBySlug[slug] = cached
+ }
+ }
+ cache.lock.Lock()
+ cache.tagByID = temp
+ cache.tagBySlug = tempBySlug
+ cache.lock.Unlock()
+}
+
+func updateCategoryCache(ctx context.Context) {
+ list, err := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{}, options.Find().
+ SetProjection(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.Desc, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Icon, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Rank, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Status, Value: 1},
+ {Key: "seoInfo", Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletcategorymod.FN.PlayletCategory.Rank, Value: -1},
+ {Key: playletcategorymod.FN.PlayletCategory.Id, Value: -1},
+ }))
+ if err != nil {
+ log.Warn("playletcache.updateCategoryCache failed", log.E(err))
+ return
+ }
+ temp := make(map[int64]*CategoryItem, len(list))
+ tempBySlug := make(map[string]*CategoryItem, len(list))
+ for _, item := range list {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ cached := &CategoryItem{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Icon: item.Icon,
+ Rank: item.Rank,
+ Status: item.Status,
+ SeoInfo: item.SeoInfo,
+ }
+ temp[item.Id] = cached
+ if slug := strings.TrimSpace(item.Slug); slug != "" {
+ tempBySlug[slug] = cached
+ }
+ }
+ cache.lock.Lock()
+ cache.categoryByID = temp
+ cache.categoryBySlug = tempBySlug
+ cache.lock.Unlock()
+}
+
+func updateHotSearchCache(ctx context.Context) {
+ list, err := hotsearchmod.FindTopHotSearch(ctx, hotsearchmod.TopLimit)
+ if err != nil {
+ log.Warn("playletcache.updateHotSearchCache failed", log.E(err))
+ return
+ }
+ temp := make([]HotSearchItem, 0, len(list))
+ for _, item := range list {
+ if item == nil || strings.TrimSpace(item.Keyword) == "" {
+ continue
+ }
+ temp = append(temp, HotSearchItem{
+ Keyword: item.Keyword,
+ Count: item.Count,
+ })
+ }
+ cache.lock.Lock()
+ cache.hotSearch = temp
+ cache.lock.Unlock()
+}
+
+// ListHotSearch 返回缓存的前10个热搜词。
+func ListHotSearch() []HotSearchItem {
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ if len(cache.hotSearch) == 0 {
+ return make([]HotSearchItem, 0)
+ }
+ result := make([]HotSearchItem, len(cache.hotSearch))
+ copy(result, cache.hotSearch)
+ return result
+}
+
+// GetTheme 按 ID 取题材缓存;不存在返回 nil。
+func GetTheme(id int64) *ThemeItem {
+ if id <= 0 {
+ return nil
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ item := cache.themeByID[id]
+ if item == nil {
+ return nil
+ }
+ cp := *item
+ return &cp
+}
+
+// GetEnabledTheme 按 ID 取上架题材;未上架或不存在返回 nil。
+func GetEnabledTheme(id int64) *ThemeItem {
+ item := GetTheme(id)
+ if item == nil || !item.Status {
+ return nil
+ }
+ return item
+}
+
+// GetThemeBySlug 按 slug 取题材缓存;不存在返回 nil。
+func GetThemeBySlug(slug string) *ThemeItem {
+ slug = strings.TrimSpace(slug)
+ if slug == "" {
+ return nil
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ item := cache.themeBySlug[slug]
+ if item == nil {
+ return nil
+ }
+ cp := *item
+ return &cp
+}
+
+// GetTag 按 ID 取标签缓存;不存在返回 nil。
+func GetTag(id int64) *TagItem {
+ if id <= 0 {
+ return nil
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ item := cache.tagByID[id]
+ if item == nil {
+ return nil
+ }
+ cp := *item
+ return &cp
+}
+
+// GetEnabledTag 按 ID 取上架标签;未上架或不存在返回 nil。
+func GetEnabledTag(id int64) *TagItem {
+ item := GetTag(id)
+ if item == nil || !item.Status {
+ return nil
+ }
+ return item
+}
+
+// GetTagBySlug 按 slug 取标签缓存;不存在返回 nil。
+func GetTagBySlug(slug string) *TagItem {
+ slug = strings.TrimSpace(slug)
+ if slug == "" {
+ return nil
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ item := cache.tagBySlug[slug]
+ if item == nil {
+ return nil
+ }
+ cp := *item
+ return &cp
+}
+
+// GetCategory 按 ID 取分类缓存;不存在返回 nil。
+func GetCategory(id int64) *CategoryItem {
+ if id <= 0 {
+ return nil
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ item := cache.categoryByID[id]
+ if item == nil {
+ return nil
+ }
+ cp := *item
+ return &cp
+}
+
+// GetEnabledCategory 按 ID 取启用分类;未启用或不存在返回 nil。
+func GetEnabledCategory(id int64) *CategoryItem {
+ item := GetCategory(id)
+ if item == nil || !item.Status {
+ return nil
+ }
+ return item
+}
+
+// GetCategoryBySlug 按 slug 取分类缓存;不存在返回 nil。
+func GetCategoryBySlug(slug string) *CategoryItem {
+ slug = strings.TrimSpace(slug)
+ if slug == "" {
+ return nil
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ item := cache.categoryBySlug[slug]
+ if item == nil {
+ return nil
+ }
+ cp := *item
+ return &cp
+}
+
+// ListEnabledThemes 返回全部上架题材,按 Rank、Id 倒序。
+func ListEnabledThemes() []*ThemeItem {
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ result := make([]*ThemeItem, 0, len(cache.themeByID))
+ for _, item := range cache.themeByID {
+ if item == nil || !item.Status {
+ continue
+ }
+ cp := *item
+ result = append(result, &cp)
+ }
+ sort.SliceStable(result, func(i, j int) bool {
+ if result[i].Rank != result[j].Rank {
+ return result[i].Rank > result[j].Rank
+ }
+ return result[i].Id > result[j].Id
+ })
+ return result
+}
+
+const homeRecommendThemeLimit = 4
+
+// ListRecommendedThemes 返回已上架且推荐的题材,按 Rank、Id 倒序,最多 limit 条。
+func ListRecommendedThemes(limit int) []*ThemeItem {
+ if limit <= 0 {
+ limit = homeRecommendThemeLimit
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ result := make([]*ThemeItem, 0, limit)
+ for _, item := range cache.themeByID {
+ if item == nil || !item.Status || !item.IsRecommend {
+ continue
+ }
+ cp := *item
+ result = append(result, &cp)
+ }
+ sort.SliceStable(result, func(i, j int) bool {
+ if result[i].Rank != result[j].Rank {
+ return result[i].Rank > result[j].Rank
+ }
+ return result[i].Id > result[j].Id
+ })
+ if len(result) > limit {
+ result = result[:limit]
+ }
+ return result
+}
+
+// ListEnabledCategories 返回全部启用分类,按 Rank、Id 倒序。
+func ListEnabledCategories() []*CategoryItem {
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ result := make([]*CategoryItem, 0, len(cache.categoryByID))
+ for _, item := range cache.categoryByID {
+ if item == nil || !item.Status {
+ continue
+ }
+ cp := *item
+ result = append(result, &cp)
+ }
+ sort.SliceStable(result, func(i, j int) bool {
+ if result[i].Rank != result[j].Rank {
+ return result[i].Rank > result[j].Rank
+ }
+ return result[i].Id > result[j].Id
+ })
+ return result
+}
+
+// ThemeNameMap 批量取题材名称(含未上架,便于列表展示历史题材名)。
+func ThemeNameMap(ids []int64) map[int64]string {
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ for _, id := range ids {
+ if item := cache.themeByID[id]; item != nil {
+ result[id] = item.Name
+ }
+ }
+ return result
+}
+
+// TagNameMap 批量取标签名称(含未上架)。
+func TagNameMap(ids []int64) map[int64]string {
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ for _, id := range ids {
+ if item := cache.tagByID[id]; item != nil {
+ result[id] = item.Name
+ }
+ }
+ return result
+}
+
+// ThemeSlugMap 批量取题材 slug(含未上架)。
+func ThemeSlugMap(ids []int64) map[int64]string {
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ for _, id := range ids {
+ if item := cache.themeByID[id]; item != nil {
+ result[id] = item.Slug
+ }
+ }
+ return result
+}
+
+// TagSlugMap 批量取标签 slug(含未上架)。
+func TagSlugMap(ids []int64) map[int64]string {
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ for _, id := range ids {
+ if item := cache.tagByID[id]; item != nil {
+ result[id] = item.Slug
+ }
+ }
+ return result
+}
+
+// CategoryNameMap 批量取分类名称(含未启用)。
+func CategoryNameMap(ids []int64) map[int64]string {
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result
+ }
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ for _, id := range ids {
+ if item := cache.categoryByID[id]; item != nil {
+ result[id] = item.Name
+ }
+ }
+ return result
+}
diff --git a/app/service/playletcache/guess_like.go b/app/service/playletcache/guess_like.go
new file mode 100644
index 0000000..9e97f36
--- /dev/null
+++ b/app/service/playletcache/guess_like.go
@@ -0,0 +1,104 @@
+package playletcache
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/playletmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const GuessLikeLimit = 15
+
+var guessLikeProjection = 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.MediaCreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaUpdatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.PlayName, Value: 1},
+ {Key: playletmod.FN.Playlet.Description, Value: 1},
+ {Key: playletmod.FN.Playlet.Slug, Value: 1},
+ {Key: playletmod.FN.Playlet.Desc, Value: 1},
+ {Key: playletmod.FN.Playlet.Anchor, Value: 1},
+ {Key: playletmod.FN.Playlet.Cover, Value: 1},
+ {Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
+ {Key: playletmod.FN.Playlet.OperateType, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+ {Key: "seoInfo", Value: 1},
+}
+
+func updateGuessLikeCache(ctx context.Context) {
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.Find().
+ SetProjection(guessLikeProjection).
+ SetSort(bson.D{
+ {Key: playletmod.FN.Playlet.CountCollect, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }).
+ SetLimit(GuessLikeLimit))
+ if err != nil {
+ log.Warn("playletcache.updateGuessLikeCache failed", log.E(err))
+ return
+ }
+ temp := make([]*playletmod.Playlet, 0, len(list))
+ for _, item := range list {
+ if cloned := clonePlaylet(item); cloned != nil {
+ temp = append(temp, cloned)
+ }
+ }
+ cache.lock.Lock()
+ cache.guessLike = temp
+ cache.lock.Unlock()
+}
+
+// ListGuessLike 返回收藏量最多的上架短剧,最多15部。
+func ListGuessLike() []*playletmod.Playlet {
+ cache.lock.RLock()
+ defer cache.lock.RUnlock()
+ if len(cache.guessLike) == 0 {
+ return make([]*playletmod.Playlet, 0)
+ }
+ result := make([]*playletmod.Playlet, 0, len(cache.guessLike))
+ for _, item := range cache.guessLike {
+ if cloned := clonePlaylet(item); cloned != nil {
+ result = append(result, cloned)
+ }
+ }
+ return result
+}
+
+func clonePlaylet(item *playletmod.Playlet) *playletmod.Playlet {
+ if item == nil || item.Id <= 0 {
+ return nil
+ }
+ cp := *item
+ if item.ThemeIds != nil {
+ cp.ThemeIds = append([]int64(nil), item.ThemeIds...)
+ }
+ if item.TagIds != nil {
+ cp.TagIds = append([]int64(nil), item.TagIds...)
+ }
+ if item.CategoryIds != nil {
+ cp.CategoryIds = append([]int64(nil), item.CategoryIds...)
+ }
+ return &cp
+}
diff --git a/app/service/playletser/playlet.go b/app/service/playletser/playlet.go
new file mode 100644
index 0000000..0c6f58e
--- /dev/null
+++ b/app/service/playletser/playlet.go
@@ -0,0 +1,3814 @@
+package playletser
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/playletcache"
+ "videoweb/app/service/reportser"
+ "videoweb/app/service/videoser"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/homebannermod"
+ "videoweb/pub/models/hotsearchmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptercollectmod"
+ "videoweb/pub/models/playletchapterlikemod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletchapterunlockmod"
+ "videoweb/pub/models/playletcollectmod"
+ "videoweb/pub/models/playletlikemod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletwatchhistorymod"
+ "videoweb/pub/models/playletwatchprogressmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/payment"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+ "videoweb/web/service/srv_search"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ homeBannerLimit int64 = 30 // banner图限制
+ homeSectionLimit int64 = 16 // 首页分区数据限制
+ homeRecommendThemeLimit int = 4 // 首页推荐主题限制
+ defaultPageSize int64 = 10 // 默认分页条数
+ recommendPageSize int64 = 20 // 今日推荐/热门好剧默认分页条数
+ maxPageSize int64 = 100 // 最大分页条数
+ sortOrderHot int64 = 1 // 热度优先
+ sortOrderUpdated int64 = 2 // 今日更新
+ operateFilterLatest int64 = 1 // 最近更新
+ operateFilterHot int64 = 2 // 当前热播
+ operateFilterOriginal int64 = 3 // 独家原创
+ rankHot int64 = 1 // 热播榜
+ rankNew int64 = 2 // 新剧榜
+ rankCollect int64 = 3 // 追剧榜
+ relatedLimit int64 = 8 // 相关推荐限制
+ operateStatusYes int64 = 1 // 已操作
+ operateStatusNo int64 = 2 // 未操作
+ playletHeatClickInc int64 = 100 // 每次查看详情增加的热度值
+ playletHeatLikeInc int64 = 1 // 点赞增加的热度值
+ playletHeatCollectInc int64 = 2 // 收藏增加的热度值
+ playletHeatBuyInc int64 = 10 // 购买剧集增加的短剧热度值
+ playletHeatForwardInc int64 = 5 // 转发增加的热度值
+ homePlayletCacheSchema = "banner-v2"
+ // playletH5DetailPath 短剧Web/H5详情页路径,供SEO canonical使用。
+ playletH5DetailPath = "/detail"
+ // playletChapterH5DetailPath 短剧Web/H5播放页路径,供SEO canonical使用。
+ playletChapterH5DetailPath = "/play"
+)
+
+var errPlayletChapterAlreadyUnlocked = errors.New("playlet chapter already unlocked")
+
+type playletChapterAccessContext struct {
+ balance int64
+ vipValid bool
+ unlockedChapterIds map[int64]bool
+}
+
+// playletHomeProjection 查询指定字段
+var playletHomeProjection = 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.MediaCreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaUpdatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.PlayName, Value: 1},
+ {Key: playletmod.FN.Playlet.Description, Value: 1},
+ {Key: playletmod.FN.Playlet.Slug, Value: 1},
+ {Key: playletmod.FN.Playlet.Desc, Value: 1},
+ {Key: playletmod.FN.Playlet.Anchor, Value: 1},
+ {Key: playletmod.FN.Playlet.Cover, Value: 1},
+ {Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
+ {Key: playletmod.FN.Playlet.OperateType, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+ {Key: "seoInfo", Value: 1},
+}
+
+// homeBannerAppProjection 查询首页 banner 指定字段
+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},
+}
+
+// playletCollectListProjection 我的收藏列表只读取卡片展示和未看集数计算需要的字段。
+var playletCollectListProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.PlayName, Value: 1},
+ {Key: playletmod.FN.Playlet.Slug, Value: 1},
+ {Key: playletmod.FN.Playlet.Cover, Value: 1},
+ {Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: "seoInfo", Value: 1},
+}
+
+// playletChapterSEOProjection 查询剧集详情SEO需要的短剧字段,避免拉取整部短剧大字段。
+var playletChapterSEOProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, 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.IsOriginal, Value: 1},
+ {Key: "seoInfo", Value: 1},
+}
+
+// playletChapterInfoProjection 查询详情章节集合需要的字段
+var playletChapterInfoProjection = bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Md5, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Hash, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Height, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Width, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Ratio, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.VideoUrl, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.MediaSize, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.MediaTime, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PayType, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.SingleCoins, Value: 1},
+}
+
+// playletChapterDetailProjection 查询剧集详情需要的字段
+var playletChapterDetailProjection = bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Md5, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Hash, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Height, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Width, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Ratio, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.VideoUrl, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.MediaSize, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.MediaTime, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountComment, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountBrowse, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountCollect, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountLike, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PayType, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.SingleCoins, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.WatermarkStatus, Value: 1},
+}
+
+// playletOperateProjection 查询短剧操作接口需要的字段
+var playletOperateProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+}
+
+// playletChapterOperateProjection 查询剧集操作接口需要的字段
+var playletChapterOperateProjection = bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountLike, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountCollect, Value: 1},
+}
+
+// playletChapterWatchReportProjection 观看上报只需要剧集归属和集数。
+var playletChapterWatchReportProjection = bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+}
+
+// playletWatchReportProjection 观看上报只需要短剧总集数计算未看数量。
+var playletWatchReportProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+}
+
+// Home 查询app首页短剧数据
+func Home(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
+ cacheKey := homePlayletCacheKey()
+ if resp := getHomePlayletCache(cacheKey); resp != nil {
+ attachHomeAnnouncements(resp)
+ attachHomeRecommendThemes(ctx, resp)
+ return resp, nil
+ }
+ resp, err := buildHomePlayletResponse(ctx)
+ if err != nil {
+ return nil, err
+ }
+ setHomePlayletCache(cacheKey, resp)
+ attachHomeAnnouncements(resp)
+ attachHomeRecommendThemes(ctx, resp)
+ return resp, nil
+}
+
+// RecommendList 分页查询app端分类短剧列表
+func RecommendList(ctx context.Context, req *proto.PlayletRecommendListReq) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ categoryID, err := resolveCategoryID(ctx, req.CategoryId, req.CategorySlug)
+ if err != nil {
+ return nil, err
+ }
+ req.CategoryId = categoryID
+ pageNum, pageSize := normalizePlayletRecommendPage(req.PageNum, req.PageSize)
+ cacheKey := playletRecommendListCacheKey(req.CategoryId, pageNum, pageSize)
+ if resp := getPlayletRecommendListCache(cacheKey); resp != nil {
+ return resp, nil
+ }
+ category, err := findEnabledPlayletCategory(ctx, req.CategoryId)
+ if err != nil {
+ return nil, err
+ }
+ filter, sort, err := playletRecommendListQuery(ctx, req.CategoryId)
+ if err != nil {
+ return nil, err
+ }
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletRecommendListResp{
+ CategoryId: category.Id,
+ CategoryName: category.Name,
+ CategorySlug: category.Slug,
+ CategoryDesc: category.Desc,
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0),
+ }
+ resp.SeoInfo = buildPlayletCategoryPageSeoInfo(category.Id, category.Name)
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ var list playletmod.PlayletList
+ if _, ok := filter[playletmod.FN.Playlet.CategoryIds]; ok {
+ list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, req.CategoryId, skip, pageSize, playletmod.CategoryRankSort(), playletHomeProjection)
+ } else {
+ list, err = findHomePlaylets(ctx, filter, sort, pageSize, options.Find().SetSkip(skip))
+ }
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildHomeThemeNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询分类短剧题材名称失败", log.Any("分类ID", req.CategoryId), log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询分类短剧标签名称失败", log.Any("分类ID", req.CategoryId), log.E(err))
+ return nil, err
+ }
+ resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
+ setPlayletRecommendListCache(cacheKey, resp)
+ return resp, nil
+}
+
+// List 分页查询app端短剧列表
+func List(ctx context.Context, req *proto.PlayletVideoReq) (*proto.PlayletVideoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ themeID, err := resolveThemeID(ctx, req.ThemeId, req.ThemeSlug)
+ if err != nil {
+ return nil, err
+ }
+ req.ThemeId = themeID
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := homeUpPlayletFilter(nil)
+ if req.ThemeId > 0 {
+ filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{req.ThemeId}}
+ }
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletVideoResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0),
+ }
+ if req.ThemeId > 0 {
+ resp.SeoInfo = buildPlayletThemePageSeoInfo(req.ThemeId)
+ } else {
+ resp.SeoInfo = buildPlayletListPageSeoInfo()
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ list, err := findHomePlaylets(ctx, filter, playletListSort(req.SortOrder), pageSize, options.Find().SetSkip(skip))
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildHomeThemeNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询短剧列表题材名称失败", log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询短剧列表标签名称失败", log.E(err))
+ return nil, err
+ }
+ resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
+ return resp, nil
+}
+
+// Rank 分页查询app端短剧排行榜
+func Rank(ctx context.Context, req *proto.PlayletRankReq) (*proto.PlayletRankResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := homeUpPlayletFilter(nil)
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletRankResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0),
+ }
+ resp.SeoInfo = buildPlayletListPageSeoInfo()
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ list, err := findHomePlaylets(ctx, filter, playletRankSort(req.SortOrder), pageSize, options.Find().SetSkip(skip))
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildHomeThemeNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询短剧排行榜题材名称失败", log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询短剧排行榜标签名称失败", log.E(err))
+ return nil, err
+ }
+ resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
+ return resp, nil
+}
+
+// TodayUpdate 分页查询app端今日更新短剧列表
+func TodayUpdate(ctx context.Context, req *proto.PlayletTodayUpdateReq) (*proto.PlayletTodayUpdateResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := homeUpPlayletFilter(nil)
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletTodayUpdateResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0),
+ }
+ resp.SeoInfo = buildPlayletListPageSeoInfo()
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ list, err := findHomePlaylets(ctx, filter, playletTodayUpdateSort(), pageSize, options.Find().SetSkip(skip))
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildHomeThemeNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询今日更新短剧题材名称失败", log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询今日更新短剧标签名称失败", log.E(err))
+ return nil, err
+ }
+ resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
+ return resp, nil
+}
+
+// OperateList 按运营类型分页查询短剧,支持最近更新、当前热播、独家原创。
+func OperateList(ctx context.Context, req *proto.PlayletOperateListReq) (*proto.PlayletOperateListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ operateType := playletmod.OperateType(req.OperateType)
+ if !playletmod.IsValidOperateType(operateType) {
+ return nil, srvdef.New(srvdef.ErrParamError, "运营类型无效", "")
+ }
+ filterType := req.FilterType
+ if filterType == 0 {
+ filterType = operateFilterLatest
+ }
+ if filterType != operateFilterLatest && filterType != operateFilterHot && filterType != operateFilterOriginal {
+ return nil, srvdef.New(srvdef.ErrParamError, "筛选类型无效", "")
+ }
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := homeUpPlayletFilter(operateTypeFilter(operateType))
+ if filterType == operateFilterOriginal {
+ filter[playletmod.FN.Playlet.IsOriginal] = true
+ }
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletOperateListResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ OperateType: int32(operateType),
+ FilterType: filterType,
+ List: make([]proto.CommonData, 0),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ list, err := findHomePlaylets(ctx, filter, playletOperateListSort(filterType), pageSize, options.Find().SetSkip(skip))
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildHomeThemeNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询运营类型短剧题材名称失败", log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询运营类型短剧标签名称失败", log.E(err))
+ return nil, err
+ }
+ resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
+ return resp, nil
+}
+
+// TagVideoList 根据标签ID分页查询app端短剧列表
+func TagVideoList(ctx context.Context, req *proto.PlayletTagVideoReq) (*proto.PlayletTagVideoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "标签ID不能为空")
+ }
+ tagID, err := resolveTagID(ctx, req.TagId, req.TagSlug)
+ if err != nil {
+ return nil, err
+ }
+ req.TagId = tagID
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.TagIds: req.TagId,
+ })
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletTagVideoResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0),
+ }
+ resp.SeoInfo = buildPlayletTagPageSeoInfo(req.TagId)
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ list, err := findHomePlaylets(ctx, filter, playletTodayUpdateSort(), pageSize, options.Find().SetSkip(skip))
+ if err != nil {
+ return nil, err
+ }
+ themeNameMap, err := buildHomeThemeNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询标签短剧题材名称失败", log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, list)
+ if err != nil {
+ log.Error("查询标签短剧标签名称失败", log.E(err))
+ return nil, err
+ }
+ resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
+ return resp, nil
+}
+
+// Search 使用ES分页搜索app端短剧
+func Search(ctx context.Context, req *proto.PlayletSearchReq) (*proto.PlayletSearchResp, *srvdef.SrvErr) {
+ // 校验搜索关键词,避免空关键词扫全量ES索引。
+ if req == nil || strings.TrimSpace(req.Keyword) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "搜索关键词不能为空", "搜索关键词不能为空")
+ }
+ if esmanager.Instance == nil {
+ log.Error("短剧搜索ES未初始化")
+ return nil, srvdef.New(srvdef.ErrESSearchError, "", "")
+ }
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ keyword := strings.TrimSpace(req.Keyword)
+ if pageNum == 1 {
+ recordHotSearch(keyword)
+ }
+ query := playletSearchQuery(keyword, (pageNum-1)*pageSize, pageSize)
+ var hits esmod.ESPlayletHits
+ if err := esmanager.Instance.Search(esmod.GetPlayletIndex(), &hits, query); err != nil {
+ if strings.Contains(err.Error(), "index_not_found_exception") {
+ log.Warn("短剧ES索引不存在,返回空搜索结果", log.Any("关键词", keyword), log.Any("索引", esmod.GetPlayletIndex()))
+ return emptyPlayletSearchResp(pageNum, pageSize), nil
+ }
+ log.Error("短剧ES搜索失败", log.Any("关键词", keyword), log.E(err))
+ return nil, srvdef.New(srvdef.ErrESSearchError, "", "")
+ }
+ resp := &proto.PlayletSearchResp{
+ TotalCount: hits.Total.Value,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0, len(hits.Hits)),
+ }
+ for _, hit := range hits.Hits {
+ if hit == nil || hit.Source == nil {
+ continue
+ }
+ resp.List = append(resp.List, esPlayletToCommonData(ctx, hit.Source))
+ }
+ return resp, nil
+}
+
+func recordHotSearch(keyword string) {
+ keyword = hotsearchmod.NormalizeKeyword(keyword)
+ if keyword == "" {
+ return
+ }
+ go func(word string) {
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+ if err := hotsearchmod.IncrByKeyword(ctx, word); err != nil {
+ log.Warn("记录热搜词失败", log.Any("关键词", word), log.E(err))
+ }
+ }(keyword)
+}
+
+// HotLike 返回缓存的热搜词和猜你喜欢。
+func HotLike(ctx context.Context) *proto.PlayletHotLikeResp {
+ hotSearch := playletcache.ListHotSearch()
+ resp := &proto.PlayletHotLikeResp{
+ HotSearch: make([]proto.PlayletHotSearchItem, 0, len(hotSearch)),
+ GuessLike: guessLikeCommonData(ctx),
+ }
+ for _, item := range hotSearch {
+ resp.HotSearch = append(resp.HotSearch, proto.PlayletHotSearchItem{
+ Keyword: item.Keyword,
+ Count: item.Count,
+ })
+ }
+ return resp
+}
+
+// SetLike 设置当前用户短剧点赞状态
+func SetLike(ctx context.Context, userId int64, req *proto.PlayletLikeSetReq) (*proto.PlayletLikeSetResp, *srvdef.SrvErr) {
+ // 使用指针布尔值校验是否传参,保证 false 能表示取消点赞。
+ if req == nil || req.Id <= 0 || req.IsLike == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
+ }
+ playlet, changed, err := setPlayletLike(ctx, userId, req.Id, *req.IsLike)
+ if err != nil {
+ return nil, err
+ }
+ countLike := displayPlayletCountLike(applyLocalCounter(playlet.CountLike, *req.IsLike, changed), playlet.VirtualCountLike)
+ resp := &proto.PlayletLikeSetResp{
+ Id: req.Id,
+ IsLike: operateStatusByFlag(*req.IsLike),
+ CountLike: countLike,
+ }
+ if changed {
+ reportser.ReportPlayletLike(ctx, userId, req.Id, resp.IsLike)
+ }
+ return resp, nil
+}
+
+// SetCollect 设置当前用户短剧收藏状态
+func SetCollect(ctx context.Context, userId int64, req *proto.PlayletCollectSetReq) (*proto.PlayletCollectSetResp, *srvdef.SrvErr) {
+ // 使用指针布尔值校验是否传参,保证 false 能表示取消收藏。
+ if req == nil || req.Id <= 0 || req.IsCollect == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
+ }
+ playlet, changed, err := setPlayletCollect(ctx, userId, req.Id, *req.IsCollect)
+ if err != nil {
+ return nil, err
+ }
+ countCollect := applyLocalCounter(playlet.CountCollect, *req.IsCollect, changed)
+ resp := &proto.PlayletCollectSetResp{
+ Id: req.Id,
+ IsCollect: operateStatusByFlag(*req.IsCollect),
+ CountCollect: countCollect,
+ }
+ if changed {
+ reportser.ReportPlayletCollect(ctx, userId, req.Id, resp.IsCollect)
+ }
+ return resp, nil
+}
+
+// ForwardHeat 短剧转发增加热度值
+func ForwardHeat(ctx context.Context, userId int64, ip string, req *proto.PlayletForwardHeatReq) (*proto.PlayletForwardHeatResp, *srvdef.SrvErr) {
+ if req == nil || req.Id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
+ }
+ limited, limitErr := checkPlayletForwardHeatLimit(userId, ip, req.Id)
+ if limitErr != nil {
+ log.Error("短剧转发热度频控失败", log.Any("用户ID", userId), log.Any("IP", ip), log.Any("短剧ID", req.Id), log.E(limitErr))
+ }
+ if limited {
+ playlet, srvErr := findPlayletForwardHeatTarget(ctx, req.Id)
+ if srvErr != nil {
+ return nil, srvErr
+ }
+ return &proto.PlayletForwardHeatResp{
+ Id: playlet.Id,
+ HeatValue: playlet.HeatValue,
+ }, nil
+ }
+ playlet, srvErr := incrementPlayletHeatValueAndReturn(ctx, req.Id, playletHeatForwardInc)
+ if srvErr != nil {
+ log.Error("短剧转发增加热度失败", log.Any("短剧ID", req.Id), log.E(srvErr))
+ return nil, srvErr
+ }
+ return &proto.PlayletForwardHeatResp{
+ Id: playlet.Id,
+ HeatValue: playlet.HeatValue,
+ }, nil
+}
+
+// SetChapterLike 设置当前用户短剧剧集点赞状态
+func SetChapterLike(ctx context.Context, userId int64, req *proto.PlayletChapterLikeSetReq) (*proto.PlayletChapterLikeSetResp, *srvdef.SrvErr) {
+ // 使用指针布尔值校验是否传参,保证 false 能表示取消点赞。
+ if req == nil || req.ChapterId <= 0 || req.IsLike == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
+ }
+ chapter, changed, err := setPlayletChapterLike(ctx, userId, req.ChapterId, *req.IsLike)
+ if err != nil {
+ return nil, err
+ }
+ countLike := applyLocalCounter(chapter.CountLike, *req.IsLike, changed)
+ resp := &proto.PlayletChapterLikeSetResp{
+ ChapterId: req.ChapterId,
+ IsLike: operateStatusByFlag(*req.IsLike),
+ CountLike: countLike,
+ }
+ if changed {
+ reportser.ReportPlayletChapterLike(ctx, userId, chapter.PlayletId, req.ChapterId, resp.IsLike)
+ }
+ return resp, nil
+}
+
+// SetChapterCollect 设置当前用户短剧剧集收藏状态
+func SetChapterCollect(ctx context.Context, userId int64, req *proto.PlayletChapterCollectSetReq) (*proto.PlayletChapterCollectSetResp, *srvdef.SrvErr) {
+ // 使用指针布尔值校验是否传参,保证 false 能表示取消收藏。
+ if req == nil || req.ChapterId <= 0 || req.IsCollect == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
+ }
+ chapter, changed, err := setPlayletChapterCollect(ctx, userId, req.ChapterId, *req.IsCollect)
+ if err != nil {
+ return nil, err
+ }
+ countCollect := applyLocalCounter(chapter.CountCollect, *req.IsCollect, changed)
+ resp := &proto.PlayletChapterCollectSetResp{
+ ChapterId: req.ChapterId,
+ IsCollect: operateStatusByFlag(*req.IsCollect),
+ CountCollect: countCollect,
+ }
+ if changed {
+ reportser.ReportPlayletChapterCollect(ctx, userId, chapter.PlayletId, req.ChapterId, resp.IsCollect)
+ }
+ return resp, nil
+}
+
+// ReportChapterWatch 上报当前用户短剧剧集观看进度
+func ReportChapterWatch(ctx context.Context, userId int64, req *proto.PlayletChapterWatchReportReq) (*proto.PlayletChapterWatchReportResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ if req == nil || req.ChapterId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
+ }
+ filter := bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: req.ChapterId,
+ }
+ if req.PlayletId > 0 {
+ filter[playletchaptermod.FN.PlayletChapter.PlayletId] = req.PlayletId
+ }
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, filter, options.FindOne().SetProjection(playletChapterWatchReportProjection))
+ if err != nil {
+ log.Error("查询短剧观看上报剧集失败", log.Any("用户ID", userId), log.Any("短剧ID", req.PlayletId), log.Any("剧集ID", req.ChapterId), log.E(err))
+ return nil, err
+ }
+ if chapter.CurrentEpisode <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "剧集集数无效", "剧集集数无效")
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ }), options.FindOne().SetProjection(playletWatchReportProjection))
+ if err != nil {
+ log.Error("查询短剧观看上报所属短剧失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", req.ChapterId), log.E(err))
+ return nil, err
+ }
+ progress, err := playletwatchprogressmod.SavePlayletWatchProgress(ctx, userId, chapter.PlayletId, chapter.Id, chapter.CurrentEpisode, true)
+ if err != nil {
+ log.Error("保存短剧观看上报进度失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapter.Id), log.E(err))
+ return nil, err
+ }
+ if err = playletwatchhistorymod.SavePlayletWatchHistory(ctx, userId, chapter.PlayletId, chapter.Id, chapter.CurrentEpisode); err != nil {
+ log.Error("保存短剧观看历史失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapter.Id), log.E(err))
+ return nil, err
+ }
+ watchedCount := collectWatchedCount(progress, playlet.TotalEpisode)
+ return &proto.PlayletChapterWatchReportResp{
+ PlayletId: chapter.PlayletId,
+ ChapterId: chapter.Id,
+ CurrentEpisode: chapter.CurrentEpisode,
+ WatchedCount: watchedCount,
+ UnwatchedCount: collectUnwatchedCount(playlet.TotalEpisode, watchedCount),
+ LastChapterId: collectLastChapterId(progress),
+ LastEpisode: collectLastEpisode(progress),
+ }, nil
+}
+
+// ChapterDetail 根据ID查询app端短剧剧集详情
+func ChapterDetail(ctx context.Context, userId int64, req *proto.PlayletChapterInfoReq) (*proto.PlayletChapterInfoResp, *srvdef.SrvErr) {
+ // 播放页切换剧集只查询当前剧集和所属短剧状态,不重复查询整部短剧详情、相关推荐和全部剧集列表。
+ if req == nil || req.ChapterId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧剧集ID不能为空", "短剧剧集ID不能为空")
+ }
+ playletID, err := resolvePlayletID(ctx, req.PlayletId, req.PlayletSlug, false)
+ if err != nil {
+ return nil, err
+ }
+ req.PlayletId = playletID
+ filter := bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: req.ChapterId,
+ }
+ if req.PlayletId > 0 {
+ filter[playletchaptermod.FN.PlayletChapter.PlayletId] = req.PlayletId
+ }
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, filter, options.FindOne().SetProjection(playletChapterDetailProjection))
+ if err != nil {
+ log.Error("查询短剧剧集详情失败", log.Any("短剧ID", req.PlayletId), log.Any("剧集ID", req.ChapterId), log.E(err))
+ return nil, err
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ }), options.FindOne().SetProjection(playletChapterSEOProjection))
+ if err != nil {
+ log.Error("查询短剧剧集详情所属短剧失败", log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapter.Id), log.E(err))
+ return nil, err
+ }
+ if err = ensureOriginalChapterWatermarkReady(playlet, chapter); err != nil {
+ return nil, err
+ }
+ isLike, isCollect, err := findPlayletChapterUserOperateStatus(ctx, userId, chapter.Id)
+ if err != nil {
+ return nil, err
+ }
+ resp := playletChapterToInfoResp(chapter)
+ resp.IsLike = isLike
+ resp.IsCollect = isCollect
+ resp.SeoInfo = buildPlayletChapterDetailSeoInfo(playlet, chapter)
+ if err = applyPlayletChapterWatchPermission(ctx, userId, chapter, resp); err != nil {
+ return nil, err
+ }
+ if !resp.CanWatch {
+ resp.VideoUrl = ""
+ return resp, nil
+ }
+ if err = incrementPlayletChapterBrowse(ctx, chapter.Id); err != nil {
+ log.Error("增加短剧剧集观看量失败", log.Any("剧集ID", chapter.Id), log.E(err))
+ return nil, err
+ }
+ resp.CountBrowse++
+ if resp.VideoUrl != "" {
+ resp.VideoUrl = videoser.GetPlayletM3U8AuthParam(
+ chapter.Id,
+ resp.VideoUrl,
+ videoser.H5M3U8RequestIP(ctx),
+ strings.TrimSpace(chapter.WatermarkVideoUrl) == "",
+ userId,
+ )
+ }
+ return resp, nil
+}
+
+// ChapterList 根据短剧ID查询app端短剧剧集列表
+func ChapterList(ctx context.Context, req *proto.PlayletChapterListReq) (*proto.PlayletChapterListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
+ }
+ playletID, err := resolvePlayletID(ctx, req.PlayletId, req.PlayletSlug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.PlayletId = playletID
+ playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: req.PlayletId,
+ }), options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询短剧剧集列表所属短剧失败", log.Any("短剧ID", req.PlayletId), log.E(err))
+ return nil, err
+ }
+ chapterInfo, err := findPlayletChapters(ctx, req.PlayletId, 0, playlet != nil && playlet.IsOriginal)
+ if err != nil {
+ return nil, err
+ }
+ return &proto.PlayletChapterListResp{
+ PlayletId: req.PlayletId,
+ List: chapterInfo,
+ }, nil
+}
+
+// UnlockChapter 使用金币逐集解锁短剧剧集。
+//
+// 幂等性约束:
+// 1. 事务前先查一次已解锁记录,命中时直接返回成功;
+// 2. 事务内先插入 userId+chapterId 唯一解锁记录,再扣金币;
+// 3. 并发重复请求只有一个能插入成功,其他请求按已解锁成功处理,不重复扣费。
+func UnlockChapter(ctx context.Context, userId int64, req *proto.PlayletChapterUnlockReq) (*proto.PlayletChapterUnlockResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ if req == nil || req.PlayletId <= 0 || req.ChapterId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID和剧集ID不能为空", "短剧ID和剧集ID不能为空")
+ }
+ chapter, playlet, err := findPlayletChapterForUnlock(ctx, req.PlayletId, req.ChapterId)
+ if err != nil {
+ return nil, err
+ }
+ if chapter.PayType == playletchaptermod.PayTypeFree {
+ user, err := findPlayletChapterAccessUser(ctx, userId)
+ if err != nil {
+ return nil, err
+ }
+ return &proto.PlayletChapterUnlockResp{
+ PlayletId: chapter.PlayletId,
+ ChapterId: chapter.Id,
+ Unlocked: true,
+ CanWatch: true,
+ Balance: user.Recharge,
+ CostCoins: 0,
+ ChangeCoin: 0,
+ }, nil
+ }
+ if chapter.PayType == playletchaptermod.PayTypeVip {
+ return nil, srvdef.New(srvdef.ErrParamError, "VIP剧集需开通会员后观看", "VIP剧集需开通会员后观看")
+ }
+ if chapter.PayType != playletchaptermod.PayTypeCoin {
+ return nil, srvdef.New(srvdef.ErrParamError, "剧集支付方式无效", "剧集支付方式无效")
+ }
+ if chapter.SingleCoins <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "剧集金币价格无效", "剧集金币价格无效")
+ }
+
+ unlocked, err := playletchapterunlockmod.ExistsPlayletChapterUnlock(ctx, userId, chapter.Id)
+ if err != nil {
+ return nil, err
+ }
+ user, err := findPlayletChapterAccessUser(ctx, userId)
+ if err != nil {
+ return nil, err
+ }
+ if unlocked {
+ return playletChapterUnlockResp(chapter, user.Recharge, 0), nil
+ }
+ if user.Recharge < chapter.SingleCoins {
+ return nil, srvdef.New(srvdef.UserNotEnoughMoney, "金币不足,请先充值", "金币不足,请先充值")
+ }
+
+ orderNo := playletChapterUnlockOrderNo(userId, chapter.Id)
+ var txSrvErr *srvdef.SrvErr
+ var balanceAfter int64
+ txErr := mongos.Shop.Transaction(ctx, func(sctx mongo.SessionContext) error {
+ inserted, srvErr := playletchapterunlockmod.InsertOnePlayletChapterUnlock(sctx, &playletchapterunlockmod.PlayletChapterUnlock{
+ UserId: userId,
+ PlayletId: chapter.PlayletId,
+ ChapterId: chapter.Id,
+ Coins: chapter.SingleCoins,
+ OrderNo: orderNo,
+ })
+ if srvErr != nil {
+ txSrvErr = srvErr
+ return srvErr
+ }
+ if !inserted {
+ return errPlayletChapterAlreadyUnlocked
+ }
+
+ updatedUser, srvErr := usermod.UserPay(sctx, userId, chapter.SingleCoins)
+ if srvErr != nil {
+ txSrvErr = srvdef.New(srvdef.UserNotEnoughMoney, "金币不足,请先充值", "金币不足,请先充值")
+ return txSrvErr
+ }
+ balanceAfter = updatedUser.Recharge
+
+ srvErr = payment.WriteCoinTrans(sctx, payment.WriteCoinTransOpt{
+ UserId: userId,
+ UserName: updatedUser.NickName,
+ FlowDir: cointransmod.FlowOut,
+ TranType: cointransmod.TranTypeVideoUnlock,
+ Amount: chapter.SingleCoins,
+ OrderNo: orderNo,
+ RelatedId: chapter.Id,
+ Remark: fmt.Sprintf("解锁《%s》", playlet.Title),
+ }, balanceAfter)
+ if srvErr != nil {
+ txSrvErr = srvErr
+ return srvErr
+ }
+ return nil
+ })
+ if txErr != nil {
+ if errors.Is(txErr, errPlayletChapterAlreadyUnlocked) {
+ user, err = findPlayletChapterAccessUser(ctx, userId)
+ if err != nil {
+ return nil, err
+ }
+ return playletChapterUnlockResp(chapter, user.Recharge, 0), nil
+ }
+ if txSrvErr != nil {
+ return nil, txSrvErr
+ }
+ log.Error("短剧剧集金币解锁事务失败", log.Any("userId", userId), log.Any("chapterId", chapter.Id), log.E(txErr))
+ return nil, srvdef.New(srvdef.DBTransaction, "解锁失败,请稍后重试", "解锁失败,请稍后重试")
+ }
+ updatePlayletChapterUnlockStatsAsync(chapter.PlayletId, chapter.Id)
+ reportser.ReportPlayletUnlock(ctx, user, playlet, chapter, chapter.SingleCoins, user.Recharge, balanceAfter, orderNo)
+ return playletChapterUnlockResp(chapter, balanceAfter, -chapter.SingleCoins), nil
+}
+
+// updatePlayletChapterUnlockStatsAsync 异步更新剧集购买数和短剧热度
+func updatePlayletChapterUnlockStatsAsync(playletId int64, chapterId int64) {
+ if playletId <= 0 || chapterId <= 0 {
+ return
+ }
+ go func() {
+ if err := playletchaptermod.UpdateOnePlayletChapter(context.Background(), bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: chapterId,
+ }, bson.M{
+ "$inc": bson.M{playletchaptermod.FN.PlayletChapter.CountBuy: 1},
+ }); err != nil {
+ log.Error("短剧剧集解锁后更新购买数失败", log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
+ }
+ if err := incrementPlayletHeatValue(context.Background(), playletId, playletHeatBuyInc); err != nil {
+ log.Error("短剧剧集解锁后更新短剧热度失败", log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
+ return
+ }
+ syncPlayletToESAsync(playletId, "短剧剧集解锁后同步ES失败")
+ }()
+}
+
+// applyPlayletChapterWatchPermission 根据剧集支付方式填充播放权限,并在未授权时清空播放地址。
+func applyPlayletChapterWatchPermission(ctx context.Context,
+ userId int64,
+ chapter *playletchaptermod.PlayletChapter,
+ resp *proto.PlayletChapterInfoResp,
+) *srvdef.SrvErr {
+ if chapter == nil || resp == nil {
+ return srvdef.New(srvdef.ErrNoData, "剧集不存在", "剧集不存在")
+ }
+ resp.CostCoins = chapter.SingleCoins
+ resp.CanWatch = false
+ resp.NeedUnlock = false
+ resp.NeedVip = false
+ resp.VipValid = false
+ resp.IsUnlocked = false
+
+ if chapter.PayType == playletchaptermod.PayTypeFree {
+ resp.CanWatch = true
+ return nil
+ }
+
+ var user *usermod.User
+ if userId > 0 {
+ var err *srvdef.SrvErr
+ user, err = findPlayletChapterAccessUser(ctx, userId)
+ if err != nil {
+ return err
+ }
+ resp.Balance = user.Recharge
+ resp.VipValid = isUserVipValid(user)
+ }
+
+ switch chapter.PayType {
+ case playletchaptermod.PayTypeCoin:
+ if userId <= 0 {
+ resp.NeedUnlock = true
+ return nil
+ }
+ unlocked, err := playletchapterunlockmod.ExistsPlayletChapterUnlock(ctx, userId, chapter.Id)
+ if err != nil {
+ return err
+ }
+ resp.IsUnlocked = unlocked
+ resp.CanWatch = unlocked
+ resp.NeedUnlock = !unlocked
+ case playletchaptermod.PayTypeVip:
+ resp.CanWatch = resp.VipValid
+ resp.NeedVip = !resp.VipValid
+ default:
+ log.Warn("未知短剧剧集支付方式", log.Any("playletId", chapter.PlayletId), log.Any("chapterId", chapter.Id), log.Any("payType", chapter.PayType))
+ resp.NeedUnlock = true
+ }
+
+ if !resp.CanWatch {
+ resp.VideoUrl = ""
+ }
+ return nil
+}
+
+// findPlayletChapterForUnlock 查询解锁所需的剧集和所属短剧,限制短剧必须已上架。
+func findPlayletChapterForUnlock(ctx context.Context, playletId int64, chapterId int64) (*playletchaptermod.PlayletChapter, *playletmod.Playlet, *srvdef.SrvErr) {
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: chapterId,
+ playletchaptermod.FN.PlayletChapter.PlayletId: playletId,
+ }, options.FindOne().SetProjection(playletChapterDetailProjection))
+ if err != nil {
+ log.Error("查询待解锁短剧剧集失败", log.Any("playletId", playletId), log.Any("chapterId", chapterId), log.E(err))
+ return nil, nil, err
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ }), options.FindOne().SetProjection(playletChapterSEOProjection))
+ if err != nil {
+ log.Error("查询待解锁剧集所属短剧失败", log.Any("playletId", chapter.PlayletId), log.Any("chapterId", chapter.Id), log.E(err))
+ return nil, nil, err
+ }
+ if err = ensureOriginalChapterWatermarkReady(playlet, chapter); err != nil {
+ return nil, nil, err
+ }
+ return chapter, playlet, nil
+}
+
+// findPlayletChapterAccessUser 查询播放权限判断需要的用户字段,避免读取完整用户文档。
+func findPlayletChapterAccessUser(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
+ user, err := usermod.FindOneUser(ctx, bson.M{
+ usermod.FN.User.Id: userId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: usermod.FN.User.Id, Value: 1},
+ {Key: usermod.FN.User.NickName, Value: 1},
+ {Key: usermod.FN.User.Recharge, Value: 1},
+ {Key: usermod.FN.User.CurrentVipExpire, Value: 1},
+ }))
+ if err != nil || user == nil {
+ return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ return user, nil
+}
+
+func isUserVipValid(user *usermod.User) bool {
+ return user != nil && user.CurrentVipExpire.After(time.Now())
+}
+
+func playletChapterUnlockOrderNo(userId int64, chapterId int64) string {
+ return fmt.Sprintf("JJS-%d-%d-%d", userId, chapterId, time.Now().UnixNano())
+}
+
+func playletChapterUnlockResp(chapter *playletchaptermod.PlayletChapter, balance int64, changeCoin int64) *proto.PlayletChapterUnlockResp {
+ return &proto.PlayletChapterUnlockResp{
+ PlayletId: chapter.PlayletId,
+ ChapterId: chapter.Id,
+ Unlocked: true,
+ CanWatch: true,
+ Balance: balance,
+ CostCoins: chapter.SingleCoins,
+ ChangeCoin: changeCoin,
+ }
+}
+
+// setPlayletLike 设置短剧点赞关系
+func setPlayletLike(ctx context.Context, userId int64, playletId int64, enabled bool) (*playletmod.Playlet, bool, *srvdef.SrvErr) {
+ // 操作接口必须登录,先查询短剧确保目标存在且已上架,再按点赞表真实变更决定是否更新点赞数。
+ if userId <= 0 {
+ return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ playlet, err := findPlayletOperateTarget(ctx, userId, playletId)
+ if err != nil {
+ return nil, false, err
+ }
+ filter := bson.M{
+ playletlikemod.FN.PlayletLike.UserId: userId,
+ playletlikemod.FN.PlayletLike.PlayletId: playletId,
+ }
+ changed := false
+ if enabled {
+ inserted, err := playletlikemod.InsertOnePlayletLike(ctx, &playletlikemod.PlayletLike{
+ UserId: userId,
+ PlayletId: playletId,
+ })
+ if err != nil {
+ log.Error("新增短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return nil, false, err
+ }
+ changed = inserted
+ } else {
+ deleted, err := playletlikemod.DeleteOnePlayletLike(ctx, filter)
+ if err != nil {
+ log.Error("删除短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return nil, false, err
+ }
+ changed = deleted > 0
+ }
+ if changed {
+ delta := int64(1)
+ if !enabled {
+ delta = -1
+ }
+ heatDelta := playletHeatLikeInc
+ if !enabled {
+ heatDelta = -playletHeatLikeInc
+ }
+ if err := incrementPlayletOperateCounter(ctx, playletId, playletmod.FN.Playlet.CountLike, delta, heatDelta); err != nil {
+ rollbackPlayletLike(ctx, userId, playletId, enabled)
+ return nil, false, err
+ }
+ // 真实点赞数和热度值已变更,异步同步 ES,保证搜索和榜单数据最终一致
+ syncPlayletToESAsync(playletId, "短剧点赞状态同步ES失败")
+ }
+ return playlet, changed, nil
+}
+
+// setPlayletCollect 设置短剧收藏关系
+func setPlayletCollect(ctx context.Context, userId int64, playletId int64, enabled bool) (*playletmod.Playlet, bool, *srvdef.SrvErr) {
+ // 操作接口必须登录,先查询短剧确保目标存在且已上架,再按收藏表真实变更决定是否更新收藏数。
+ if userId <= 0 {
+ return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ playlet, err := findPlayletOperateTarget(ctx, userId, playletId)
+ if err != nil {
+ return nil, false, err
+ }
+ filter := bson.M{
+ playletcollectmod.FN.PlayletCollect.UserId: userId,
+ playletcollectmod.FN.PlayletCollect.PlayletId: playletId,
+ }
+ changed := false
+ if enabled {
+ inserted, err := playletcollectmod.InsertOnePlayletCollect(ctx, &playletcollectmod.PlayletCollect{
+ UserId: userId,
+ PlayletId: playletId,
+ })
+ if err != nil {
+ log.Error("新增短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return nil, false, err
+ }
+ changed = inserted
+ } else {
+ deleted, err := playletcollectmod.DeleteOnePlayletCollect(ctx, filter)
+ if err != nil {
+ log.Error("删除短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return nil, false, err
+ }
+ changed = deleted > 0
+ }
+ if changed {
+ delta := int64(1)
+ if !enabled {
+ delta = -1
+ }
+ heatDelta := playletHeatCollectInc
+ if !enabled {
+ heatDelta = -playletHeatCollectInc
+ }
+ if err := incrementPlayletOperateCounter(ctx, playletId, playletmod.FN.Playlet.CountCollect, delta, heatDelta); err != nil {
+ rollbackPlayletCollect(ctx, userId, playletId, enabled)
+ return nil, false, err
+ }
+ // 真实收藏数和热度值已变更,异步同步 ES,保证追剧榜和搜索数据最终一致
+ syncPlayletToESAsync(playletId, "短剧收藏状态同步ES失败")
+ }
+ return playlet, changed, nil
+}
+
+// setPlayletChapterLike 设置短剧剧集点赞关系
+func setPlayletChapterLike(ctx context.Context, userId int64, chapterId int64, enabled bool) (*playletchaptermod.PlayletChapter, bool, *srvdef.SrvErr) {
+ // 剧集点赞必须登录,先校验剧集存在且所属短剧已上架,再按点赞关系真实变化更新 playlet_chapter.countLike。
+ if userId <= 0 {
+ return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ chapter, err := findPlayletChapterOperateTarget(ctx, userId, chapterId)
+ if err != nil {
+ return nil, false, err
+ }
+ filter := bson.M{
+ playletchapterlikemod.FN.PlayletChapterLike.UserId: userId,
+ playletchapterlikemod.FN.PlayletChapterLike.ChapterId: chapterId,
+ }
+ changed := false
+ if enabled {
+ inserted, err := playletchapterlikemod.InsertOnePlayletChapterLike(ctx, &playletchapterlikemod.PlayletChapterLike{
+ UserId: userId,
+ PlayletId: chapter.PlayletId,
+ ChapterId: chapterId,
+ })
+ if err != nil {
+ log.Error("新增短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
+ return nil, false, err
+ }
+ changed = inserted
+ } else {
+ deleted, err := playletchapterlikemod.DeleteOnePlayletChapterLike(ctx, filter)
+ if err != nil {
+ log.Error("删除短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
+ return nil, false, err
+ }
+ changed = deleted > 0
+ }
+ if changed {
+ delta := int64(1)
+ if !enabled {
+ delta = -1
+ }
+ if err := incrementPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountLike, delta); err != nil {
+ rollbackPlayletChapterLike(ctx, userId, chapter.PlayletId, chapterId, enabled)
+ return nil, false, err
+ }
+ heatDelta := playletHeatLikeInc
+ if !enabled {
+ heatDelta = -playletHeatLikeInc
+ }
+ if err := incrementPlayletHeatValue(ctx, chapter.PlayletId, heatDelta); err != nil {
+ log.Error("更新短剧剧集点赞对应短剧热度失败", log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.Any("增量", heatDelta), log.E(err))
+ rollbackPlayletChapterLike(ctx, userId, chapter.PlayletId, chapterId, enabled)
+ rollbackPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountLike, delta)
+ return nil, false, err
+ }
+ }
+ return chapter, changed, nil
+}
+
+// setPlayletChapterCollect 设置短剧剧集收藏关系
+func setPlayletChapterCollect(ctx context.Context, userId int64, chapterId int64, enabled bool) (*playletchaptermod.PlayletChapter, bool, *srvdef.SrvErr) {
+ // 剧集收藏必须登录,先校验剧集存在且所属短剧已上架,再按收藏关系真实变化更新 playlet_chapter.countCollect。
+ if userId <= 0 {
+ return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ chapter, err := findPlayletChapterOperateTarget(ctx, userId, chapterId)
+ if err != nil {
+ return nil, false, err
+ }
+ filter := bson.M{
+ playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
+ playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: chapterId,
+ }
+ changed := false
+ if enabled {
+ inserted, err := playletchaptercollectmod.InsertOnePlayletChapterCollect(ctx, &playletchaptercollectmod.PlayletChapterCollect{
+ UserId: userId,
+ PlayletId: chapter.PlayletId,
+ ChapterId: chapterId,
+ })
+ if err != nil {
+ log.Error("新增短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
+ return nil, false, err
+ }
+ changed = inserted
+ } else {
+ deleted, err := playletchaptercollectmod.DeleteOnePlayletChapterCollect(ctx, filter)
+ if err != nil {
+ log.Error("删除短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
+ return nil, false, err
+ }
+ changed = deleted > 0
+ }
+ if changed {
+ delta := int64(1)
+ if !enabled {
+ delta = -1
+ }
+ if err := incrementPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountCollect, delta); err != nil {
+ rollbackPlayletChapterCollect(ctx, userId, chapter.PlayletId, chapterId, enabled)
+ return nil, false, err
+ }
+ heatDelta := playletHeatCollectInc
+ if !enabled {
+ heatDelta = -playletHeatCollectInc
+ }
+ if err := incrementPlayletHeatValue(ctx, chapter.PlayletId, heatDelta); err != nil {
+ log.Error("更新短剧剧集收藏对应短剧热度失败", log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.Any("增量", heatDelta), log.E(err))
+ rollbackPlayletChapterCollect(ctx, userId, chapter.PlayletId, chapterId, enabled)
+ rollbackPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountCollect, delta)
+ return nil, false, err
+ }
+ }
+ return chapter, changed, nil
+}
+
+// findPlayletOperateTarget 查询短剧操作目标
+func findPlayletOperateTarget(ctx context.Context, userId int64, playletId int64) (*playletmod.Playlet, *srvdef.SrvErr) {
+ // 操作前只读取计数字段,减少Mongo返回体;状态过滤保证下架短剧不能被点赞或收藏。
+ playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: playletId,
+ }), options.FindOne().SetProjection(playletOperateProjection))
+ if err != nil {
+ log.Error("查询短剧操作目标失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return nil, err
+ }
+ return playlet, nil
+}
+
+// findPlayletChapterOperateTarget 查询短剧剧集操作目标
+func findPlayletChapterOperateTarget(ctx context.Context, userId int64, chapterId int64) (*playletchaptermod.PlayletChapter, *srvdef.SrvErr) {
+ // 先用章节ID读取最小字段,再校验所属短剧已上架,避免用户操作已下架短剧的剧集。
+ chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: chapterId,
+ }, options.FindOne().SetProjection(playletChapterOperateProjection))
+ if err != nil {
+ log.Error("查询短剧剧集操作目标失败", log.Any("用户ID", userId), log.Any("剧集ID", chapterId), log.E(err))
+ return nil, err
+ }
+ if _, err = playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: chapter.PlayletId,
+ }), options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ })); err != nil {
+ log.Error("查询短剧剧集所属短剧失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
+ return nil, err
+ }
+ return chapter, nil
+}
+
+// rollbackPlayletLike 回滚短剧点赞关系
+func rollbackPlayletLike(ctx context.Context, userId int64, playletId int64, enabled bool) {
+ // 点赞表已经变化但短剧点赞数更新失败时做补偿,尽量保持点赞关系和点赞数一致。
+ filter := bson.M{
+ playletlikemod.FN.PlayletLike.UserId: userId,
+ playletlikemod.FN.PlayletLike.PlayletId: playletId,
+ }
+ if enabled {
+ if _, err := playletlikemod.DeleteOnePlayletLike(ctx, filter); err != nil {
+ log.Error("回滚短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ }
+ return
+ }
+ if _, err := playletlikemod.InsertOnePlayletLike(ctx, &playletlikemod.PlayletLike{
+ UserId: userId,
+ PlayletId: playletId,
+ }); err != nil {
+ log.Error("恢复短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ }
+}
+
+// rollbackPlayletCollect 回滚短剧收藏关系
+func rollbackPlayletCollect(ctx context.Context, userId int64, playletId int64, enabled bool) {
+ // 收藏表已经变化但短剧收藏数更新失败时做补偿,尽量保持收藏关系和收藏数一致。
+ filter := bson.M{
+ playletcollectmod.FN.PlayletCollect.UserId: userId,
+ playletcollectmod.FN.PlayletCollect.PlayletId: playletId,
+ }
+ if enabled {
+ if _, err := playletcollectmod.DeleteOnePlayletCollect(ctx, filter); err != nil {
+ log.Error("回滚短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ }
+ return
+ }
+ if _, err := playletcollectmod.InsertOnePlayletCollect(ctx, &playletcollectmod.PlayletCollect{
+ UserId: userId,
+ PlayletId: playletId,
+ }); err != nil {
+ log.Error("恢复短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ }
+}
+
+// rollbackPlayletChapterLike 回滚短剧剧集点赞关系
+func rollbackPlayletChapterLike(ctx context.Context, userId int64, playletId int64, chapterId int64, enabled bool) {
+ // 剧集点赞关系已经变化但剧集点赞数更新失败时做补偿,尽量保持点赞关系和点赞数一致。
+ filter := bson.M{
+ playletchapterlikemod.FN.PlayletChapterLike.UserId: userId,
+ playletchapterlikemod.FN.PlayletChapterLike.ChapterId: chapterId,
+ }
+ if enabled {
+ if _, err := playletchapterlikemod.DeleteOnePlayletChapterLike(ctx, filter); err != nil {
+ log.Error("回滚短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
+ }
+ return
+ }
+ if _, err := playletchapterlikemod.InsertOnePlayletChapterLike(ctx, &playletchapterlikemod.PlayletChapterLike{
+ UserId: userId,
+ PlayletId: playletId,
+ ChapterId: chapterId,
+ }); err != nil {
+ log.Error("恢复短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
+ }
+}
+
+// rollbackPlayletChapterCollect 回滚短剧剧集收藏关系
+func rollbackPlayletChapterCollect(ctx context.Context, userId int64, playletId int64, chapterId int64, enabled bool) {
+ // 剧集收藏关系已经变化但剧集收藏数更新失败时做补偿,尽量保持收藏关系和收藏数一致。
+ filter := bson.M{
+ playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
+ playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: chapterId,
+ }
+ if enabled {
+ if _, err := playletchaptercollectmod.DeleteOnePlayletChapterCollect(ctx, filter); err != nil {
+ log.Error("回滚短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
+ }
+ return
+ }
+ if _, err := playletchaptercollectmod.InsertOnePlayletChapterCollect(ctx, &playletchaptercollectmod.PlayletChapterCollect{
+ UserId: userId,
+ PlayletId: playletId,
+ ChapterId: chapterId,
+ }); err != nil {
+ log.Error("恢复短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
+ }
+}
+
+// incrementPlayletOperateCounter 增减短剧点赞或收藏计数
+func incrementPlayletOperateCounter(ctx context.Context, playletId int64, countField string, delta int64, heatDelta int64) *srvdef.SrvErr {
+ // 取消操作时增加 countField > 0 条件,避免历史脏数据或并发场景把计数减成负数。
+ filter := bson.M{
+ playletmod.FN.Playlet.Id: playletId,
+ }
+ if delta < 0 {
+ filter[countField] = bson.M{"$gt": 0}
+ }
+ incDoc := bson.M{
+ countField: delta,
+ }
+ if heatDelta != 0 {
+ incDoc[playletmod.FN.Playlet.HeatValue] = heatDelta
+ }
+ err := playletmod.UpdateOnePlaylet(ctx, filter, bson.M{
+ "$inc": incDoc,
+ })
+ if err != nil {
+ if delta < 0 && err.Code == srvdef.ErrNoData {
+ log.Warn("短剧操作计数已为0,跳过扣减", log.Any("短剧ID", playletId), log.Any("计数字段", countField))
+ return nil
+ }
+ log.Error("更新短剧操作计数失败", log.Any("短剧ID", playletId), log.Any("计数字段", countField), log.Any("增量", delta), log.E(err))
+ return err
+ }
+ return nil
+}
+
+// incrementPlayletChapterOperateCounter 增减短剧剧集点赞或收藏计数
+func incrementPlayletChapterOperateCounter(ctx context.Context, chapterId int64, countField string, delta int64) *srvdef.SrvErr {
+ // 取消操作时增加 countField > 0 条件,避免历史脏数据或并发场景把计数减成负数。
+ filter := bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: chapterId,
+ }
+ if delta < 0 {
+ filter[countField] = bson.M{"$gt": 0}
+ }
+ err := playletchaptermod.UpdateOnePlayletChapter(ctx, filter, bson.M{
+ "$inc": bson.M{
+ countField: delta,
+ },
+ })
+ if err != nil {
+ if delta < 0 && err.Code == srvdef.ErrNoData {
+ log.Warn("短剧剧集操作计数已为0,跳过扣减", log.Any("剧集ID", chapterId), log.Any("计数字段", countField))
+ return nil
+ }
+ log.Error("更新短剧剧集操作计数失败", log.Any("剧集ID", chapterId), log.Any("计数字段", countField), log.Any("增量", delta), log.E(err))
+ return err
+ }
+ return nil
+}
+
+func rollbackPlayletChapterOperateCounter(ctx context.Context, chapterId int64, countField string, delta int64) {
+ rollbackDelta := -delta
+ if err := incrementPlayletChapterOperateCounter(ctx, chapterId, countField, rollbackDelta); err != nil {
+ log.Error("回滚短剧剧集操作计数失败", log.Any("剧集ID", chapterId), log.Any("计数字段", countField), log.Any("增量", rollbackDelta), log.E(err))
+ }
+}
+
+// applyLocalCounter 根据本次关系变更结果计算响应计数
+func applyLocalCounter(current int64, enabled bool, changed bool) int64 {
+ // 响应计数基于操作前短剧计数和本次真实变更计算,避免为返回值再查一次短剧表。
+ if !changed {
+ return current
+ }
+ if enabled {
+ return current + 1
+ }
+ if current <= 0 {
+ return 0
+ }
+ return current - 1
+}
+
+func displayPlayletCountLike(countLike int64, virtualCountLike int64) int64 {
+ return countLike + virtualCountLike
+}
+
+// syncPlayletToESAsync 异步同步短剧到ES
+func syncPlayletToESAsync(playletId int64, failMsg string) {
+ if playletId <= 0 {
+ return
+ }
+ go func() {
+ // 点赞、收藏入口只允许操作已上架短剧
+ if err := srv_search.SyncPlayletStatusToES(context.Background(), []int64{playletId}, playletmod.StatusUp); err != nil {
+ log.Error(failMsg, log.Any("短剧ID", playletId), log.E(err))
+ }
+ }()
+}
+
+// operateStatusByFlag 转换操作状态响应值
+func operateStatusByFlag(enabled bool) int64 {
+ if enabled {
+ return operateStatusYes
+ }
+ return operateStatusNo
+}
+
+// Detail 根据ID查询app端短剧详情
+func Detail(ctx context.Context, userId int64, req *proto.PlayletInfoReq) (*proto.PlayletInfoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
+ }
+ playletID, err := resolvePlayletID(ctx, req.Id, req.Slug, true)
+ if err != nil {
+ return nil, err
+ }
+ req.Id = playletID
+ item, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: req.Id,
+ }), options.FindOne().SetProjection(playletHomeProjection))
+ if err != nil {
+ log.Error("查询短剧详情失败", log.Any("短剧ID", req.Id), log.E(err))
+ return nil, err
+ }
+ if err = incrementPlayletBrowse(ctx, item.Id); err != nil {
+ log.Error("增加短剧观看量失败", log.Any("短剧ID", item.Id), log.E(err))
+ return nil, err
+ }
+ item.CountBrowse++
+ item.HeatValue += playletHeatClickInc
+ relatedPlaylets, err := findRelatedRecommendPlaylets(ctx, item)
+ if err != nil {
+ return nil, err
+ }
+ allPlaylets := mergeHomePlayletLists(playletmod.PlayletList{item}, relatedPlaylets)
+ themeNameMap, err := buildHomeThemeNameMap(ctx, allPlaylets)
+ if err != nil {
+ log.Error("查询短剧详情题材名称失败", log.Any("短剧ID", item.Id), log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, allPlaylets)
+ if err != nil {
+ log.Error("查询短剧详情标签名称失败", log.Any("短剧ID", item.Id), log.E(err))
+ return nil, err
+ }
+ commonList := playletsToHomeCommonData(ctx, playletmod.PlayletList{item}, themeNameMap, tagNameMap)
+ if len(commonList) == 0 || commonList[0] == nil {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ chapterInfo, err := findPlayletChapters(ctx, item.Id, userId, item.IsOriginal)
+ if err != nil {
+ return nil, err
+ }
+ isLike, isCollect, err := findPlayletUserOperateStatus(ctx, userId, item.Id)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletInfoResp{
+ CommonData: *commonList[0],
+ IsLike: isLike,
+ IsCollect: isCollect,
+ RelatedRecommendData: playletsToHomeCommonData(ctx, relatedPlaylets, themeNameMap, tagNameMap),
+ PlayletChapterInfo: chapterInfo,
+ SeoInfo: buildPlayletDetailSeoInfo(item, themeNameMap, tagNameMap),
+ }
+ return resp, nil
+}
+
+// findPlayletUserOperateStatus 查询当前用户短剧点赞和收藏状态
+func findPlayletUserOperateStatus(ctx context.Context, userId int64, playletId int64) (int64, int64, *srvdef.SrvErr) {
+ // 游客或无有效token时没有用户ID,直接返回未点赞、未收藏,保证详情接口仍可匿名访问。
+ if userId <= 0 {
+ return operateStatusNo, operateStatusNo, nil
+ }
+ likeExists, err := playletlikemod.ExistsPlayletLike(ctx, bson.M{
+ playletlikemod.FN.PlayletLike.UserId: userId,
+ playletlikemod.FN.PlayletLike.PlayletId: playletId,
+ })
+ if err != nil {
+ log.Error("查询短剧用户点赞状态失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return operateStatusNo, operateStatusNo, err
+ }
+ collectExists, err := playletcollectmod.ExistsPlayletCollect(ctx, bson.M{
+ playletcollectmod.FN.PlayletCollect.UserId: userId,
+ playletcollectmod.FN.PlayletCollect.PlayletId: playletId,
+ })
+ if err != nil {
+ log.Error("查询短剧用户收藏状态失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
+ return operateStatusNo, operateStatusNo, err
+ }
+ isLike := operateStatusByFlag(likeExists)
+ isCollect := operateStatusByFlag(collectExists)
+ return isLike, isCollect, nil
+}
+
+// CollectList 分页查询当前用户的短剧收藏列表
+func CollectList(ctx context.Context, userId int64, req *proto.PlayletCollectListReq) (*proto.PlayletCollectListResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := bson.M{
+ playletcollectmod.FN.PlayletCollect.UserId: userId,
+ }
+ totalCount, err := playletcollectmod.CountPlayletCollect(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletCollectListResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.PlayletCollectData, 0),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ collectList, err := playletcollectmod.FindManyPlayletCollect(ctx, filter, options.Find().
+ SetProjection(bson.D{
+ {Key: playletcollectmod.FN.PlayletCollect.Id, Value: 1},
+ {Key: playletcollectmod.FN.PlayletCollect.PlayletId, Value: 1},
+ {Key: playletcollectmod.FN.PlayletCollect.CreatedAt, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletcollectmod.FN.PlayletCollect.CreatedAt, Value: -1},
+ {Key: playletcollectmod.FN.PlayletCollect.Id, Value: -1},
+ }).
+ SetSkip(skip).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ if len(collectList) == 0 {
+ return resp, nil
+ }
+ playletIds := collectPlayletIdsFromCollects(collectList)
+ playletMap, err := findCollectPlayletMap(ctx, playletIds)
+ if err != nil {
+ return nil, err
+ }
+ progressMap, err := findCollectWatchProgressMap(ctx, userId, playletIds)
+ if err != nil {
+ return nil, err
+ }
+ playletList := playletListFromMapByCollects(collectList, playletMap)
+ themeNameMap, err := buildHomeThemeNameMap(ctx, playletList)
+ if err != nil {
+ log.Error("查询我的收藏题材名称失败", log.Any("用户ID", userId), log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, playletList)
+ if err != nil {
+ log.Error("查询我的收藏标签名称失败", log.Any("用户ID", userId), log.E(err))
+ return nil, err
+ }
+ resp.List = buildPlayletCollectData(ctx, collectList, playletMap, progressMap, themeNameMap, tagNameMap)
+ return resp, nil
+}
+
+// ChapterCollectList 分页查询当前用户收藏的短剧剧集列表
+func ChapterCollectList(ctx context.Context, userId int64, req *proto.PlayletChapterCollectListReq) (*proto.PlayletChapterCollectListResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
+ filter := bson.M{
+ playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
+ }
+ totalCount, err := playletchaptercollectmod.CountPlayletChapterCollect(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletChapterCollectListResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.PlayletChapterCollectData, 0),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ collectList, err := playletchaptercollectmod.FindManyPlayletChapterCollect(ctx, filter, options.Find().
+ SetProjection(bson.D{
+ {Key: playletchaptercollectmod.FN.PlayletChapterCollect.Id, Value: 1},
+ {Key: playletchaptercollectmod.FN.PlayletChapterCollect.PlayletId, Value: 1},
+ {Key: playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId, Value: 1},
+ {Key: playletchaptercollectmod.FN.PlayletChapterCollect.CreatedAt, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletchaptercollectmod.FN.PlayletChapterCollect.CreatedAt, Value: -1},
+ {Key: playletchaptercollectmod.FN.PlayletChapterCollect.Id, Value: -1},
+ }).
+ SetSkip(skip).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ if len(collectList) == 0 {
+ return resp, nil
+ }
+ chapterIds := collectChapterIdsFromChapterCollects(collectList)
+ chapterMap, err := findChapterCollectChapterMap(ctx, chapterIds)
+ if err != nil {
+ return nil, err
+ }
+ playletIds := collectPlayletIdsFromChapterCollects(collectList, chapterMap)
+ playletMap, err := findChapterCollectPlayletMap(ctx, playletIds)
+ if err != nil {
+ return nil, err
+ }
+ resp.List = buildPlayletChapterCollectData(ctx, collectList, chapterMap, playletMap)
+ return resp, nil
+}
+
+// HistoryList 分页查询当前用户的短剧观看历史
+func HistoryList(ctx context.Context, userId int64, req *proto.PlayletHistoryListReq) (*proto.PlayletHistoryListResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizePlayletHistoryPage(req.PageNum, req.PageSize)
+ filter := bson.M{
+ playletwatchhistorymod.FN.PlayletWatchHistory.UserId: userId,
+ }
+ totalCount, err := playletwatchhistorymod.CountPlayletWatchHistory(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletHistoryListResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.PlayletHistoryData, 0),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ return resp, nil
+ }
+ historyList, err := playletwatchhistorymod.FindManyPlayletWatchHistory(ctx, filter, options.Find().
+ SetProjection(bson.D{
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.Id, Value: 1},
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.PlayletId, Value: 1},
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.LastChapterId, Value: 1},
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.LastEpisode, Value: 1},
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.UpdatedAt, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.UpdatedAt, Value: -1},
+ {Key: playletwatchhistorymod.FN.PlayletWatchHistory.Id, Value: -1},
+ }).
+ SetSkip(skip).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ if len(historyList) == 0 {
+ return resp, nil
+ }
+ playletIds := collectPlayletIdsFromHistories(historyList)
+ playletMap, err := findHistoryPlayletMap(ctx, playletIds)
+ if err != nil {
+ return nil, err
+ }
+ progressMap, err := findCollectWatchProgressMap(ctx, userId, playletIds)
+ if err != nil {
+ return nil, err
+ }
+ playletList := playletListFromMapByHistories(historyList, playletMap)
+ themeNameMap, err := buildHomeThemeNameMap(ctx, playletList)
+ if err != nil {
+ log.Error("查询观看历史题材名称失败", log.Any("用户ID", userId), log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, playletList)
+ if err != nil {
+ log.Error("查询观看历史标签名称失败", log.Any("用户ID", userId), log.E(err))
+ return nil, err
+ }
+ resp.List = buildPlayletHistoryData(ctx, historyList, playletMap, progressMap, themeNameMap, tagNameMap)
+ return resp, nil
+}
+
+// ClearHistory 清空当前用户的短剧观看历史,不影响观看进度和收藏列表。
+func ClearHistory(ctx context.Context, userId int64) (*proto.PlayletHistoryClearResp, *srvdef.SrvErr) {
+ if userId <= 0 {
+ return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
+ }
+ deleted, err := playletwatchhistorymod.DeleteManyPlayletWatchHistory(ctx, bson.M{
+ playletwatchhistorymod.FN.PlayletWatchHistory.UserId: userId,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &proto.PlayletHistoryClearResp{DeletedCount: deleted}, nil
+}
+
+// collectPlayletIdsFromCollects 按收藏列表顺序收集短剧ID并去重,避免批量查询重复ID。
+func collectPlayletIdsFromCollects(list playletcollectmod.PlayletCollectList) []int64 {
+ result := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, item := range list {
+ if item == nil || item.PlayletId <= 0 {
+ continue
+ }
+ if _, ok := seen[item.PlayletId]; ok {
+ continue
+ }
+ seen[item.PlayletId] = struct{}{}
+ result = append(result, item.PlayletId)
+ }
+ return result
+}
+
+// collectPlayletIdsFromHistories 按观看历史顺序收集短剧ID并去重,避免批量查询重复ID。
+func collectPlayletIdsFromHistories(list playletwatchhistorymod.PlayletWatchHistoryList) []int64 {
+ result := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, item := range list {
+ if item == nil || item.PlayletId <= 0 {
+ continue
+ }
+ if _, ok := seen[item.PlayletId]; ok {
+ continue
+ }
+ seen[item.PlayletId] = struct{}{}
+ result = append(result, item.PlayletId)
+ }
+ return result
+}
+
+// collectChapterIdsFromChapterCollects 按剧集收藏顺序收集剧集ID并去重。
+func collectChapterIdsFromChapterCollects(list playletchaptercollectmod.PlayletChapterCollectList) []int64 {
+ result := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, item := range list {
+ if item == nil || item.ChapterId <= 0 {
+ continue
+ }
+ if _, ok := seen[item.ChapterId]; ok {
+ continue
+ }
+ seen[item.ChapterId] = struct{}{}
+ result = append(result, item.ChapterId)
+ }
+ return result
+}
+
+// collectPlayletIdsFromChapterCollects 从收藏关系和剧集数据中收集短剧ID,兼容收藏关系里playletId缺失的历史数据。
+func collectPlayletIdsFromChapterCollects(
+ list playletchaptercollectmod.PlayletChapterCollectList,
+ chapterMap map[int64]*playletchaptermod.PlayletChapter,
+) []int64 {
+ result := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ playletId := item.PlayletId
+ if playletId <= 0 {
+ if chapter := chapterMap[item.ChapterId]; chapter != nil {
+ playletId = chapter.PlayletId
+ }
+ }
+ if playletId <= 0 {
+ continue
+ }
+ if _, ok := seen[playletId]; ok {
+ continue
+ }
+ seen[playletId] = struct{}{}
+ result = append(result, playletId)
+ }
+ return result
+}
+
+// findCollectPlayletMap 批量查询收藏关联的短剧数据,返回map便于按收藏时间顺序组装。
+func findCollectPlayletMap(ctx context.Context, playletIds []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
+ result := make(map[int64]*playletmod.Playlet, len(playletIds))
+ if len(playletIds) == 0 {
+ return result, nil
+ }
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
+ }, options.Find().SetProjection(playletCollectListProjection))
+ if err != nil {
+ log.Error("批量查询我的收藏短剧失败", log.Any("短剧ID集合", playletIds), log.E(err))
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result[item.Id] = item
+ }
+ return result, nil
+}
+
+// findChapterCollectPlayletMap 批量查询剧集收藏关联的短剧数据。
+func findChapterCollectPlayletMap(ctx context.Context, playletIds []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
+ result := make(map[int64]*playletmod.Playlet, len(playletIds))
+ if len(playletIds) == 0 {
+ return result, nil
+ }
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.Cover, Value: 1},
+ {Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ }))
+ if err != nil {
+ log.Error("批量查询我的剧集收藏短剧失败", log.Any("短剧ID集合", playletIds), log.E(err))
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result[item.Id] = item
+ }
+ return result, nil
+}
+
+// findChapterCollectChapterMap 批量查询收藏关联的剧集数据。
+func findChapterCollectChapterMap(ctx context.Context, chapterIds []int64) (map[int64]*playletchaptermod.PlayletChapter, *srvdef.SrvErr) {
+ result := make(map[int64]*playletchaptermod.PlayletChapter, len(chapterIds))
+ if len(chapterIds) == 0 {
+ return result, nil
+ }
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": chapterIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.VideoUrl, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.MediaTime, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PayType, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.SingleCoins, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountBrowse, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CountCollect, Value: 1},
+ }))
+ if err != nil {
+ log.Error("批量查询我的剧集收藏剧集失败", log.Any("剧集ID集合", chapterIds), log.E(err))
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result[item.Id] = item
+ }
+ return result, nil
+}
+
+// findHistoryPlayletMap 批量查询观看历史关联的短剧数据,返回map便于按观看时间顺序组装。
+func findHistoryPlayletMap(ctx context.Context, playletIds []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
+ result := make(map[int64]*playletmod.Playlet, len(playletIds))
+ if len(playletIds) == 0 {
+ return result, nil
+ }
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
+ }, options.Find().SetProjection(playletHomeProjection))
+ if err != nil {
+ log.Error("批量查询观看历史短剧失败", log.Any("短剧ID集合", playletIds), log.E(err))
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result[item.Id] = item
+ }
+ return result, nil
+}
+
+// findCollectWatchProgressMap 批量查询当前用户收藏短剧的观看进度,避免收藏列表逐条查询。
+func findCollectWatchProgressMap(ctx context.Context, userId int64, playletIds []int64) (map[int64]*playletwatchprogressmod.PlayletWatchProgress, *srvdef.SrvErr) {
+ result := make(map[int64]*playletwatchprogressmod.PlayletWatchProgress, len(playletIds))
+ if userId <= 0 || len(playletIds) == 0 {
+ return result, nil
+ }
+ list, err := playletwatchprogressmod.FindManyPlayletWatchProgress(ctx, bson.M{
+ playletwatchprogressmod.FN.PlayletWatchProgress.UserId: userId,
+ playletwatchprogressmod.FN.PlayletWatchProgress.PlayletId: bson.M{"$in": playletIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletwatchprogressmod.FN.PlayletWatchProgress.PlayletId, Value: 1},
+ {Key: playletwatchprogressmod.FN.PlayletWatchProgress.LastChapterId, Value: 1},
+ {Key: playletwatchprogressmod.FN.PlayletWatchProgress.LastEpisode, Value: 1},
+ {Key: playletwatchprogressmod.FN.PlayletWatchProgress.WatchedEpisodes, Value: 1},
+ {Key: playletwatchprogressmod.FN.PlayletWatchProgress.WatchedCount, Value: 1},
+ }))
+ if err != nil {
+ log.Error("批量查询我的收藏观看进度失败", log.Any("用户ID", userId), log.Any("短剧ID集合", playletIds), log.E(err))
+ return nil, err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result[item.PlayletId] = item
+ }
+ return result, nil
+}
+
+// playletListFromMapByCollects 按收藏列表顺序构造短剧列表,用于批量查询题材和标签名称。
+func playletListFromMapByCollects(collectList playletcollectmod.PlayletCollectList, playletMap map[int64]*playletmod.Playlet) playletmod.PlayletList {
+ result := make(playletmod.PlayletList, 0, len(collectList))
+ for _, collect := range collectList {
+ if collect == nil {
+ continue
+ }
+ if item := playletMap[collect.PlayletId]; item != nil {
+ result = append(result, item)
+ }
+ }
+ return result
+}
+
+// playletListFromMapByHistories 按观看历史顺序构造短剧列表,用于批量查询题材和标签名称。
+func playletListFromMapByHistories(historyList playletwatchhistorymod.PlayletWatchHistoryList, playletMap map[int64]*playletmod.Playlet) playletmod.PlayletList {
+ result := make(playletmod.PlayletList, 0, len(historyList))
+ for _, history := range historyList {
+ if history == nil {
+ continue
+ }
+ if item := playletMap[history.PlayletId]; item != nil {
+ result = append(result, item)
+ }
+ }
+ return result
+}
+
+// buildPlayletCollectData 组装我的收藏响应,保持playlet_collect的倒序分页顺序。
+func buildPlayletCollectData(
+ ctx context.Context,
+ collectList playletcollectmod.PlayletCollectList,
+ playletMap map[int64]*playletmod.Playlet,
+ progressMap map[int64]*playletwatchprogressmod.PlayletWatchProgress,
+ themeNameMap map[int64]string,
+ tagNameMap map[int64]string,
+) []proto.PlayletCollectData {
+ result := make([]proto.PlayletCollectData, 0, len(collectList))
+ for _, collect := range collectList {
+ if collect == nil {
+ continue
+ }
+ item := playletMap[collect.PlayletId]
+ if item == nil {
+ continue
+ }
+ progress := progressMap[item.Id]
+ watchedCount := collectWatchedCount(progress, item.TotalEpisode)
+ result = append(result, proto.PlayletCollectData{
+ Id: item.Id,
+ Title: item.Title,
+ PlayName: item.PlayName,
+ Slug: item.Slug,
+ Cover: withImageDomain(ctx, item.DisplayCover()),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: namesByIds(item.ThemeIds, themeNameMap),
+ ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
+ TagIds: item.TagIds,
+ TagNames: namesByIds(item.TagIds, tagNameMap),
+ TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.CountBrowse,
+ HeatValue: item.HeatValue,
+ CountCollect: item.CountCollect,
+ IsCollect: operateStatusYes,
+ CollectedAt: collect.CreatedAt,
+ WatchedCount: watchedCount,
+ UnwatchedCount: collectUnwatchedCount(item.TotalEpisode, watchedCount),
+ LastChapterId: collectLastChapterId(progress),
+ LastEpisode: collectLastEpisode(progress),
+ SeoInfo: item.SeoInfo,
+ })
+ }
+ return result
+}
+
+// buildPlayletChapterCollectData 组装我的剧集收藏响应,保持playlet_chapter_collect的倒序分页顺序。
+func buildPlayletChapterCollectData(
+ ctx context.Context,
+ collectList playletchaptercollectmod.PlayletChapterCollectList,
+ chapterMap map[int64]*playletchaptermod.PlayletChapter,
+ playletMap map[int64]*playletmod.Playlet,
+) []proto.PlayletChapterCollectData {
+ result := make([]proto.PlayletChapterCollectData, 0, len(collectList))
+ for _, collect := range collectList {
+ if collect == nil {
+ continue
+ }
+ chapter := chapterMap[collect.ChapterId]
+ if chapter == nil {
+ continue
+ }
+ playletId := collect.PlayletId
+ if playletId <= 0 {
+ playletId = chapter.PlayletId
+ }
+ playlet := playletMap[playletId]
+ if playlet == nil {
+ continue
+ }
+ result = append(result, proto.PlayletChapterCollectData{
+ PlayletId: playlet.Id,
+ PlayletTitle: playlet.Title,
+ PlayletCover: withImageDomain(ctx, playlet.DisplayCover()),
+ TotalEpisode: playlet.TotalEpisode,
+ UpdateStatus: playlet.UpdateStatus,
+ ChapterId: chapter.Id,
+ CurrentEpisode: chapter.CurrentEpisode,
+ ChapterTitle: chapter.Title,
+ VideoUrl: playletChapterPlayVideoUrl(chapter),
+ MediaTime: chapter.MediaTime,
+ PayType: int32(chapter.PayType),
+ SingleCoins: chapter.SingleCoins,
+ CountBrowse: chapter.CountBrowse,
+ CountCollect: chapter.CountCollect,
+ IsCollect: operateStatusYes,
+ CollectedAt: collect.CreatedAt,
+ })
+ }
+ return result
+}
+
+// buildPlayletHistoryData 组装观看历史响应,保持playlet_watch_history的倒序分页顺序。
+func buildPlayletHistoryData(
+ ctx context.Context,
+ historyList playletwatchhistorymod.PlayletWatchHistoryList,
+ playletMap map[int64]*playletmod.Playlet,
+ progressMap map[int64]*playletwatchprogressmod.PlayletWatchProgress,
+ themeNameMap map[int64]string,
+ tagNameMap map[int64]string,
+) []proto.PlayletHistoryData {
+ result := make([]proto.PlayletHistoryData, 0, len(historyList))
+ for _, history := range historyList {
+ if history == nil {
+ continue
+ }
+ item := playletMap[history.PlayletId]
+ if item == nil {
+ continue
+ }
+ progress := progressMap[item.Id]
+ watchedCount := collectWatchedCount(progress, item.TotalEpisode)
+ result = append(result, proto.PlayletHistoryData{
+ CommonData: proto.CommonData{
+ Id: item.Id,
+ Status: int32(item.Status),
+ IsFeatured: item.IsFeatured,
+ IsRecommend: item.IsRecommend,
+ IsNew: isOnlineThisWeek(item.OnlineAt),
+ OperateType: int32(playletmod.NormalizeOperateType(item.OperateType)),
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: withImageDomain(ctx, item.Banner),
+ OnlineAt: item.OnlineAt,
+ MediaCreatedAt: item.MediaCreatedAt,
+ MediaUpdatedAt: item.MediaUpdatedAt,
+ ThemeIds: item.ThemeIds,
+ ThemeNames: namesByIds(item.ThemeIds, themeNameMap),
+ ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
+ Title: item.Title,
+ PlayName: item.PlayName,
+ Description: item.Description,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: withImageDomain(ctx, item.DisplayCover()),
+ TagIds: item.TagIds,
+ TagNames: namesByIds(item.TagIds, tagNameMap),
+ TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.CountBrowse,
+ CountCollect: item.CountCollect,
+ CountLike: displayPlayletCountLike(item.CountLike, item.VirtualCountLike),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: item.SeoInfo,
+ },
+ LastChapterId: history.LastChapterId,
+ LastEpisode: history.LastEpisode,
+ WatchedCount: watchedCount,
+ UnwatchedCount: collectUnwatchedCount(item.TotalEpisode, watchedCount),
+ IsFinished: isPlayletFinished(item.TotalEpisode, watchedCount),
+ HistoryAt: history.UpdatedAt,
+ })
+ }
+ return result
+}
+
+// collectWatchedCount 读取已看集数,兼容仅保存watchedEpisodes但未写watchedCount的历史数据。
+func collectWatchedCount(progress *playletwatchprogressmod.PlayletWatchProgress, totalEpisode int64) int64 {
+ if progress == nil {
+ return 0
+ }
+ watchedCount := progress.WatchedCount
+ if watchedCount <= 0 && len(progress.WatchedEpisodes) > 0 {
+ watchedCount = int64(len(progress.WatchedEpisodes))
+ }
+ if watchedCount < 0 {
+ return 0
+ }
+ if totalEpisode > 0 && watchedCount > totalEpisode {
+ return totalEpisode
+ }
+ return watchedCount
+}
+
+// collectUnwatchedCount 计算未看集数,避免历史脏数据导致负数。
+func collectUnwatchedCount(totalEpisode int64, watchedCount int64) int64 {
+ if totalEpisode <= watchedCount {
+ return 0
+ }
+ return totalEpisode - watchedCount
+}
+
+// isPlayletFinished 判断短剧是否已看完。
+func isPlayletFinished(totalEpisode int64, watchedCount int64) bool {
+ return totalEpisode > 0 && watchedCount >= totalEpisode
+}
+
+// collectLastChapterId 获取最近观看剧集ID,无有效进度时返回0。
+func collectLastChapterId(progress *playletwatchprogressmod.PlayletWatchProgress) int64 {
+ if progress == nil || progress.LastChapterId < 0 {
+ return 0
+ }
+ return progress.LastChapterId
+}
+
+// collectLastEpisode 获取最近观看集数,无有效进度时返回0。
+func collectLastEpisode(progress *playletwatchprogressmod.PlayletWatchProgress) int64 {
+ if progress == nil || progress.LastEpisode < 0 {
+ return 0
+ }
+ return progress.LastEpisode
+}
+
+// findPlayletChapterUserOperateStatus 查询当前用户短剧剧集点赞和收藏状态
+func findPlayletChapterUserOperateStatus(ctx context.Context, userId int64, chapterId int64) (int64, int64, *srvdef.SrvErr) {
+ // 游客或无有效token时没有用户ID,直接返回未点赞、未收藏,保证剧集详情接口可匿名访问。
+ if userId <= 0 {
+ return operateStatusNo, operateStatusNo, nil
+ }
+ likeList, err := playletchapterlikemod.FindManyPlayletChapterLike(ctx, bson.M{
+ playletchapterlikemod.FN.PlayletChapterLike.UserId: userId,
+ playletchapterlikemod.FN.PlayletChapterLike.ChapterId: chapterId,
+ }, options.Find().
+ SetLimit(1).
+ SetProjection(bson.D{{Key: playletchapterlikemod.FN.PlayletChapterLike.Id, Value: 1}}))
+ if err != nil {
+ log.Error("查询短剧剧集用户点赞状态失败", log.Any("用户ID", userId), log.Any("剧集ID", chapterId), log.E(err))
+ return operateStatusNo, operateStatusNo, err
+ }
+ collectList, err := playletchaptercollectmod.FindManyPlayletChapterCollect(ctx, bson.M{
+ playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
+ playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: chapterId,
+ }, options.Find().
+ SetLimit(1).
+ SetProjection(bson.D{{Key: playletchaptercollectmod.FN.PlayletChapterCollect.Id, Value: 1}}))
+ if err != nil {
+ log.Error("查询短剧剧集用户收藏状态失败", log.Any("用户ID", userId), log.Any("剧集ID", chapterId), log.E(err))
+ return operateStatusNo, operateStatusNo, err
+ }
+ isLike := operateStatusByFlag(len(likeList) > 0)
+ isCollect := operateStatusByFlag(len(collectList) > 0)
+ return isLike, isCollect, nil
+}
+
+// incrementPlayletBrowse 原子增加短剧观看量和热度值
+func incrementPlayletBrowse(ctx context.Context, id int64) *srvdef.SrvErr {
+ // 不更新updatedAt,避免详情访问影响今日更新排序。
+ return playletmod.UpdateOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: id,
+ }), bson.M{
+ "$inc": bson.M{
+ playletmod.FN.Playlet.CountBrowse: 1,
+ playletmod.FN.Playlet.HeatValue: playletHeatClickInc,
+ },
+ })
+}
+
+func incrementPlayletHeatValue(ctx context.Context, id int64, delta int64) *srvdef.SrvErr {
+ if id <= 0 || delta == 0 {
+ return nil
+ }
+ return playletmod.UpdateOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: id,
+ }, bson.M{
+ "$inc": bson.M{
+ playletmod.FN.Playlet.HeatValue: delta,
+ },
+ })
+}
+
+// checkPlayletForwardHeatLimit 校验同一用户或IP对同一短剧的转发加热频控
+func checkPlayletForwardHeatLimit(userId int64, ip string, playletId int64) (bool, error) {
+ operator := strings.TrimSpace(ip)
+ if userId > 0 {
+ operator = "user-" + strconv.FormatInt(userId, 10)
+ }
+ if operator == "" {
+ operator = "unknown"
+ }
+ ok, err := redis.Setnx_NewOK(
+ redisconst.AppPlayletForwardHeatKey(operator, playletId),
+ 1,
+ redisconst.AppPlayletForwardHeatExpire,
+ )
+ if err != nil {
+ return false, err
+ }
+ return !ok, nil
+}
+
+// findPlayletForwardHeatTarget 查询转发加热目标短剧,用于频控命中时返回当前热度
+func findPlayletForwardHeatTarget(ctx context.Context, id int64) (*playletmod.Playlet, *srvdef.SrvErr) {
+ playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: id,
+ }), options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询短剧转发热度目标失败", log.Any("短剧ID", id), log.E(err))
+ return nil, err
+ }
+ return playlet, nil
+}
+
+// incrementPlayletHeatValueAndReturn 原子增加短剧热度值并返回更新后的热度
+func incrementPlayletHeatValueAndReturn(ctx context.Context, id int64, delta int64) (*playletmod.Playlet, *srvdef.SrvErr) {
+ if id <= 0 || delta == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
+ }
+ return playletmod.FindOneAndUpdatePlaylet(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: id,
+ }), bson.M{
+ "$inc": bson.M{
+ playletmod.FN.Playlet.HeatValue: delta,
+ },
+ }, options.FindOneAndUpdate().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ }))
+}
+
+// incrementPlayletChapterBrowse 原子增加短剧剧集观看量
+func incrementPlayletChapterBrowse(ctx context.Context, id int64) *srvdef.SrvErr {
+ // 只递增剧集countBrowse,不更新updatedAt,避免播放行为影响后台维护时间和排序逻辑。
+ return playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: id,
+ }, bson.M{
+ "$inc": bson.M{
+ playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
+ },
+ })
+}
+
+// findPlayletChapters 查询短剧全部章节,并批量填充当前用户播放权限。
+// 原创短剧只返回已有水印地址的剧集:首次未处理好的不露出,换水印期间仍可播旧地址。
+func findPlayletChapters(ctx context.Context, playletId int64, userId int64, isOriginal bool) ([]proto.PlayletChapterData, *srvdef.SrvErr) {
+ filter := bson.M{
+ playletchaptermod.FN.PlayletChapter.PlayletId: playletId,
+ }
+ if isOriginal {
+ filter[playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl] = bson.M{"$gt": ""}
+ }
+ // 详情页播放区需要展示完整选集列表,这里按剧集序号一次性查询全部章节,避免前端再发一次章节请求
+ list, err := playletchaptermod.FindManyPlayletChapter(ctx, filter, options.Find().
+ SetProjection(playletChapterInfoProjection).
+ SetSort(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ }))
+ if err != nil {
+ log.Error("查询短剧章节列表失败", log.Any("短剧ID", playletId), log.E(err))
+ return nil, err
+ }
+ access, err := buildPlayletChapterAccessContext(ctx, userId, playletId, list)
+ if err != nil {
+ return nil, err
+ }
+ result := make([]proto.PlayletChapterData, 0, len(list))
+ for _, chapter := range list {
+ if chapter == nil {
+ continue
+ }
+ result = append(result, playletChapterToData(chapter, access))
+ }
+ return result, nil
+}
+
+// buildPlayletChapterAccessContext 批量查询详情章节列表所需的用户权限数据。
+func buildPlayletChapterAccessContext(ctx context.Context, userId int64, playletId int64, chapters playletchaptermod.PlayletChapterList) (*playletChapterAccessContext, *srvdef.SrvErr) {
+ access := &playletChapterAccessContext{
+ unlockedChapterIds: make(map[int64]bool),
+ }
+ if userId <= 0 {
+ return access, nil
+ }
+ user, err := findPlayletChapterAccessUser(ctx, userId)
+ if err != nil {
+ return nil, err
+ }
+ access.balance = user.Recharge
+ access.vipValid = isUserVipValid(user)
+
+ coinChapterIds := collectCoinPayChapterIds(chapters)
+ if len(coinChapterIds) == 0 {
+ return access, nil
+ }
+ unlocks, err := playletchapterunlockmod.FindManyPlayletChapterUnlock(ctx, bson.M{
+ playletchapterunlockmod.FN.PlayletChapterUnlock.UserId: userId,
+ playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId: playletId,
+ playletchapterunlockmod.FN.PlayletChapterUnlock.ChapterId: bson.M{"$in": coinChapterIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletchapterunlockmod.FN.PlayletChapterUnlock.ChapterId, Value: 1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range unlocks {
+ if item == nil || item.ChapterId <= 0 {
+ continue
+ }
+ access.unlockedChapterIds[item.ChapterId] = true
+ }
+ return access, nil
+}
+
+// collectCoinPayChapterIds 收集金币付费剧集ID,减少解锁记录查询范围。
+func collectCoinPayChapterIds(chapters playletchaptermod.PlayletChapterList) []int64 {
+ ids := make([]int64, 0)
+ for _, chapter := range chapters {
+ if chapter == nil || chapter.Id <= 0 || chapter.PayType != playletchaptermod.PayTypeCoin {
+ continue
+ }
+ ids = append(ids, chapter.Id)
+ }
+ return ids
+}
+
+// findRelatedRecommendPlaylets 查询相关推荐短剧
+func findRelatedRecommendPlaylets(ctx context.Context, item *playletmod.Playlet) (playletmod.PlayletList, *srvdef.SrvErr) {
+ // 按相同题材匹配相关推荐,排除当前短剧,并按热度值倒序返回8条数据。
+ result := make(playletmod.PlayletList, 0)
+ if item == nil || len(item.ThemeIds) == 0 {
+ return result, nil
+ }
+ list, err := findHomePlaylets(ctx, homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$ne": item.Id},
+ playletmod.FN.Playlet.ThemeIds: bson.M{"$in": item.ThemeIds},
+ }), bson.D{
+ {Key: playletmod.FN.Playlet.HeatValue, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }, relatedLimit)
+ if err != nil {
+ log.Error("查询相关推荐短剧失败", log.Any("短剧ID", item.Id), log.E(err))
+ return nil, err
+ }
+ return list, nil
+}
+
+// playletChapterToData 转换短剧章节响应数据
+func playletChapterToData(item *playletchaptermod.PlayletChapter, access *playletChapterAccessContext) proto.PlayletChapterData {
+ if item == nil {
+ return proto.PlayletChapterData{}
+ }
+ videoURL := playletChapterPlayVideoUrl(item)
+ data := proto.PlayletChapterData{
+ Id: item.Id,
+ PlayletId: item.PlayletId,
+ 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,
+ PayType: int32(item.PayType),
+ SingleCoins: item.SingleCoins,
+ CostCoins: item.SingleCoins,
+ }
+ applyPlayletChapterDataWatchPermission(item, access, &data)
+ if !data.CanWatch {
+ videoURL = ""
+ }
+ data.VideoUrl = videoURL
+ return data
+}
+
+// applyPlayletChapterDataWatchPermission 填充详情页章节列表的播放权限状态。
+func applyPlayletChapterDataWatchPermission(item *playletchaptermod.PlayletChapter, access *playletChapterAccessContext, data *proto.PlayletChapterData) {
+ if item == nil || data == nil {
+ return
+ }
+ if access != nil {
+ data.Balance = access.balance
+ data.VipValid = access.vipValid
+ }
+ switch item.PayType {
+ case playletchaptermod.PayTypeFree:
+ data.CanWatch = true
+ case playletchaptermod.PayTypeCoin:
+ unlocked := access != nil && access.unlockedChapterIds[item.Id]
+ data.IsUnlocked = unlocked
+ data.CanWatch = unlocked
+ data.NeedUnlock = !unlocked
+ case playletchaptermod.PayTypeVip:
+ vipValid := access != nil && access.vipValid
+ data.CanWatch = vipValid
+ data.NeedVip = !vipValid
+ default:
+ data.NeedUnlock = true
+ }
+}
+
+// playletChapterToInfoResp 转换短剧剧集详情响应数据
+func playletChapterToInfoResp(item *playletchaptermod.PlayletChapter) *proto.PlayletChapterInfoResp {
+ // 剧集详情只返回播放页需要的当前剧集字段;播放地址优先水印地址,为空回退原片。
+ if item == nil {
+ return &proto.PlayletChapterInfoResp{}
+ }
+ return &proto.PlayletChapterInfoResp{
+ Id: item.Id,
+ PlayletId: item.PlayletId,
+ CurrentEpisode: item.CurrentEpisode,
+ Title: item.Title,
+ Md5: item.Md5,
+ Hash: item.Hash,
+ Height: item.Height,
+ Width: item.Width,
+ Ratio: item.Ratio,
+ VideoUrl: playletChapterPlayVideoUrl(item),
+ MediaSize: item.MediaSize,
+ MediaTime: item.MediaTime,
+ CountComment: item.CountComment,
+ CountBrowse: item.CountBrowse,
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ IsLike: operateStatusNo,
+ IsCollect: operateStatusNo,
+ PayType: int32(item.PayType),
+ SingleCoins: item.SingleCoins,
+ }
+}
+
+// ensureOriginalChapterWatermarkReady 原创剧集必须已有水印地址才允许播放/解锁。
+func ensureOriginalChapterWatermarkReady(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) *srvdef.SrvErr {
+ if playlet == nil || !playlet.IsOriginal || chapter == nil {
+ return nil
+ }
+ if strings.TrimSpace(chapter.WatermarkVideoUrl) != "" {
+ return nil
+ }
+ return srvdef.New(srvdef.ErrNoData, "剧集处理中,请稍后观看", "剧集处理中,请稍后观看")
+}
+
+// playletChapterPlayVideoUrl 优先返回水印地址,为空时回退原片地址。
+func playletChapterPlayVideoUrl(item *playletchaptermod.PlayletChapter) string {
+ if item == nil {
+ return ""
+ }
+ if u := strings.TrimSpace(item.WatermarkVideoUrl); u != "" {
+ return u
+ }
+ return item.VideoUrl
+}
+
+// buildPlayletDetailSeoInfo 组装短剧详情页SEO信息
+func buildPlayletDetailSeoInfo(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string) *proto.SeoInfo {
+ seo := configser.GetSiteSeoInfo()
+ if item == nil {
+ return &seo
+ }
+ // 优先使用后台配置的 SeoInfo,缺失字段再回退剧名/简介自动生成。
+ if title := strings.TrimSpace(item.SeoInfo.Title); title != "" {
+ seo.Title = title
+ } else if title := strings.TrimSpace(item.Title); title != "" {
+ seo.Title = title
+ }
+ if desc := strings.TrimSpace(item.Description); desc != "" {
+ seo.Description = desc
+ } else if desc := strings.TrimSpace(item.SeoInfo.Description); desc != "" {
+ seo.Description = desc
+ } else if desc := truncatePlayletSEODescription(strings.TrimSpace(item.Desc), 120); desc != "" {
+ seo.Description = desc
+ }
+ if len(item.SeoInfo.Keyworld) > 0 {
+ seo.Keywords = append([]string{}, item.SeoInfo.Keyworld...)
+ } else {
+ seo.Keywords = buildPlayletSEOKeywords(item, themeNameMap, tagNameMap, seo.Keywords)
+ }
+ seo.CanonicalURL = buildPlayletDetailCanonicalURL(seo.CanonicalURL, item.Slug, item.Id)
+ return &seo
+}
+
+// buildPlayletChapterDetailSeoInfo 组装短剧剧集详情页SEO信息
+func buildPlayletChapterDetailSeoInfo(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) *proto.SeoInfo {
+ seo := configser.GetSiteSeoInfo()
+ if playlet == nil && chapter == nil {
+ return &seo
+ }
+ // 优先使用短剧后台配置的 SeoInfo,再回退到剧名/简介自动生成。
+ if playlet != nil {
+ if title := strings.TrimSpace(playlet.SeoInfo.Title); title != "" {
+ seo.Title = title
+ }
+ if desc := strings.TrimSpace(playlet.Description); desc != "" {
+ seo.Description = desc
+ } else if desc := strings.TrimSpace(playlet.SeoInfo.Description); desc != "" {
+ seo.Description = desc
+ }
+ if len(playlet.SeoInfo.Keyworld) > 0 {
+ seo.Keywords = append([]string{}, playlet.SeoInfo.Keyworld...)
+ }
+ }
+ if strings.TrimSpace(seo.Title) == "" {
+ if title := buildPlayletChapterSEOTitle(playlet, chapter); title != "" {
+ seo.Title = title
+ }
+ } else if chapter != nil {
+ // 已有短剧 SEO 标题时,补充剧集名便于播放页区分。
+ if episodeTitle := strings.TrimSpace(chapter.Title); episodeTitle != "" {
+ seo.Title = seo.Title + " " + episodeTitle
+ } else if chapter.CurrentEpisode > 0 {
+ seo.Title = seo.Title + " 第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集"
+ }
+ }
+ if strings.TrimSpace(seo.Description) == "" && playlet != nil {
+ if desc := truncatePlayletSEODescription(strings.TrimSpace(playlet.Desc), 120); desc != "" {
+ seo.Description = desc
+ }
+ }
+ if len(seo.Keywords) == 0 {
+ if playlet != nil {
+ seo.Keywords = buildPlayletSEOKeywords(playlet, nil, nil, seo.Keywords)
+ } else {
+ seo.Keywords = buildPlayletChapterSEOKeywords(chapter, seo.Keywords)
+ }
+ }
+ if chapter != nil {
+ slug := ""
+ if playlet != nil {
+ slug = playlet.Slug
+ }
+ seo.CanonicalURL = buildPlayletChapterDetailCanonicalURL(seo.CanonicalURL, slug, chapter.PlayletId, chapter.Id)
+ }
+ return &seo
+}
+
+// buildPlayletListPageSeoInfo 组装短剧总列表页 SEO(无题材/分类筛选)。
+func buildPlayletListPageSeoInfo() *proto.SeoInfo {
+ return buildPageSeoInfoFromValue(pubdef.SeoValue{}, "", "", "/list")
+}
+
+// buildPlayletThemePageSeoInfo 组装题材短剧列表页SEO信息(读题材缓存,避免每次查库)
+func buildPlayletThemePageSeoInfo(themeId int64) *proto.SeoInfo {
+ if themeId <= 0 {
+ return buildPlayletListPageSeoInfo()
+ }
+ theme := playletcache.GetEnabledTheme(themeId)
+ if theme == nil {
+ return buildPlayletListPageSeoInfo()
+ }
+ return buildPageSeoInfoFromValue(theme.SeoInfo, theme.Name, "", buildPlayletThemeListCanonicalURL(theme.Slug, theme.Id))
+}
+
+// buildPlayletTagPageSeoInfo 组装标签短剧列表页SEO信息(读标签缓存,避免每次查库)
+func buildPlayletTagPageSeoInfo(tagId int64) *proto.SeoInfo {
+ if tagId <= 0 {
+ return buildPlayletListPageSeoInfo()
+ }
+ tag := playletcache.GetEnabledTag(tagId)
+ if tag == nil {
+ return buildPageSeoInfoFromValue(pubdef.SeoValue{}, "短剧标签", "", buildPlayletTagListCanonicalURL("", tagId))
+ }
+ return buildPageSeoInfoFromValue(tag.SeoInfo, tag.Name, "", buildPlayletTagListCanonicalURL(tag.Slug, tag.Id))
+}
+
+// buildPlayletCategoryPageSeoInfo 组装分类短剧列表页SEO信息(读分类缓存,避免每次查库)
+func buildPlayletCategoryPageSeoInfo(categoryId int64, fallbackName string) *proto.SeoInfo {
+ if categoryId <= 0 {
+ return buildPlayletListPageSeoInfo()
+ }
+ category := playletcache.GetEnabledCategory(categoryId)
+ canonical := buildPlayletCategoryListCanonicalURL("", categoryId)
+ if category != nil {
+ canonical = buildPlayletCategoryListCanonicalURL(category.Slug, category.Id)
+ }
+ if category == nil {
+ return buildPageSeoInfoFromValue(pubdef.SeoValue{}, fallbackName, "", canonical)
+ }
+ name := strings.TrimSpace(category.Name)
+ if name == "" {
+ name = fallbackName
+ }
+ return buildPageSeoInfoFromValue(category.SeoInfo, name, "", canonical)
+}
+
+// buildPageSeoInfoFromValue 将后台 SeoValue 转为页面 SeoInfo,缺失字段回退站点默认。
+func buildPageSeoInfoFromValue(custom pubdef.SeoValue, fallbackTitle, fallbackDesc, canonicalURL string) *proto.SeoInfo {
+ seo := configser.GetSiteSeoInfo()
+ if title := strings.TrimSpace(custom.Title); title != "" {
+ seo.Title = title
+ } else if title := strings.TrimSpace(fallbackTitle); title != "" {
+ seo.Title = title
+ }
+ if desc := strings.TrimSpace(custom.Description); desc != "" {
+ seo.Description = desc
+ } else if desc := strings.TrimSpace(fallbackDesc); desc != "" {
+ seo.Description = desc
+ }
+ if len(custom.Keyworld) > 0 {
+ seo.Keywords = append([]string{}, custom.Keyworld...)
+ }
+ if canonicalURL != "" {
+ seo.CanonicalURL = buildPlayletCanonicalURL(seo.CanonicalURL, canonicalURL)
+ }
+ return &seo
+}
+
+// buildPlayletThemeListCanonicalURL 专题详情页相对路径(对齐 jha-1043-hgdj-web /topic/:slug/)
+func buildPlayletThemeListCanonicalURL(slug string, themeId int64) string {
+ return playletSiteEntityPath("/topic", slug, themeId)
+}
+
+// buildPlayletTagListCanonicalURL 标签列表页相对路径(对齐 jha-1043-hgdj-web /tag/:slug/)
+func buildPlayletTagListCanonicalURL(slug string, tagId int64) string {
+ return playletSiteEntityPath("/tag", slug, tagId)
+}
+
+// buildPlayletCategoryListCanonicalURL 分类列表页相对路径(对齐 jha-1043-hgdj-web /category/:slug/)
+func buildPlayletCategoryListCanonicalURL(slug string, categoryId int64) string {
+ return playletSiteEntityPath("/category", slug, categoryId)
+}
+
+func playletSiteEntityPath(prefix, slug string, id int64) string {
+ if segment := utils.PathSegment(slug, id); segment != "" {
+ return strings.TrimSuffix(prefix, "/") + "/" + segment + "/"
+ }
+ if strings.HasSuffix(prefix, "/") {
+ return prefix
+ }
+ return prefix + "/"
+}
+
+// buildPlayletChapterSEOTitle 生成剧集详情页标题,优先展示“短剧名 + 剧集名”。
+func buildPlayletChapterSEOTitle(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) string {
+ playletTitle := ""
+ if playlet != nil {
+ playletTitle = strings.TrimSpace(playlet.Title)
+ }
+ chapterTitle := ""
+ if chapter != nil {
+ chapterTitle = strings.TrimSpace(chapter.Title)
+ if chapterTitle == "" && chapter.CurrentEpisode > 0 {
+ chapterTitle = "第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集"
+ }
+ }
+ if playletTitle == "" {
+ return chapterTitle
+ }
+ if chapterTitle == "" {
+ return playletTitle
+ }
+ return playletTitle + " " + chapterTitle
+}
+
+// buildPlayletSEOKeywords 合并短剧标题、题材、标签和全站关键词,并去重。
+func buildPlayletSEOKeywords(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string, siteKeywords []string) []string {
+ capacity := len(siteKeywords) + 1
+ if item != nil {
+ capacity += len(item.ThemeIds) + len(item.TagIds)
+ }
+ keywords := make([]string, 0, capacity)
+ seen := make(map[string]struct{}, cap(keywords))
+ add := func(value string) {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ return
+ }
+ if _, ok := seen[value]; ok {
+ return
+ }
+ seen[value] = struct{}{}
+ keywords = append(keywords, value)
+ }
+ if item != nil {
+ add(item.Title)
+ for _, name := range namesByIds(item.ThemeIds, themeNameMap) {
+ add(name)
+ }
+ for _, name := range namesByIds(item.TagIds, tagNameMap) {
+ add(name)
+ }
+ }
+ for _, keyword := range siteKeywords {
+ add(keyword)
+ }
+ return keywords
+}
+
+// buildPlayletChapterSEOKeywords 合并剧集标题和全站关键词,并去重。
+func buildPlayletChapterSEOKeywords(chapter *playletchaptermod.PlayletChapter, siteKeywords []string) []string {
+ keywords := make([]string, 0, len(siteKeywords)+1)
+ seen := make(map[string]struct{}, cap(keywords))
+ add := func(value string) {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ return
+ }
+ if _, ok := seen[value]; ok {
+ return
+ }
+ seen[value] = struct{}{}
+ keywords = append(keywords, value)
+ }
+ if chapter != nil {
+ add(chapter.Title)
+ }
+ for _, keyword := range siteKeywords {
+ add(keyword)
+ }
+ return keywords
+}
+
+// truncatePlayletSEODescription 限制meta description长度,避免详情长文本直接写进SEO描述。
+func truncatePlayletSEODescription(desc string, maxRunes int) string {
+ if maxRunes <= 0 || desc == "" {
+ return ""
+ }
+ runes := []rune(desc)
+ if len(runes) <= maxRunes {
+ return desc
+ }
+ return string(runes[:maxRunes])
+}
+
+// buildPlayletDetailCanonicalURL 拼接短剧详情页标准URL。
+func buildPlayletDetailCanonicalURL(baseURL, slug string, id int64) string {
+ return buildPlayletCanonicalURL(baseURL, playletSiteEntityPath(playletH5DetailPath, slug, id))
+}
+
+// buildPlayletChapterDetailCanonicalURL 拼接短剧剧集详情页标准URL。
+func buildPlayletChapterDetailCanonicalURL(baseURL, slug string, playletId int64, chapterId int64) string {
+ pagePath := playletSiteEntityPath(playletChapterH5DetailPath, slug, playletId)
+ if chapterId > 0 {
+ pagePath += strconv.FormatInt(chapterId, 10) + "/"
+ }
+ return buildPlayletCanonicalURL(baseURL, pagePath)
+}
+
+// buildPlayletCanonicalURL 使用全站Web地址拼接页面路径;未配置域名时返回相对路径。
+func buildPlayletCanonicalURL(baseURL string, pagePath string) string {
+ baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
+ if baseURL == "" {
+ return pagePath
+ }
+ return baseURL + pagePath
+}
+
+// emptyPlayletSearchResp 构建短剧ES空分页响应
+func emptyPlayletSearchResp(pageNum int64, pageSize int64) *proto.PlayletSearchResp {
+ // ES索引未创建或暂无数据时返回空列表,保持接口响应结构稳定。
+ return &proto.PlayletSearchResp{
+ TotalCount: 0,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]proto.CommonData, 0),
+ }
+}
+
+// buildHomePlayletResponse 查询并组装app首页短剧数据
+func buildHomePlayletResponse(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
+ // 定义返回
+ resp := &proto.HomePlayletResponse{
+ HomeBannerData: make([]*proto.HomeBannerData, 0),
+ HomeTodayUpdateData: make([]*proto.CommonData, 0),
+ HomeCategoryData: make([]*proto.HomeCategoryData, 0),
+ HomeRecommendThemes: make([]*proto.HomeRecommendTheme, 0),
+ Announcements: make([]*proto.AnnouncementItem, 0),
+ }
+ // Home首页数据响应
+ var bannerList homebannermod.HomeBannerList
+ var updateList playletmod.PlayletList
+ categoryPlayletMap := make(map[int64]playletmod.PlayletList)
+ var queryErr *srvdef.SrvErr
+ categories, err := findHomeCategories(ctx)
+ if err != nil {
+ log.Error("查询首页短剧分类失败", log.E(err))
+ return nil, err
+ }
+ var mu sync.Mutex
+ var wg sync.WaitGroup
+ runQuery := func(name string, target *playletmod.PlayletList, filter bson.M, sort bson.D, limit int64) {
+ defer wg.Done()
+ list, err := findHomePlaylets(ctx, filter, sort, limit)
+ if err != nil {
+ log.Error("查询首页短剧数据失败", log.Any("分区", name), log.E(err))
+ mu.Lock()
+ if queryErr == nil {
+ queryErr = err
+ }
+ mu.Unlock()
+ return
+ }
+ mu.Lock()
+ *target = list
+ mu.Unlock()
+ }
+ runCategoryQuery := func(category *playletcategorymod.PlayletCategory) {
+ defer wg.Done()
+ if category == nil {
+ return
+ }
+ filter, sort, qerr := playletRecommendListQuery(ctx, category.Id)
+ if qerr != nil {
+ log.Error("构建首页分类短剧查询失败", log.Any("分类ID", category.Id), log.Any("分类名称", category.Name), log.E(qerr))
+ mu.Lock()
+ if queryErr == nil {
+ queryErr = qerr
+ }
+ mu.Unlock()
+ return
+ }
+ var list playletmod.PlayletList
+ var err *srvdef.SrvErr
+ if _, ok := filter[playletmod.FN.Playlet.CategoryIds]; ok {
+ list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, category.Id, 0, homeSectionLimit, playletmod.CategoryRankSort(), playletHomeProjection)
+ } else {
+ list, err = findHomePlaylets(ctx, filter, sort, homeSectionLimit)
+ }
+ if err != nil {
+ log.Error("查询首页分类短剧数据失败", log.Any("分类ID", category.Id), log.Any("分类名称", category.Name), log.E(err))
+ mu.Lock()
+ if queryErr == nil {
+ queryErr = err
+ }
+ mu.Unlock()
+ return
+ }
+ mu.Lock()
+ categoryPlayletMap[category.Id] = list
+ mu.Unlock()
+ }
+ runBannerQuery := func() {
+ defer wg.Done()
+ list, err := findHomeBanners(ctx)
+ if err != nil {
+ log.Error("查询首页banner数据失败", log.E(err))
+ mu.Lock()
+ if queryErr == nil {
+ queryErr = err
+ }
+ mu.Unlock()
+ return
+ }
+ mu.Lock()
+ bannerList = list
+ mu.Unlock()
+ }
+ wg.Add(2 + len(categories))
+ go runBannerQuery()
+ // 最近更新:只展示已上架短剧,按上架时间从近到远排序。
+ go runQuery("最近更新", &updateList, homeUpPlayletFilter(nil), bson.D{
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }, homeSectionLimit)
+ for _, category := range categories {
+ go runCategoryQuery(category)
+ }
+ wg.Wait()
+ if queryErr != nil {
+ return nil, queryErr
+ }
+ allPlaylets := mergeHomePlayletLists(updateList, mergeHomeCategoryPlayletLists(categoryPlayletMap))
+ themeNameMap, err := buildHomeThemeNameMap(ctx, allPlaylets)
+ if err != nil {
+ log.Error("查询首页短剧题材名称失败", log.E(err))
+ return nil, err
+ }
+ tagNameMap, err := buildHomeTagNameMap(ctx, allPlaylets)
+ if err != nil {
+ log.Error("查询首页短剧标签名称失败", log.E(err))
+ return nil, err
+ }
+ resp.HomeBannerData = homeBannersToHomeBannerData(ctx, bannerList)
+ resp.HomeTodayUpdateData = playletsToHomeCommonData(ctx, updateList, themeNameMap, tagNameMap)
+ resp.HomeCategoryData = playletCategoriesToHomeData(ctx, categories, categoryPlayletMap, themeNameMap, tagNameMap)
+ seo := configser.GetSiteSeoInfo()
+ resp.SeoInfo = &seo
+ return resp, nil
+}
+
+func attachHomeAnnouncements(resp *proto.HomePlayletResponse) {
+ if resp == nil {
+ return
+ }
+ resp.Announcements = configser.GetAnnouncements()
+}
+
+func attachHomeRecommendThemes(ctx context.Context, resp *proto.HomePlayletResponse) {
+ if resp == nil {
+ return
+ }
+ list := playletcache.ListRecommendedThemes(homeRecommendThemeLimit)
+ items := make([]*proto.HomeRecommendTheme, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.HomeRecommendTheme{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Icon: withImageDomain(ctx, item.Icon),
+ Rank: item.Rank,
+ PlayletCount: item.PlayletCount,
+ })
+ }
+ resp.HomeRecommendThemes = items
+}
+
+func guessLikeCommonData(ctx context.Context) []*proto.CommonData {
+ list := playletcache.ListGuessLike()
+ themeNameMap, _ := buildHomeThemeNameMap(ctx, list)
+ tagNameMap, _ := buildHomeTagNameMap(ctx, list)
+ return playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap)
+}
+
+// findHomeCategories 查询首页展示的启用短剧分类,按分类 rank 倒序排序。
+func findHomeCategories(ctx context.Context) (playletcategorymod.PlayletCategoryList, *srvdef.SrvErr) {
+ return playletcategorymod.FindManyPlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Status: true,
+ }, options.Find().
+ SetProjection(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.Desc, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Icon, Value: 1},
+ {Key: playletcategorymod.FN.PlayletCategory.Rank, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: playletcategorymod.FN.PlayletCategory.Rank, Value: -1},
+ {Key: playletcategorymod.FN.PlayletCategory.Id, Value: -1},
+ }))
+}
+
+// findHomeBanners 查询启用的首页 banner
+func findHomeBanners(ctx context.Context) (homebannermod.HomeBannerList, *srvdef.SrvErr) {
+ return 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(homeBannerLimit))
+}
+
+// homeCategoryPlayletFilter 构建首页分类分区短剧过滤条件。
+func homeCategoryPlayletFilter(categoryId int64) bson.M {
+ return homeUpPlayletFilter(bson.M{
+ playletmod.FN.Playlet.CategoryIds: categoryId,
+ })
+}
+
+// homeUpPlayletFilter 构建首页短剧上架过滤条件
+func homeUpPlayletFilter(extra bson.M) bson.M {
+ filter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
+ for key, value := range extra {
+ filter[key] = value
+ }
+ return filter
+}
+
+// homePlayletCacheKey 获取app首页短剧缓存key
+func homePlayletCacheKey() string {
+ return redisconst.AppPlayletHomeCacheDataKey(homePlayletCacheVersion() + ":" + homePlayletCacheSchema)
+}
+
+// playletRecommendListCacheKey 获取app分类短剧列表缓存key
+func playletRecommendListCacheKey(categoryId int64, pageNum int64, pageSize int64) string {
+ return redisconst.AppPlayletRecommendListCacheKey(homePlayletCacheVersion(), categoryId, pageNum, pageSize)
+}
+
+// homePlayletCacheVersion 获取app首页短剧缓存版本
+func homePlayletCacheVersion() string {
+ if redis.Client() == nil {
+ return "0"
+ }
+ value, err := redis.Get(redisconst.AppPlayletHomeCacheVersionKey())
+ if err != nil {
+ log.Warn("读取app首页短剧缓存版本失败", log.E(err))
+ return "0"
+ }
+ if value == nil || strings.TrimSpace(*value) == "" {
+ return "0"
+ }
+ return strings.TrimSpace(*value)
+}
+
+// getHomePlayletCache 读取app首页短剧缓存
+func getHomePlayletCache(cacheKey string) *proto.HomePlayletResponse {
+ if redis.Client() == nil || cacheKey == "" {
+ return nil
+ }
+ value, err := redis.Get(cacheKey)
+ if err != nil {
+ log.Warn("读取app首页短剧缓存失败", 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("解析app首页短剧缓存失败", log.E(err))
+ return nil
+ }
+ return &resp
+}
+
+// setHomePlayletCache 写入app首页短剧缓存
+func setHomePlayletCache(cacheKey string, resp *proto.HomePlayletResponse) {
+ if redis.Client() == nil || cacheKey == "" || resp == nil {
+ return
+ }
+ data, err := json.Marshal(resp)
+ if err != nil {
+ log.Warn("序列化app首页短剧缓存失败", log.E(err))
+ return
+ }
+ if err = redis.Set(cacheKey, data, redisconst.AppPlayletHomeCacheExpire); err != nil {
+ log.Warn("写入app首页短剧缓存失败", log.E(err))
+ }
+}
+
+// getPlayletRecommendListCache 读取app分类短剧列表缓存
+func getPlayletRecommendListCache(cacheKey string) *proto.PlayletRecommendListResp {
+ if redis.Client() == nil || cacheKey == "" {
+ return nil
+ }
+ value, err := redis.Get(cacheKey)
+ if err != nil {
+ log.Warn("读取app分类短剧列表缓存失败", log.E(err))
+ return nil
+ }
+ if value == nil || *value == "" {
+ return nil
+ }
+ var resp proto.PlayletRecommendListResp
+ if err = json.Unmarshal([]byte(*value), &resp); err != nil {
+ log.Warn("解析app分类短剧列表缓存失败", log.E(err))
+ return nil
+ }
+ return &resp
+}
+
+// setPlayletRecommendListCache 写入app分类短剧列表缓存
+func setPlayletRecommendListCache(cacheKey string, resp *proto.PlayletRecommendListResp) {
+ if redis.Client() == nil || cacheKey == "" || resp == nil || len(resp.List) == 0 {
+ return
+ }
+ data, err := json.Marshal(resp)
+ if err != nil {
+ log.Warn("序列化app分类短剧列表缓存失败", log.E(err))
+ return
+ }
+ if err = redis.Set(cacheKey, data, redisconst.AppPlayletRecommendListExpire); err != nil {
+ log.Warn("写入app分类短剧列表缓存失败", log.E(err))
+ }
+}
+
+// findHomePlaylets 查询首页分区或列表短剧数据
+func findHomePlaylets(ctx context.Context, filter bson.M, sort bson.D, limit int64, extraOpts ...*options.FindOptions) (playletmod.PlayletList, *srvdef.SrvErr) {
+ opt := options.Find().
+ SetProjection(playletHomeProjection).
+ SetSort(sort).
+ SetLimit(limit)
+ for _, extra := range extraOpts {
+ if extra == nil {
+ continue
+ }
+ if extra.Skip != nil {
+ opt.SetSkip(*extra.Skip)
+ }
+ }
+ return playletmod.FindManyPlaylet(ctx, filter, opt)
+}
+
+// mergeHomePlayletLists 合并首页各分区短剧,便于后续批量查询题材和标签名称
+func mergeHomePlayletLists(lists ...playletmod.PlayletList) playletmod.PlayletList {
+ total := 0
+ for _, list := range lists {
+ total += len(list)
+ }
+ result := make(playletmod.PlayletList, 0, total)
+ for _, list := range lists {
+ result = append(result, list...)
+ }
+ return result
+}
+
+// mergeHomeCategoryPlayletLists 合并首页所有分类短剧列表。
+func mergeHomeCategoryPlayletLists(categoryPlayletMap map[int64]playletmod.PlayletList) playletmod.PlayletList {
+ total := 0
+ for _, list := range categoryPlayletMap {
+ total += len(list)
+ }
+ result := make(playletmod.PlayletList, 0, total)
+ for _, list := range categoryPlayletMap {
+ result = append(result, list...)
+ }
+ return result
+}
+
+// playletCategoriesToHomeData 转换首页分类短剧响应数据。
+func playletCategoriesToHomeData(ctx context.Context, categories playletcategorymod.PlayletCategoryList, categoryPlayletMap map[int64]playletmod.PlayletList, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.HomeCategoryData {
+ result := make([]*proto.HomeCategoryData, 0, len(categories))
+ for _, category := range categories {
+ if category == nil {
+ continue
+ }
+ result = append(result, &proto.HomeCategoryData{
+ Id: category.Id,
+ Name: category.Name,
+ Slug: category.Slug,
+ Desc: category.Desc,
+ Icon: category.Icon,
+ Rank: category.Rank,
+ List: playletsToHomeCommonData(ctx, categoryPlayletMap[category.Id], themeNameMap, tagNameMap),
+ })
+ }
+ return result
+}
+
+// buildHomeThemeNameMap 从缓存批量取首页短剧题材名称
+func buildHomeThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ themeIds := collectHomeIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.ThemeIds
+ })
+ return playletcache.ThemeNameMap(themeIds), nil
+}
+
+// buildHomeTagNameMap 从缓存批量取首页短剧标签名称
+func buildHomeTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ tagIds := collectHomeIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.TagIds
+ })
+ return playletcache.TagNameMap(tagIds), nil
+}
+
+// collectHomeIds 汇总并去重首页短剧的题材或标签ID
+func collectHomeIds(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
+}
+
+// homeBannersToHomeBannerData 转换首页banner响应数据
+func homeBannersToHomeBannerData(ctx context.Context, list homebannermod.HomeBannerList) []*proto.HomeBannerData {
+ result := make([]*proto.HomeBannerData, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, &proto.HomeBannerData{
+ Id: item.Id,
+ Name: item.Name,
+ Image: withImageDomain(ctx, item.Image),
+ VerticalImage: withImageDomain(ctx, item.VerticalImage),
+ Url: item.Url,
+ Rank: item.Rank,
+ Status: item.Status,
+ })
+ }
+ return result
+}
+
+// playletsToHomeCommonData 转换首页通用短剧响应数据
+func playletsToHomeCommonData(ctx context.Context, list playletmod.PlayletList, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.CommonData {
+ result := make([]*proto.CommonData, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if len(item.Description) > 0 {
+ item.SeoInfo.Description = item.Description
+ }
+ result = append(result, &proto.CommonData{
+ Id: item.Id,
+ Status: int32(item.Status),
+ IsFeatured: item.IsFeatured,
+ IsRecommend: item.IsRecommend,
+ IsNew: isOnlineThisWeek(item.OnlineAt),
+ OperateType: int32(playletmod.NormalizeOperateType(item.OperateType)),
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: withImageDomain(ctx, item.Banner),
+ OnlineAt: item.OnlineAt,
+ MediaCreatedAt: item.MediaCreatedAt,
+ MediaUpdatedAt: item.MediaUpdatedAt,
+ ThemeIds: item.ThemeIds,
+ ThemeNames: namesByIds(item.ThemeIds, themeNameMap),
+ ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
+ Title: item.Title,
+ PlayName: item.PlayName,
+ Description: item.Description,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: withImageDomain(ctx, item.DisplayCover()),
+ TagIds: item.TagIds,
+ TagNames: namesByIds(item.TagIds, tagNameMap),
+ TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.CountBrowse,
+ HeatValue: item.HeatValue,
+ CountCollect: item.CountCollect,
+ CountLike: displayPlayletCountLike(item.CountLike, item.VirtualCountLike),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: item.SeoInfo,
+ })
+ }
+ return result
+}
+
+// slugsByIds 按ID顺序返回 slug
+func slugsByIds(ids []int64, slugMap map[int64]string) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if slug := slugMap[id]; slug != "" {
+ result = append(result, slug)
+ }
+ }
+ return result
+}
+
+// namesByIds 按ID顺序返回名称
+func namesByIds(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
+}
+
+// isOnlineThisWeek 判断短剧是否为本周内上架,用于app端展示“上新”标识。
+func isOnlineThisWeek(onlineAt time.Time) bool {
+ if onlineAt.IsZero() {
+ return false
+ }
+ now := time.Now()
+ weekday := int(now.Weekday())
+ if weekday == 0 {
+ weekday = 7
+ }
+ weekStart := time.Date(now.Year(), now.Month(), now.Day()-weekday+1, 0, 0, 0, 0, now.Location())
+ return !onlineAt.Before(weekStart) && !onlineAt.After(now)
+}
+
+// commonDataValues 将短剧指针列表转换为响应值列表
+func commonDataValues(list []*proto.CommonData) []proto.CommonData {
+ result := make([]proto.CommonData, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, *item)
+ }
+ return result
+}
+
+// playletRecommendListQuery 按短剧分类构建分页查询条件。
+// 分类下没有已上架短剧时,回退为全部已上架短剧,并按上架时间倒序。
+func playletRecommendListQuery(ctx context.Context, categoryId int64) (bson.M, bson.D, *srvdef.SrvErr) {
+ if categoryId <= 0 {
+ return nil, nil, srvdef.New(srvdef.ErrParamError, "分类ID无效", "分类ID无效")
+ }
+ filter := homeCategoryPlayletFilter(categoryId)
+ count, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, nil, err
+ }
+ if count == 0 {
+ return homeUpPlayletFilter(nil), playletTodayUpdateSort(), nil
+ }
+ return filter, playletTodayRecommendSort(), nil
+}
+
+// findEnabledPlayletCategory 查询启用的短剧分类。
+func findEnabledPlayletCategory(ctx context.Context, categoryId int64) (*playletcategorymod.PlayletCategory, *srvdef.SrvErr) {
+ if categoryId <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "分类ID无效", "分类ID无效")
+ }
+ return playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: categoryId,
+ playletcategorymod.FN.PlayletCategory.Status: true,
+ }, options.FindOne().SetProjection(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.Desc, Value: 1},
+ }))
+}
+
+// playletSearchQuery 构建短剧ES搜索查询条件
+func playletSearchQuery(keyword string, from int64, size int64) esmanager.M {
+ return esmanager.M{
+ "from": from,
+ "size": size,
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {
+ "bool": esmanager.M{
+ "should": esmanager.A{
+ {
+ "multi_match": esmanager.M{
+ "query": keyword,
+ "fields": playletSearchFields(),
+ "type": "phrase",
+ "boost": 3,
+ },
+ },
+ {
+ "multi_match": esmanager.M{
+ "query": keyword,
+ "fields": playletSearchFields(),
+ "type": "best_fields",
+ "operator": "and",
+ },
+ },
+ },
+ "minimum_should_match": 1,
+ },
+ },
+ },
+ "filter": esmanager.A{
+ {
+ "term": esmanager.M{
+ esmod.FN.ESPlaylet.Status: int32(playletmod.StatusUp),
+ },
+ },
+ },
+ },
+ },
+ "sort": esmanager.A{
+ {
+ "_score": esmanager.M{"order": "desc"},
+ },
+ {
+ esmod.FN.ESPlaylet.OnlineAt: esmanager.M{"order": "desc"},
+ },
+ {
+ esmod.FN.ESPlaylet.Id: esmanager.M{"order": "desc"},
+ },
+ },
+ }
+}
+
+// playletSearchFields 返回短剧ES搜索字段和权重
+func playletSearchFields() []string {
+ // 剧名权重最高,题材名和标签名次之,简介兜底命中。
+ return []string{
+ esmod.FN.ESPlaylet.Title + "^3",
+ esmod.FN.ESPlaylet.PlayName + "^3",
+ esmod.FN.ESPlaylet.ThemeNames + "^2",
+ esmod.FN.ESPlaylet.TagNames + "^2",
+ esmod.FN.ESPlaylet.Desc,
+ }
+}
+
+// esPlayletToCommonData 转换ES短剧文档为app通用短剧响应
+func esPlayletToCommonData(ctx context.Context, item *esmod.ESPlaylet) proto.CommonData {
+ return proto.CommonData{
+ Id: item.Id,
+ Status: int32(item.Status),
+ IsFeatured: item.IsFeatured,
+ IsRecommend: item.IsRecommend,
+ OperateType: int32(playletmod.NormalizeOperateType(item.OperateType)),
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: withImageDomain(ctx, item.Banner),
+ OnlineAt: item.OnlineAt,
+ ThemeIds: item.ThemeIds,
+ ThemeNames: item.ThemeNames,
+ ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
+ Title: item.Title,
+ PlayName: item.PlayName,
+ Description: item.Description,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: withImageDomain(ctx, item.Cover),
+ TagIds: item.TagIds,
+ TagNames: item.TagNames,
+ TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.CountBrowse,
+ HeatValue: item.HeatValue,
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: item.SeoInfo,
+ }
+}
+
+// playletListSort 构建app短剧列表排序
+func playletListSort(sortOrder int64) bson.D {
+ switch sortOrder {
+ case sortOrderHot:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.CountCollect, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ case sortOrderUpdated:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ default:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ }
+}
+
+// playletRankSort 构建app短剧排行榜排序
+func playletRankSort(sortOrder int64) bson.D {
+ switch sortOrder {
+ case rankHot:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.HeatValue, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ case rankNew:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ case rankCollect:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.CountCollect, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ default:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.HeatValue, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ }
+}
+
+// playletTodayRecommendSort 构建今日推荐短剧排序
+func playletTodayRecommendSort() bson.D {
+ return bson.D{
+ {Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+}
+
+// playletHitSeriesSort 构建热门好剧短剧排序
+func playletHitSeriesSort() bson.D {
+ return bson.D{
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+}
+
+// playletTodayUpdateSort 构建今日更新短剧排序
+func playletTodayUpdateSort() bson.D {
+ return bson.D{
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+}
+
+// operateTypeFilter 构建运营类型过滤条件;历史未写入字段按短剧处理。
+func operateTypeFilter(operateType playletmod.OperateType) bson.M {
+ if operateType == playletmod.OperateTypeComic {
+ return bson.M{playletmod.FN.Playlet.OperateType: playletmod.OperateTypeComic}
+ }
+ return bson.M{playletmod.FN.Playlet.OperateType: bson.M{"$ne": playletmod.OperateTypeComic}}
+}
+
+// playletOperateListSort 构建运营类型列表排序
+func playletOperateListSort(filterType int64) bson.D {
+ switch filterType {
+ case operateFilterHot:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.CountCollect, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ case operateFilterOriginal:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ default:
+ return bson.D{
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+ }
+}
+
+// normalizePlayletPage 规范化短剧分页参数
+func normalizePlayletPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultPageSize
+ }
+ if pageSize > maxPageSize {
+ pageSize = maxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// normalizePlayletRecommendPage 规范化分类短剧分页参数
+func normalizePlayletRecommendPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultPageSize
+ }
+ if pageSize > maxPageSize {
+ pageSize = maxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// normalizePlayletHistoryPage 规范化观看历史分页参数
+func normalizePlayletHistoryPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = recommendPageSize
+ }
+ if pageSize > maxPageSize {
+ pageSize = maxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// withImageDomain 给图片相对路径拼接图片域名,海外 IP 使用 imgAppUsCdn。
+func withImageDomain(ctx context.Context, path string) string {
+ path = strings.TrimSpace(path)
+ if path == "" {
+ return ""
+ }
+ if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
+ return path
+ }
+ imageDomain := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
+ if imageDomain == "" {
+ return path
+ }
+ return netutil.MergeURL(imageDomain, path)
+}
diff --git a/app/service/playletser/slug.go b/app/service/playletser/slug.go
new file mode 100644
index 0000000..9610d6d
--- /dev/null
+++ b/app/service/playletser/slug.go
@@ -0,0 +1,101 @@
+package playletser
+
+import (
+ "context"
+ "strings"
+ "videoweb/app/service/playletcache"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// resolvePlayletID 解析短剧ID:slug 非空时先按 slug 查 ID,否则使用传入 ID。
+func resolvePlayletID(ctx context.Context, id int64, slug string, required bool) (int64, *srvdef.SrvErr) {
+ slug = strings.TrimSpace(slug)
+ if slug != "" {
+ item, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Slug: slug,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ }))
+ if err != nil {
+ return 0, err
+ }
+ return item.Id, nil
+ }
+ if required && id <= 0 {
+ return 0, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
+ }
+ return id, nil
+}
+
+// resolveThemeID 解析题材ID:slug 非空时先按 slug 查 ID,否则使用传入 ID(0 表示不按题材筛选)。
+func resolveThemeID(ctx context.Context, id int64, slug string) (int64, *srvdef.SrvErr) {
+ slug = strings.TrimSpace(slug)
+ if slug == "" {
+ return id, nil
+ }
+ if item := playletcache.GetThemeBySlug(slug); item != nil {
+ return item.Id, nil
+ }
+ theme, err := playletthememod.FindOnePlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Slug: slug,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
+ }))
+ if err != nil {
+ return 0, err
+ }
+ return theme.Id, nil
+}
+
+// resolveTagID 解析标签ID:slug 非空时先按 slug 查 ID,否则使用传入 ID。
+func resolveTagID(ctx context.Context, id int64, slug string) (int64, *srvdef.SrvErr) {
+ slug = strings.TrimSpace(slug)
+ if slug != "" {
+ if item := playletcache.GetTagBySlug(slug); item != nil {
+ return item.Id, nil
+ }
+ tag, err := playlettagmod.FindOnePlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Slug: slug,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playlettagmod.FN.PlayletTag.Id, Value: 1},
+ }))
+ if err != nil {
+ return 0, err
+ }
+ return tag.Id, nil
+ }
+ if id <= 0 {
+ return 0, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "标签ID不能为空")
+ }
+ return id, nil
+}
+
+// resolveCategoryID 解析分类ID:slug 非空时先按 slug 查 ID,否则使用传入 ID。
+func resolveCategoryID(ctx context.Context, id int64, slug string) (int64, *srvdef.SrvErr) {
+ slug = strings.TrimSpace(slug)
+ if slug != "" {
+ if item := playletcache.GetCategoryBySlug(slug); item != nil {
+ return item.Id, nil
+ }
+ category, err := playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Slug: slug,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
+ }))
+ if err != nil {
+ return 0, err
+ }
+ return category.Id, nil
+ }
+ if id <= 0 {
+ return 0, srvdef.New(srvdef.ErrParamError, "分类ID无效", "分类ID无效")
+ }
+ return id, nil
+}
diff --git a/app/service/playletthemeser/theme.go b/app/service/playletthemeser/theme.go
new file mode 100644
index 0000000..0b9b29b
--- /dev/null
+++ b/app/service/playletthemeser/theme.go
@@ -0,0 +1,109 @@
+package playletthemeser
+
+import (
+ "context"
+ "strconv"
+ "strings"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/playletcache"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/pubdef"
+)
+
+// List 查询 app 端短剧题材列表(读题材缓存,含 SEO)
+func List(ctx context.Context) (*proto.PlayletThemeListResponse, *srvdef.SrvErr) {
+ list := playletcache.ListEnabledThemes()
+ imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
+ items := make([]*proto.PlayletThemeItem, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.PlayletThemeItem{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Icon: mergeThemeIcon(imgCdn, item.Icon),
+ Rank: item.Rank,
+ Status: item.Status,
+ IsRecommend: item.IsRecommend,
+ PlayletCount: item.PlayletCount,
+ SeoInfo: buildThemeSeoInfo(item),
+ })
+ }
+ return &proto.PlayletThemeListResponse{List: items}, nil
+}
+
+// buildThemeSeoInfo 将题材后台 SEO 转为页面 SeoInfo,缺失字段回退题材名/站点默认。
+func buildThemeSeoInfo(item *playletcache.ThemeItem) *proto.SeoInfo {
+ seo := configser.GetSiteSeoInfo()
+ if item == nil {
+ return &seo
+ }
+ custom := normalizeThemeSeoValue(item.SeoInfo)
+ if title := strings.TrimSpace(custom.Title); title != "" {
+ seo.Title = title
+ } else if name := strings.TrimSpace(item.Name); name != "" {
+ seo.Title = name
+ }
+ if desc := strings.TrimSpace(custom.Description); desc != "" {
+ seo.Description = desc
+ }
+ if len(custom.Keyworld) > 0 {
+ seo.Keywords = append([]string{}, custom.Keyworld...)
+ }
+ pagePath := "/list?themeId=" + strconv.FormatInt(item.Id, 10)
+ seo.CanonicalURL = joinCanonicalURL(seo.CanonicalURL, pagePath)
+ return &seo
+}
+
+func normalizeThemeSeoValue(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 joinCanonicalURL(baseURL, pagePath string) string {
+ pagePath = strings.TrimSpace(pagePath)
+ if pagePath == "" {
+ return strings.TrimSpace(baseURL)
+ }
+ if !strings.HasPrefix(pagePath, "/") {
+ pagePath = "/" + pagePath
+ }
+ baseURL = strings.TrimSpace(baseURL)
+ if baseURL == "" {
+ return pagePath
+ }
+ return strings.TrimRight(baseURL, "/") + pagePath
+}
+
+// mergeThemeIcon 给题材图标拼接图片域名
+func mergeThemeIcon(imgCdn string, icon string) string {
+ icon = strings.TrimSpace(icon)
+ if icon == "" {
+ return ""
+ }
+ if strings.HasPrefix(icon, "http://") || strings.HasPrefix(icon, "https://") {
+ return icon
+ }
+ if imgCdn == "" {
+ return icon
+ }
+ return netutil.MergeURL(imgCdn, icon)
+}
diff --git a/app/service/rechargepackageser/package.go b/app/service/rechargepackageser/package.go
new file mode 100644
index 0000000..6b71e43
--- /dev/null
+++ b/app/service/rechargepackageser/package.go
@@ -0,0 +1,108 @@
+package rechargepackageser
+
+import (
+ "context"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/rechargeser/jiahua"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/rechargepackagemod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetRechargePackageList 查询购买套餐页所需的会员畅享和金币充值列表,并为各套餐附上可用支付方式。
+// 内部统一调用一次嘉华通道查询,避免 N+1 请求。
+func GetRechargePackageList(ctx context.Context, sysType string) (*proto.RechargePackageListResp, *srvdef.SrvErr) {
+ filter := bson.M{rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled}
+ opts := options.Find().SetSort(bson.D{
+ {Key: rechargepackagemod.FN.RechargePackage.PackageType, Value: 1},
+ {Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
+ {Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
+ })
+ list, err := rechargepackagemod.FindManyRechargePackage(ctx, filter, opts)
+ if err != nil {
+ return nil, err
+ }
+ if len(list) == 0 {
+ return &proto.RechargePackageListResp{
+ VipList: make([]*proto.RechargePackageItem, 0),
+ CoinList: make([]*proto.RechargePackageItem, 0),
+ }, nil
+ }
+
+ channelMap := buildPayMethodMap(ctx, list, sysType)
+
+ resp := &proto.RechargePackageListResp{
+ VipList: make([]*proto.RechargePackageItem, 0),
+ CoinList: make([]*proto.RechargePackageItem, 0),
+ }
+ for _, item := range list {
+ data := rechargePackageToAppItem(item, channelMap[item.Price])
+ switch item.PackageType {
+ case rechargepackagemod.PackageTypeVip:
+ resp.VipList = append(resp.VipList, data)
+ case rechargepackagemod.PackageTypeCoin:
+ resp.CoinList = append(resp.CoinList, data)
+ }
+ }
+ return resp, nil
+}
+
+// buildPayMethodMap 按金额批量查询可用支付方式;查询失败时返回空映射,不影响套餐列表展示。
+func buildPayMethodMap(ctx context.Context, list rechargepackagemod.RechargePackageList, sysType string) map[int64][]proto.PayMethodInfo {
+ seen := map[int64]struct{}{}
+ moneys := make([]int64, 0)
+ for _, item := range list {
+ if item == nil || item.Price <= 0 {
+ continue
+ }
+ if _, ok := seen[item.Price]; ok {
+ continue
+ }
+ seen[item.Price] = struct{}{}
+ moneys = append(moneys, item.Price)
+ }
+ if len(moneys) == 0 {
+ return map[int64][]proto.PayMethodInfo{}
+ }
+
+ channels, _ := jiahua.GetPayChannelByMoney(ctx, moneys, sysType)
+ channelMap := make(map[int64][]proto.PayMethodInfo, len(channels))
+ for _, ch := range channels {
+ methods := make([]proto.PayMethodInfo, 0, 3)
+ if ch.Alipay {
+ methods = append(methods, proto.PayMethodInfo{Type: "alipay", TypeName: "支付宝"})
+ }
+ if ch.Wechat {
+ methods = append(methods, proto.PayMethodInfo{Type: "wechat", TypeName: "微信"})
+ }
+ if ch.Union {
+ methods = append(methods, proto.PayMethodInfo{Type: "union", TypeName: "银联"})
+ }
+ channelMap[ch.Money] = methods
+ }
+ return channelMap
+}
+
+// rechargePackageToAppItem 将购买套餐表数据转换为 App 展示字段
+func rechargePackageToAppItem(item *rechargepackagemod.RechargePackage, payMethods []proto.PayMethodInfo) *proto.RechargePackageItem {
+ if payMethods == nil {
+ payMethods = []proto.PayMethodInfo{}
+ }
+ return &proto.RechargePackageItem{
+ Id: item.Id,
+ PackageType: item.PackageType,
+ Name: item.Name,
+ SubTitle: item.SubTitle,
+ Price: item.Price,
+ OriginalPrice: item.OriginalPrice,
+ DurationDays: item.DurationDays,
+ DurationHours: item.DurationDays * 24,
+ BaseCoin: item.BaseCoin,
+ GiftCoin: item.GiftCoin,
+ TotalCoin: item.BaseCoin + item.GiftCoin,
+ Sort: item.Sort,
+ PayMethods: payMethods,
+ }
+}
diff --git a/app/service/rechargeser/jiahua/define.go b/app/service/rechargeser/jiahua/define.go
new file mode 100644
index 0000000..0da3bbe
--- /dev/null
+++ b/app/service/rechargeser/jiahua/define.go
@@ -0,0 +1,52 @@
+package jiahua
+
+import "time"
+
+// PayTypeParam 查询支付通道请求参数
+type PayTypeParam struct {
+ Moneys []int64 `json:"moneys"`
+ DevType string `json:"devType"`
+ PlatformID string `json:"platformID"`
+}
+
+// RchgType 支付通道信息
+type RchgType struct {
+ Money int64 `json:"money"`
+ Alipay bool `json:"alipay"`
+ Wechat bool `json:"wechat"`
+ Union bool `json:"union"`
+}
+
+// PayCenterMsg 查询支付通道响应
+type PayCenterMsg struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data []RchgType `json:"data"`
+}
+
+// PayCenterRchg 下单请求参数
+type PayCenterRchg struct {
+ TransNo string `json:"transNo"`
+ PlatformID string `json:"platformID"`
+ UID string `json:"uid"`
+ DevID string `json:"devID"`
+ UserIP string `json:"userIP"`
+ Name string `json:"name"`
+ DevType string `json:"devType"`
+ Money int64 `json:"money"`
+ Channel string `json:"channel"`
+ CreatedAt time.Time `json:"createdAt"`
+ NotifyUrl string `json:"notifyUrl"`
+ PType string `json:"pType"`
+ PayMethod string `json:"payMethod"`
+}
+
+// PayCenterRchgMsg 下单响应
+type PayCenterRchgMsg struct {
+ Err string `json:"err"`
+ PayUrl string `json:"payUrl"`
+ OID string `json:"oid"`
+ Mode string `json:"mode"`
+ CID string `json:"cid"`
+ Rate string `json:"rate"`
+}
diff --git a/app/service/rechargeser/jiahua/product.go b/app/service/rechargeser/jiahua/product.go
new file mode 100644
index 0000000..7800c13
--- /dev/null
+++ b/app/service/rechargeser/jiahua/product.go
@@ -0,0 +1,100 @@
+package jiahua
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/pubdef"
+)
+
+const (
+ notifyURLSuffix = "/api/web/third/paycenter/bill/callback"
+)
+
+// GetPayChannelByMoney 向嘉华支付中心查询指定金额(分)支持的支付方式
+func GetPayChannelByMoney(ctx context.Context, moneys []int64, devType string) ([]RchgType, *srvdef.SrvErr) {
+ if devType != "android" {
+ devType = "ios"
+ }
+ param := PayTypeParam{
+ Moneys: moneys,
+ DevType: devType,
+ PlatformID: pubdef.AppNameZhifu,
+ }
+ reqBody, err := json.Marshal(param)
+ if err != nil {
+ return nil, srvdef.New(srvdef.Err, "", err.Error())
+ }
+ resp := &PayCenterMsg{}
+ url := appg.Conf.TransactionUrl + "/api/recharge/payType"
+ code, err := utils.POSTJsonWithJResp(resp, url, nil, reqBody)
+ if err != nil {
+ log.Error("jiahua:GetPayChannelByMoney failed", log.Any("url", url), log.E(err))
+ return nil, srvdef.New(srvdef.Err, "查询支付通道失败", err.Error())
+ }
+ if code != http.StatusOK || resp.Code != 200 {
+ log.Error("jiahua:GetPayChannelByMoney bad resp", log.Any("code", code), log.Any("resp.Code", resp.Code))
+ return nil, srvdef.New(srvdef.Err, "查询支付通道失败", "")
+ }
+ return resp.Data, nil
+}
+
+// SubmitOrder 向嘉华支付中心提交下单
+// 返回 SubmitVipOrderInfo(含 PayUrl、Oid 等),供调用方存入本地订单。
+func SubmitOrder(ctx context.Context,
+ userId int64, userName string,
+ tradeNo string, amount int64,
+ payMode string, sysType string, devType string, ip string,
+) (*pubdef.SubmitVipOrderInfo, *srvdef.SrvErr) {
+
+ reqParam := &PayCenterRchg{
+ TransNo: tradeNo,
+ PlatformID: pubdef.AppNameZhifu,
+ UID: strconv.FormatInt(userId, 10),
+ Name: userName,
+ UserIP: ip,
+ DevType: sysType,
+ Money: amount,
+ Channel: "self",
+ CreatedAt: time.Now(),
+ PayMethod: payMode,
+ PType: payMode,
+ NotifyUrl: appg.Conf.LocalWebUrl + notifyURLSuffix,
+ }
+
+ reqBody, err := json.Marshal(reqParam)
+ if err != nil {
+ return nil, srvdef.New(srvdef.Err, "", err.Error())
+ }
+
+ resp := &PayCenterRchgMsg{}
+ url := appg.Conf.TransactionUrl + "/api/recharge/recharge"
+ code, err := utils.POSTJsonWithJResp(resp, url, nil, reqBody)
+ if err != nil {
+ log.Error("jiahua:SubmitOrder POSTJsonWithJResp failed", log.Any("url", url), log.E(err))
+ return nil, srvdef.New(srvdef.Err, "调用支付中心失败", err.Error())
+ }
+ if code != http.StatusOK {
+ log.Error("jiahua:SubmitOrder bad http code", log.Any("code", code))
+ return nil, srvdef.New(srvdef.Err, "调用支付中心失败", "")
+ }
+ if resp.Err != "" {
+ log.Error("jiahua:SubmitOrder resp error", log.Any("err", resp.Err), log.Any("tradeNo", tradeNo))
+ return nil, srvdef.New(srvdef.Err, "支付中心下单失败", resp.Err)
+ }
+
+ return &pubdef.SubmitVipOrderInfo{
+ TradeNo: tradeNo,
+ Oid: resp.OID,
+ PayUrl: resp.PayUrl,
+ PayUrlMode: resp.Mode,
+ PayChannel: resp.CID,
+ PayRate: resp.Rate,
+ }, nil
+}
diff --git a/app/service/rechargeser/recharge.go b/app/service/rechargeser/recharge.go
new file mode 100644
index 0000000..392eab8
--- /dev/null
+++ b/app/service/rechargeser/recharge.go
@@ -0,0 +1,182 @@
+package rechargeser
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/app/service/rechargeser/jiahua"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/vipordermod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// SubmitOrderResp 创建第三方支付订单的响应
+type SubmitOrderResp struct {
+ TradeNo string `json:"tradeNo"` // 本地订单号(可用于查询)
+ PayUrl string `json:"payUrl"` // 支付链接
+ PayUrlMode string `json:"payUrlMode"` // 链接打开方式
+ IsOpenNewBrowser bool `json:"isOpenNewBrowser"` // 是否需要外部浏览器打开
+}
+
+// CreateOrder 创建第三方支付 VIP 订单。
+// 防重复:15 秒内同一用户+同一套餐的 pending 订单直接返回原支付链接。
+func CreateOrder(ctx context.Context,
+ userId int64, userName string,
+ packageId int64, payMode string,
+ sysType string, devType string, ip string,
+ districtCode string,
+) (*SubmitOrderResp, *srvdef.SrvErr) {
+
+ pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
+ rechargepackagemod.FN.RechargePackage.Id: packageId,
+ rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
+ rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
+ })
+ if serr != nil || pkg == nil {
+ return nil, srvdef.New(srvdef.ErrNoData, "会员套餐不存在或已下架", "")
+ }
+
+ now := time.Now()
+ dupFilter := bson.M{
+ vipordermod.FN.VipOrder.UserId: userId,
+ vipordermod.FN.VipOrder.CardId: packageId,
+ vipordermod.FN.VipOrder.PayMode: payMode,
+ vipordermod.FN.VipOrder.Status: pubdef.VipOrderPending,
+ vipordermod.FN.VipOrder.CreatedAt: bson.M{"$gte": now.Add(-15 * time.Second)},
+ }
+ existing, _ := vipordermod.FindOneVipOrder(ctx, dupFilter)
+ if existing != nil && existing.PayUrl != "" {
+ return &SubmitOrderResp{
+ TradeNo: existing.TradeNo,
+ PayUrl: existing.PayUrl,
+ PayUrlMode: existing.PayUrlMode,
+ IsOpenNewBrowser: true,
+ }, nil
+ }
+
+ tradeNo := pubdef.GetTrandNoWithUUID()
+ amount := pkg.Price
+
+ payInfo, serr := jiahua.SubmitOrder(ctx, userId, userName, tradeNo, amount, payMode, sysType, devType, ip)
+ if serr != nil {
+ return nil, serr
+ }
+
+ order := &vipordermod.VipOrder{
+ Id: mongos.Lid.VipOrder.Get(),
+ OrderType: vipordermod.OrderTypeVip,
+ UserId: userId,
+ CardId: packageId, // 兼容历史字段,存购买套餐ID
+ CardName: pkg.Name,
+ Amount: amount,
+ PayMode: payMode,
+ TradeNo: tradeNo,
+ Oid: payInfo.Oid,
+ Status: pubdef.VipOrderPending,
+ PayUrl: payInfo.PayUrl,
+ PayUrlMode: payInfo.PayUrlMode,
+ PayChannel: payInfo.PayChannel,
+ PayRate: payInfo.PayRate,
+ IPAddr: ip,
+ SysType: sysType,
+ DistrictCode: strings.TrimSpace(districtCode),
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ if serr = vipordermod.InsertOneVipOrder(ctx, order); serr != nil {
+ log.Error("rechargeser:CreateOrder InsertOneVipOrder failed",
+ log.Any("tradeNo", tradeNo), log.E(serr))
+ return nil, serr
+ }
+
+ go reportOrderCreated(ctx, order, pkg, userId, ip)
+
+ return &SubmitOrderResp{
+ TradeNo: tradeNo,
+ PayUrl: payInfo.PayUrl,
+ PayUrlMode: payInfo.PayUrlMode,
+ IsOpenNewBrowser: true,
+ }, nil
+}
+
+// GetOrderHistory 查询用户的 VIP 订单历史
+func GetOrderHistory(ctx context.Context, userId int64, page, pageSize int64) (vipordermod.VipOrderList, int64, *srvdef.SrvErr) {
+ filter := bson.M{
+ vipordermod.FN.VipOrder.UserId: userId,
+ vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeVip,
+ }
+ total, serr := vipordermod.CountVipOrder(ctx, filter)
+ if serr != nil {
+ return nil, 0, serr
+ }
+
+ skip := (page - 1) * pageSize
+ opts := options.Find().
+ SetSort(bson.M{vipordermod.FN.VipOrder.CreatedAt: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize)
+ list, serr := vipordermod.FindManyVipOrder(ctx, filter, opts)
+ return list, total, serr
+}
+
+// GetUserInfo 获取用户信息(供 app 层展示当前 VIP 状态)
+func GetUserInfo(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
+ return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+}
+
+func reportOrderCreated(ctx context.Context, order *vipordermod.VipOrder, pkg *rechargepackagemod.RechargePackage, userId int64, ip string) {
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ districtCode := ""
+ if user != nil {
+ districtCode = user.DistrictCode
+ }
+
+ ua := useragent.UA{SysType: order.SysType}
+ eventId := newEventId(pubdef.TrackerAppId(), user.Id, order.TradeNo, order.CreatedAt.Unix())
+ commonField := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderCreated, districtCode, ua, strconv.FormatInt(user.Id, 10), ip)
+
+ commonField.Payload = dataReport.JsonRawMessage(&dataReport.OrderCreatedEvent{
+ OrderID: order.TradeNo,
+ OrderType: "vip_subscription",
+ ProductID: strconv.FormatInt(pkg.Id, 10),
+ ProductName: pkg.Name,
+ Amount: dataReport.JsonNumber(order.Amount),
+ Currency: "CNY",
+ CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
+ })
+
+ eventData, _ := json.Marshal(commonField)
+ if err := eventmod.InsertOneEvent(ctx, &eventmod.Event{
+ EventId: commonField.EventID,
+ EventType: commonField.Event,
+ Data: string(eventData),
+ Status: eventmod.Default,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }); err != nil {
+ log.Error("reportOrderCreated InsertOneEvent failed",
+ log.Any("tradeNo", order.TradeNo), log.E(err))
+ }
+}
+
+func newEventId(appId string, uid int64, orderId interface{}, createTime int64) string {
+ input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
+ h := md5.New()
+ h.Write([]byte(input))
+ return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/app/service/reportser/report.go b/app/service/reportser/report.go
new file mode 100644
index 0000000..d356251
--- /dev/null
+++ b/app/service/reportser/report.go
@@ -0,0 +1,386 @@
+package reportser
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "time"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const videoContentTypePlaylet = "short_video"
+
+// ReportUserRegister 用户注册事件
+func ReportUserRegister(user *usermod.User, ua useragent.UA, ip, registerType string) {
+ if user == nil {
+ return
+ }
+ if registerType == "" {
+ registerType = "deviceid"
+ }
+ ua = fillUserUA(user, ua)
+ createAt := user.CreatedAt
+ if !user.RegisterTime.IsZero() {
+ createAt = user.RegisterTime
+ }
+ eventId := newEventId(pubdef.TrackerAppId(), user.Id, "", createAt.Unix())
+ event := dataReport.NewCommonField(eventId, dataReport.EventTypeUserRegister, user.DistrictCode, ua, strconv.FormatInt(user.Id, 10), ip)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.UserRegisterEvent{
+ Type: registerType,
+ CreateTime: dataReport.JsonNumber(createAt.Unix()),
+ })
+ insertEvent(event)
+}
+
+// ReportUserLogin 用户登录事件
+func ReportUserLogin(user *usermod.User, ua useragent.UA, ip string, loginType pubdef.LoginType) {
+ if user == nil {
+ return
+ }
+ ua = fillUserUA(user, ua)
+ createTime := time.Now().Unix()
+ eventId := newEventId(pubdef.TrackerAppId(), user.Id, "", createTime)
+ event := dataReport.NewCommonField(eventId, dataReport.EventTypeUserLogin, user.DistrictCode, ua, strconv.FormatInt(user.Id, 10), ip)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.UserLoginEvent{
+ Type: loginTypeEventValue(loginType),
+ })
+ insertEvent(event)
+}
+
+// ReportPlayletLike 短剧点赞,复用 video_like
+func ReportPlayletLike(ctx context.Context, userId, playletId, flag int64) {
+ reportPlayletVideoEvent(ctx, userId, playletId, 0, dataReport.EventTypeVideoLike, flag, "")
+}
+
+// ReportPlayletCollect 短剧收藏,复用 video_collect
+func ReportPlayletCollect(ctx context.Context, userId, playletId, flag int64) {
+ reportPlayletVideoEvent(ctx, userId, playletId, 0, dataReport.EventTypeVideoCollect, flag, "")
+}
+
+// ReportPlayletChapterLike 短剧剧集点赞,复用 video_like
+func ReportPlayletChapterLike(ctx context.Context, userId, playletId, chapterId, flag int64) {
+ reportPlayletVideoEvent(ctx, userId, playletId, chapterId, dataReport.EventTypeVideoLike, flag, "")
+}
+
+// ReportPlayletChapterCollect 短剧剧集收藏,复用 video_collect
+func ReportPlayletChapterCollect(ctx context.Context, userId, playletId, chapterId, flag int64) {
+ reportPlayletVideoEvent(ctx, userId, playletId, chapterId, dataReport.EventTypeVideoCollect, flag, "")
+}
+
+// ReportPlayletComment 短剧/剧集评论,复用 video_comment
+func ReportPlayletComment(ctx context.Context, user *usermod.User, playletId, chapterId int64, content string) {
+ if user == nil || playletId <= 0 {
+ return
+ }
+ pub := buildPlayletVideoPub(ctx, playletId, chapterId)
+ if pub.VideoID == "" {
+ return
+ }
+ ua, ip := uaAndIPFromCtx(ctx)
+ ua = fillUserUA(user, ua)
+ event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoComment, user.DistrictCode, ua, strconv.FormatInt(user.Id, 10), ip)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoCommentEvent{
+ VideoEventPub: pub,
+ CommentContent: content,
+ })
+ insertEvent(event)
+}
+
+// ReportPlayletUnlock 短剧剧集金币解锁:同时上报金币消耗和购买
+func ReportPlayletUnlock(ctx context.Context, user *usermod.User, playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter, amount, balanceBefore, balanceAfter int64, orderNo string) {
+ if user == nil || playlet == nil || chapter == nil || amount <= 0 {
+ return
+ }
+ if full, err := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}); err == nil && full != nil {
+ user = full
+ }
+ ua, ip := uaAndIPFromCtx(ctx)
+ ua = fillUserUA(user, ua)
+ productId := strconv.FormatInt(chapter.Id, 10)
+ productName := playletUnlockProductName(playlet, chapter)
+ reportCoinConsume(user, ua, ip, productId, productName, amount, balanceBefore, balanceAfter, KeyPlayletUnlock, orderNo)
+ reportPlayletPurchase(user, ua, ip, playlet, chapter, amount, orderNo)
+}
+
+// ReportPlayletStatusChange 短剧上下架,复用 video_status_change
+func ReportPlayletStatusChange(playlet *playletmod.Playlet, flag int64) {
+ if playlet == nil || playlet.Id == 0 || flag == 0 {
+ return
+ }
+ typeID, typeName := resolvePlayletType(context.Background(), playlet)
+ event := dataReport.NewCommonField("", dataReport.EventTypeVideoStatusChange, "", useragent.UA{}, "", "")
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoStatusChangeEvent{
+ MediaId: playletMediaId(playlet),
+ VideoID: strconv.FormatInt(playlet.Id, 10),
+ VideoTitle: playlet.Title,
+ VideoTypeID: typeID,
+ VideoTypeName: typeName,
+ Flag: dataReport.JsonNumber(flag),
+ })
+ insertEvent(event)
+}
+
+// PlayletStatusFlag 短剧上下架映射上报 flag:1上架 2下架
+func PlayletStatusFlag(status playletmod.Status) int64 {
+ switch status {
+ case playletmod.StatusUp:
+ return 1
+ case playletmod.StatusDown:
+ return 2
+ default:
+ return 0
+ }
+}
+
+func reportPlayletVideoEvent(ctx context.Context, userId, playletId, chapterId int64, eventType string, flag int64, content string) {
+ if userId <= 0 || playletId <= 0 {
+ return
+ }
+ user, err := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ if err != nil || user == nil {
+ return
+ }
+ pub := buildPlayletVideoPub(ctx, playletId, chapterId)
+ if pub.VideoID == "" {
+ return
+ }
+ ua, ip := uaAndIPFromCtx(ctx)
+ ua = fillUserUA(user, ua)
+ event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), eventType, user.DistrictCode, ua, strconv.FormatInt(user.Id, 10), ip)
+ switch eventType {
+ case dataReport.EventTypeVideoLike:
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoLikeEvent{
+ VideoEventPub: pub,
+ Flag: dataReport.JsonNumber(flag),
+ })
+ case dataReport.EventTypeVideoCollect:
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoCollectEvent{
+ VideoEventPub: pub,
+ Flag: dataReport.JsonNumber(flag),
+ })
+ case dataReport.EventTypeVideoComment:
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoCommentEvent{
+ VideoEventPub: pub,
+ CommentContent: content,
+ })
+ default:
+ return
+ }
+ insertEvent(event)
+}
+
+func reportCoinConsume(user *usermod.User, ua useragent.UA, ip, productId, productName string, amount, balanceBefore, balanceAfter int64, reason Key, orderId string) {
+ if user == nil || amount == 0 {
+ return
+ }
+ if productId == "" {
+ if productName == "" {
+ productName = "金币消耗"
+ }
+ productId = md5String(productName)
+ }
+ createTime := time.Now().Unix()
+ eventId := newEventId(pubdef.TrackerAppId(), user.Id, orderId, createTime)
+ event := dataReport.NewCommonField(eventId, dataReport.EventTypeCoinConsume, user.DistrictCode, ua, strconv.FormatInt(user.Id, 10), ip)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.CoinConsumeEvent{
+ ProductID: productId,
+ ProductName: productName,
+ CoinConsumeAmount: dataReport.JsonNumber(amount),
+ CoinBalanceBefore: dataReport.JsonNumber(balanceBefore),
+ CoinBalanceAfter: dataReport.JsonNumber(balanceAfter),
+ ConsumeReasonKey: reason.Key(),
+ ConsumeReasonName: reason.Name(),
+ CreateTime: dataReport.JsonNumber(createTime),
+ OrderId: orderId,
+ })
+ insertEvent(event)
+}
+
+func reportPlayletPurchase(user *usermod.User, ua useragent.UA, ip string, playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter, amount int64, orderId string) {
+ pub := playletToVideoPub(playlet, chapter)
+ event := dataReport.NewCommonField(dataReport.GenerateGlobalEventID(), dataReport.EventTypeVideoPurchase, user.DistrictCode, ua, strconv.FormatInt(user.Id, 10), ip)
+ event.Payload = dataReport.JsonRawMessage(&dataReport.VideoPurchaseEvent{
+ MediaId: pub.MediaId,
+ VideoID: pub.VideoID,
+ VideoTitle: pub.VideoTitle,
+ VideoTypeID: pub.VideoTypeID,
+ VideoTypeName: pub.VideoTypeName,
+ VideoContentType: pub.VideoContentType,
+ RecommendTraceId: pub.RecommendTraceId,
+ CoinQuantity: dataReport.JsonNumber(amount),
+ OrderID: orderId,
+ })
+ insertEvent(event)
+}
+
+func buildPlayletVideoPub(ctx context.Context, playletId, chapterId int64) dataReport.VideoEventPub {
+ 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.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
+ }))
+ if err != nil || playlet == nil {
+ return dataReport.VideoEventPub{}
+ }
+ var chapter *playletchaptermod.PlayletChapter
+ if chapterId > 0 {
+ chapter, _ = playletchaptermod.FindOnePlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.Id: chapterId}, options.FindOne().SetProjection(bson.D{
+ {Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
+ {Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
+ }))
+ }
+ return playletToVideoPub(playlet, chapter)
+}
+
+func playletToVideoPub(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) dataReport.VideoEventPub {
+ if playlet == nil {
+ return dataReport.VideoEventPub{}
+ }
+ typeID, typeName := resolvePlayletType(context.Background(), playlet)
+ videoID := strconv.FormatInt(playlet.Id, 10)
+ title := playlet.Title
+ if chapter != nil && chapter.Id > 0 {
+ videoID = strconv.FormatInt(chapter.Id, 10)
+ title = playletUnlockProductName(playlet, chapter)
+ }
+ return dataReport.VideoEventPub{
+ MediaId: playletMediaId(playlet),
+ VideoID: videoID,
+ VideoTitle: title,
+ VideoTypeID: typeID,
+ VideoTypeName: typeName,
+ VideoContentType: videoContentTypePlaylet,
+ }
+}
+
+func playletUnlockProductName(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) string {
+ title := ""
+ if playlet != nil {
+ title = playlet.Title
+ }
+ if chapter == nil {
+ return title
+ }
+ if chapter.Title != "" {
+ if title == "" {
+ return chapter.Title
+ }
+ return fmt.Sprintf("%s %s", title, chapter.Title)
+ }
+ if chapter.CurrentEpisode > 0 {
+ if title == "" {
+ return fmt.Sprintf("第%d集", chapter.CurrentEpisode)
+ }
+ return fmt.Sprintf("%s 第%d集", title, chapter.CurrentEpisode)
+ }
+ return title
+}
+
+func playletMediaId(playlet *playletmod.Playlet) string {
+ if playlet == nil || playlet.MediaCenterId == 0 {
+ return ""
+ }
+ return strconv.FormatUint(uint64(playlet.MediaCenterId), 10)
+}
+
+func resolvePlayletType(ctx context.Context, playlet *playletmod.Playlet) (string, string) {
+ if playlet == nil || len(playlet.CategoryIds) == 0 {
+ return "default", "默认分类"
+ }
+ queryCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ category, err := playletcategorymod.FindOnePlayletCategory(queryCtx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: playlet.CategoryIds[0],
+ })
+ if err != nil || category == nil {
+ return "default", "默认分类"
+ }
+ return strconv.FormatInt(category.Id, 10), category.Name
+}
+
+func uaAndIPFromCtx(ctx context.Context) (useragent.UA, string) {
+ gc, ok := ctx.(*gin.Context)
+ if !ok {
+ if v := ctx.Value(consts.CtxUA); v != nil {
+ if ua, ok := v.(useragent.UA); ok {
+ return ua, ""
+ }
+ }
+ return useragent.UA{}, ""
+ }
+ ua, _ := common.GetUA(gc)
+ return ua, ipaddr.ResolveClientIP(gc)
+}
+
+func fillUserUA(user *usermod.User, ua useragent.UA) useragent.UA {
+ if user == nil {
+ return ua
+ }
+ if ua.DevID == "" {
+ ua.DevID = user.DevID
+ }
+ if ua.DevType == "" {
+ ua.DevType = user.Device
+ }
+ if ua.SysType == "" {
+ ua.SysType = user.SysType
+ }
+ return ua
+}
+
+func loginTypeEventValue(loginType pubdef.LoginType) string {
+ switch loginType {
+ case pubdef.LoginTypeAccount:
+ return "username"
+ case pubdef.LoginTypeEmail:
+ return "email"
+ default:
+ return "deviceid"
+ }
+}
+
+func newEventId(appId string, uid int64, orderId interface{}, createTime int64) string {
+ input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
+ return md5String(input)
+}
+
+func md5String(input string) string {
+ h := md5.New()
+ h.Write([]byte(input))
+ return hex.EncodeToString(h.Sum(nil))
+}
+
+func insertEvent(event dataReport.CommonField) {
+ 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("reportser insertEvent failed", log.Any("eventType", event.Event), log.Any("eventId", event.EventID), log.E(err))
+ }
+}
diff --git a/app/service/reportser/type.go b/app/service/reportser/type.go
new file mode 100644
index 0000000..a7ffb02
--- /dev/null
+++ b/app/service/reportser/type.go
@@ -0,0 +1,21 @@
+package reportser
+
+type Key string
+
+const (
+ KeyPlayletUnlock Key = "playlet_unlock" // 短剧剧集解锁
+ KeyCoinConsume Key = "coin_consume" // 金币消耗
+)
+
+var keyMap = map[Key]string{
+ KeyPlayletUnlock: "短剧解锁",
+ KeyCoinConsume: "金币消耗",
+}
+
+func (p Key) Name() string {
+ return keyMap[p]
+}
+
+func (p Key) Key() string {
+ return string(p)
+}
diff --git a/app/service/searchser/cache.go b/app/service/searchser/cache.go
new file mode 100644
index 0000000..06303b9
--- /dev/null
+++ b/app/service/searchser/cache.go
@@ -0,0 +1 @@
+package searchser
diff --git a/app/service/searchser/search.go b/app/service/searchser/search.go
new file mode 100644
index 0000000..f104313
--- /dev/null
+++ b/app/service/searchser/search.go
@@ -0,0 +1,61 @@
+package searchser
+
+import (
+ "context"
+ "time"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/translationser"
+ "videoweb/pub/pubdef"
+
+ "github.com/patrickmn/go-cache"
+)
+
+var (
+ Cache *cache.Cache
+)
+
+func init() {
+ Cache = cache.New(5*time.Minute, 5*time.Minute)
+}
+
+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),
+ PageNum: page,
+ PageSize: pubdef.PageSize,
+ }
+ resp.InTitle = title
+ resp.InType = mediaType
+ resp.Sort = getSearchSort()
+ resp.SearchRecommend = getSearchRecommend(title)
+ resp.Config = configser.GetConfigInfo()
+ resp.Translations = translationser.GetTranslationData()
+ resp.BottonTranslations = translationser.GetTranslationBottomList()
+
+ resp.TotalSize = pubdef.GetTotalPage(resp.TotalSize, pubdef.PageSize)
+ return resp
+}
+
+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/translationser/catch.go b/app/service/translationser/catch.go
new file mode 100644
index 0000000..e1a5ee9
--- /dev/null
+++ b/app/service/translationser/catch.go
@@ -0,0 +1,171 @@
+package translationser
+
+import (
+ "context"
+ "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/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
+)
+
+type CacheInfo struct {
+ Lock sync.RWMutex // 读写锁
+ TranslationData proto.AppTranslationInfo // 导航信息缓存
+ BottomTranslationData proto.AppTranslationInfo // 底部导航信息缓存
+ TranslationMap map[int64]proto.TranslationInfo
+}
+
+func init() {
+ skdCron = cron.New(cron.WithSeconds()) // 秒级定时任务
+ CacheMgr.TranslationData = proto.AppTranslationInfo{}
+ CacheMgr.BottomTranslationData = proto.AppTranslationInfo{}
+ CacheMgr.TranslationMap = make(map[int64]proto.TranslationInfo)
+}
+
+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(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()
+
+ 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,
+ SeoInfo: categoryInfo.SeoInfo,
+ }
+ if categoryTemp.CategoryType == pubdef.TranslationTypeUrl {
+ categoryTemp.UrlValue = categoryInfo.UrlValue
+ } else if categoryInfo.CategoryType == pubdef.TranslationTypeSub {
+ for _, translationInfo := range translationList {
+ if categoryInfo.Id == translationInfo.TransCategoryId {
+ translationTmp := proto.TranslationInfo{
+ Name: translationInfo.Name,
+ }
+ if translationInfo.IsFriendAds {
+ translationTmp.IsFriendAds = true
+ // 友链广告 获取的时候添加
+ // TODO:顶部暂不支持 。
+ continue
+ } else {
+ translationTmp.UrlValue = translationInfo.Link
+ }
+ if translationTmp.UrlValue == "" {
+ continue
+ }
+ tempTranslationMap[translationInfo.Id] = translationTmp
+ categoryTemp.TranslationList = append(categoryTemp.TranslationList, translationTmp)
+ }
+ }
+ } else if categoryInfo.CategoryType == pubdef.TranslationTypeBottom {
+ for _, translationInfo := range translationList {
+ if categoryInfo.Id == translationInfo.TransCategoryId {
+ translationTmp := proto.TranslationInfo{
+ Name: translationInfo.Name,
+ }
+ if translationInfo.IsFriendAds {
+ translationTmp.IsFriendAds = true
+ // 友链广告 获取的时候添加
+ } else {
+ 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("", log.Any("categoryInfo.Name", translation.CategoryList))
+ CacheMgr.Lock.Lock()
+ defer CacheMgr.Lock.Unlock()
+ CacheMgr.TranslationData = translation
+ CacheMgr.BottomTranslationData = bottomTranslation
+ CacheMgr.TranslationMap = tempTranslationMap
+
+}
+func GetTranslationData() proto.AppTranslationInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+
+ log.Info("GetTranslationData", log.Any("TranslationData", CacheMgr.TranslationData))
+ return CacheMgr.TranslationData
+}
+func GetTranslationBottomList() proto.AppTranslationInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ return CacheMgr.BottomTranslationData
+}
+
+func GetTranslationInfo(id int64) *proto.TranslationInfo {
+ CacheMgr.Lock.RLock()
+ defer CacheMgr.Lock.RUnlock()
+ if v, ok := CacheMgr.TranslationMap[id]; ok {
+ return &v
+ }
+ return nil
+}
diff --git a/app/service/translationser/translation.go b/app/service/translationser/translation.go
new file mode 100644
index 0000000..6d21bde
--- /dev/null
+++ b/app/service/translationser/translation.go
@@ -0,0 +1 @@
+package translationser
diff --git a/app/service/uploadser/upload.go b/app/service/uploadser/upload.go
new file mode 100644
index 0000000..2338a53
--- /dev/null
+++ b/app/service/uploadser/upload.go
@@ -0,0 +1,127 @@
+package uploadser
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "strconv"
+ "videoweb/app/service/configser"
+
+ "videoweb/pub/common/log"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+)
+
+const (
+ // 图片上传路径
+ imagePath = "/media/web/autopublish/image"
+)
+
+// 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"`
+}
+
+// ImageParam 图片上传入参
+type ImageParam struct {
+ AppId string
+ Watermark int64
+ FileHeader *multipart.FileHeader
+}
+
+// targetDomain 获取转发目标域名
+func targetDomain() string {
+ return proxy.DomainAddressRepair("", proxy.BucketVideoAddressV2)
+}
+
+// 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.Watermark == 1 {
+ watermarkImage := configser.GetConfigInfo().WatermarkImage
+ if err := mw.WriteField("watermarkImage", watermarkImage); 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/h265.go b/app/service/videoser/h265.go
new file mode 100644
index 0000000..89f119d
--- /dev/null
+++ b/app/service/videoser/h265.go
@@ -0,0 +1,60 @@
+package videoser
+
+import (
+ "strings"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/models/videoinformationmod"
+)
+
+func toVideoInfoData(videoInfo *videoinformationmod.VideoInformation, imgCdn string) proto.VideoInfoData {
+ data := proto.VideoInfoToData(videoInfo, imgCdn)
+ normalizeVideoInfoH265Paths(&data)
+ configser.ApplyVideoH265Policy(&data)
+ return data
+}
+
+func applyVideoUrlAuth(resp *proto.VideoInfoData, videoId int64, ip, ua string, userId int64) {
+ normalizeVideoInfoH265Paths(resp)
+ configser.ApplyVideoH265Policy(resp)
+
+ if len(resp.Video.Url) > 0 {
+ resp.Video.Url = GetM3U8AuthParam(videoId, resp.Video.Url, ip, ua, userId)
+ }
+ if len(resp.Video.H265Url) > 0 {
+ resp.Video.H265Url = GetM3U8AuthParam(videoId, resp.Video.H265Url, ip, ua, userId)
+ }
+ for i, node := range resp.Nodes {
+ for j, video := range node.Videos {
+ if len(video.Url) > 0 {
+ video.Url = GetM3U8AuthParam(videoId, video.Url, ip, ua, userId)
+ }
+ if len(video.H265Url) > 0 {
+ video.H265Url = GetM3U8AuthParam(videoId, video.H265Url, ip, ua, userId)
+ }
+ node.Videos[j] = video
+ }
+ resp.Nodes[i] = node
+ }
+}
+
+func normalizeVideoInfoH265Paths(data *proto.VideoInfoData) {
+ if data == nil {
+ return
+ }
+ data.Video.H265Url = normalizeLaosijiHEVCPath(data.Video.H265Url)
+ for i := range data.Nodes {
+ for j := range data.Nodes[i].Videos {
+ data.Nodes[i].Videos[j].H265Url = normalizeLaosijiHEVCPath(data.Nodes[i].Videos[j].H265Url)
+ }
+ }
+}
+
+func normalizeLaosijiHEVCPath(path string) string {
+ path = strings.TrimSpace(path)
+ path = strings.TrimPrefix(path, "laosiji/m3m/")
+ if path != "" && !strings.HasPrefix(path, "laosiji/") {
+ return "laosiji/" + path
+ }
+ return path
+}
diff --git a/app/service/videoser/h5_m3u8_ticket.go b/app/service/videoser/h5_m3u8_ticket.go
new file mode 100644
index 0000000..0cf9cc1
--- /dev/null
+++ b/app/service/videoser/h5_m3u8_ticket.go
@@ -0,0 +1,255 @@
+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/enum/redisconst"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/redis"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ h5M3U8TicketTTLSeconds = int64(600)
+ h5M3U8TicketKeyVersion = "v1"
+ h5M3U8TicketBindIP = true
+ h5M3U8RoutePrefix = "/h5/m3u8/"
+ hardcodedH5M3U8TicketKey = "3d058714838683a97c04d7b6ab609fc196f29e55df36197341d87325dc63154a"
+ H5M3U8FallbackPath = "/v3/av/w0/w6/db/7j/029e2f2df53d4741ad39b471e2ad1806.m3u8"
+)
+
+// H5M3U8Ticket 记录业务接口已确认的正片/预览权限,供 m3u8 接口可信计数。
+type H5M3U8Ticket struct {
+ MediaID int64 `json:"mediaId"`
+ Path string `json:"path"`
+ ExpireAt int64 `json:"expireAt"`
+ IPHash string `json:"ipHash,omitempty"`
+ UserId int64 `json:"userId,omitempty"` // 签发时登录用户,供会员日限识别
+ VipExpireAt int64 `json:"vipExpireAt,omitempty"` // 签发时会员过期时间(unix),播放侧据此判断会员日限,避免查库
+ Playable bool `json:"playable"`
+ Preview bool `json:"preview"`
+ UseNewKey bool `json:"useNewKey,omitempty"`
+ KeyVersion string `json:"keyVersion,omitempty"`
+}
+
+func h5M3U8TicketKey() string {
+ return hardcodedH5M3U8TicketKey
+}
+
+// BuildH5M3U8LegacyURL 返回不带路由前缀的相对票据地址,兼容 H5 自行拼接 /h5/m3u8/。
+func BuildH5M3U8LegacyURL(mediaID int64, path, ip string, playable, preview bool, userId int64) string {
+ return buildH5M3U8LegacyURL(mediaID, path, ip, playable, preview, false, userId)
+}
+
+// BuildH5M3U8PlayletURL 为短剧播放地址生成票据,并在票据中记录原片密钥类型。
+func BuildH5M3U8PlayletURL(mediaID int64, path, ip string, useNewKey bool, userId int64) string {
+ return buildH5M3U8LegacyURL(mediaID, path, ip, true, false, useNewKey, userId)
+}
+
+func buildH5M3U8LegacyURL(mediaID int64, path, ip string, playable, preview, useNewKey bool, userId 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,
+ UserId: userId,
+ VipExpireAt: lookupUserVipExpireUnix(userId),
+ Playable: playable,
+ Preview: preview,
+ UseNewKey: useNewKey,
+ KeyVersion: h5M3U8TicketKeyVersion,
+ }
+ 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, ok := decodeAndValidateH5M3U8Ticket(mediaID, expireAt, ip, ticket)
+ if !ok || data == nil {
+ return nil, false
+ }
+ normalizedPath, pathOK := normalizeH5M3U8Path(path)
+ if !pathOK {
+ return nil, false
+ }
+ if payloadPath, valid := normalizeH5M3U8Path(data.Path); !valid || payloadPath != normalizedPath {
+ return nil, false
+ }
+ return data, true
+}
+
+// CheckH5M3U8TicketForEnkey 校验 /enkey 请求,不校验 m3u8 路径。
+func CheckH5M3U8TicketForEnkey(mediaID, expireAt int64, ip, ticket string) (*H5M3U8Ticket, bool) {
+ return decodeAndValidateH5M3U8Ticket(mediaID, expireAt, ip, ticket)
+}
+
+func decodeAndValidateH5M3U8Ticket(mediaID, expireAt int64, ip, ticket string) (*H5M3U8Ticket, bool) {
+ key := h5M3U8TicketKey()
+ if key == "" || mediaID <= 0 || expireAt <= 0 || strings.TrimSpace(ticket) == "" {
+ return nil, false
+ }
+ data, err := decodeH5M3U8Ticket(ticket, key)
+ if err != nil {
+ return nil, false
+ }
+ if data.MediaID != mediaID || data.ExpireAt != expireAt {
+ return nil, false
+ }
+ if data.KeyVersion != "" && data.KeyVersion != h5M3U8TicketKeyVersion {
+ return nil, false
+ }
+ if time.Now().Unix() > data.ExpireAt {
+ return nil, false
+ }
+ if h5M3U8TicketBindIP && data.IPHash != h5M3U8Hash(ip) {
+ return nil, false
+ }
+ return data, true
+}
+
+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[:])
+}
+
+// lookupUserVipExpireUnix 取用户会员过期 unix 时间;优先 Redis 短缓存,未登录返回 0。
+// 仅在签发票据时调用,避免 /h5/m3u8 热路径查库。
+func lookupUserVipExpireUnix(userId int64) int64 {
+ if userId <= 0 {
+ return 0
+ }
+ cacheKey := redisconst.M3U8UserVipExpireKey(userId)
+ if cached, err := redis.Get(cacheKey); err == nil && cached != nil {
+ if n, parseErr := strconv.ParseInt(*cached, 10, 64); parseErr == nil {
+ return n
+ }
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ user, serr := usermod.FindOneUser(ctx, bson.M{
+ usermod.FN.User.Id: userId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: usermod.FN.User.Id, Value: 1},
+ {Key: usermod.FN.User.CurrentVipExpire, Value: 1},
+ }))
+ vipExpireAt := int64(0)
+ if serr == nil && user != nil && !user.CurrentVipExpire.IsZero() {
+ vipExpireAt = user.CurrentVipExpire.Unix()
+ }
+ _ = redis.Set(cacheKey, strconv.FormatInt(vipExpireAt, 10), redisconst.M3U8UserVipExpireCache)
+ return vipExpireAt
+}
diff --git a/app/service/videoser/m3u8.go b/app/service/videoser/m3u8.go
new file mode 100644
index 0000000..8f9ec26
--- /dev/null
+++ b/app/service/videoser/m3u8.go
@@ -0,0 +1,349 @@
+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/redis"
+
+ "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) {
+ handleTicketEnkey(ctx, false)
+}
+
+// NewEnkey 返回短剧未完成水印时使用的新 M3U8 密钥。
+func NewEnkey(ctx *gin.Context) {
+ handleTicketEnkey(ctx, true)
+}
+
+func handleTicketEnkey(ctx *gin.Context, useNewKey bool) {
+ 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 != useNewKey {
+ log.Warn("Enkey: ticket invalid", log.Any("ip", ip), log.Any("videoId", videoId))
+ err = errors.New("ticket check failed")
+ return
+ }
+
+ var buffer *bytes.Buffer
+ if useNewKey {
+ buffer, err = m3u8.NewEnkey(c)
+ } else {
+ 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)
+ isOverseas = configser.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 := CheckH5M3U8Ticket(
+ mediaID, expireAt, origM3u8Name, ip, ticket,
+ )
+
+ if !ticketOK {
+ m3u8Name = H5M3U8FallbackPath
+ log.Warn("【GetM3U8Ex】H5播放票据无效",
+ log.Any("mediaId", mediaID),
+ log.Any("expireAt", expireAt),
+ log.Any("ip", ip),
+ log.Any("hasTicket", ticket != ""))
+ }
+
+ countTowardDailyLimit := ticketOK && ticketInfo.Playable && !ticketInfo.Preview
+ if countTowardDailyLimit && appg.Conf.RunMod == string(consts.RunModeProd) {
+ userId := resolveM3U8RateLimitUserId(c, ticketInfo)
+ vipValid := isM3U8TicketVipValid(ticketInfo)
+ limit := redisconst.M3U8DayRateLimitMax
+ var cnt int64
+ var countErr error
+ if vipValid {
+ limit = redisconst.M3U8DayRateLimitVipMax
+ cnt, countErr = m3u8DailyMediaCountByUser(userId, mediaID)
+ } else {
+ cnt, countErr = m3u8DailyMediaCountByIP(ip, mediaID)
+ }
+ if countErr != nil {
+ log.Error("【GetM3U8Ex】统计正片获取次数失败",
+ log.Any("ip", ip),
+ log.Any("userId", userId),
+ log.Any("vipValid", vipValid),
+ log.E(countErr))
+ } else if cnt > limit {
+ m3u8Name = m3u8RateLimitFallback
+ status = "rate_limited"
+ log.Warn("GetM3U8Ex daily rate limit, use fallback m3u8",
+ log.Any("ip", ip),
+ log.Any("userId", userId),
+ log.Any("vipValid", vipValid),
+ log.Any("limit", limit),
+ 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 := ""
+ if ticketOK {
+ enkeyPath := "/enkey"
+ if ticketInfo.UseNewKey {
+ enkeyPath = "/newenkey"
+ }
+ enkeyUri = fmt.Sprintf("%s?id=%d&t=%d&ticket=%s", enkeyPath, mediaID, expireAt, url.QueryEscape(ticket))
+ }
+
+ var buff *bytes.Buffer
+ if ticketOK && ticketInfo.UseNewKey {
+ _, 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 resolveM3U8RateLimitUserId(c *gin.Context, ticketInfo *H5M3U8Ticket) int64 {
+ if ticketInfo != nil && ticketInfo.UserId > 0 {
+ return ticketInfo.UserId
+ }
+ if c == nil {
+ return 0
+ }
+ return common.GetUserId(c)
+}
+
+// isM3U8TicketVipValid 用票据内签发时写入的会员过期时间判断,播放热路径不查库。
+func isM3U8TicketVipValid(ticketInfo *H5M3U8Ticket) bool {
+ return ticketInfo != nil && ticketInfo.UserId > 0 && ticketInfo.VipExpireAt > time.Now().Unix()
+}
+
+func m3u8DailyMediaCountByIP(ip string, mediaID int64) (int64, error) {
+ return m3u8DailyPlayableCount(
+ redisconst.M3U8DayRateLimitIpKey(ip),
+ redisconst.M3U8DailyMediaIpKey(ip, mediaID),
+ )
+}
+
+func m3u8DailyMediaCountByUser(userId int64, mediaID int64) (int64, error) {
+ return m3u8DailyPlayableCount(
+ redisconst.M3U8DayRateLimitUserKey(userId),
+ redisconst.M3U8DailyMediaUserKey(userId, mediaID),
+ )
+}
+
+func m3u8DailyPlayableCount(countKey, firstMediaKey string) (int64, error) {
+ expire := redisconst.M3U8DayRateLimitExpire()
+ first, err := redis.Setnx_NewOK(firstMediaKey, 1, expire)
+ if err != nil {
+ return 0, err
+ }
+ 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, userId int64) string {
+ _ = ua
+ if signed := BuildH5M3U8LegacyURL(videoId, urlPath, ip, true, false, userId); signed != "" {
+ return signed
+ }
+ return urlPath
+}
+
+// GetPlayletM3U8AuthParam 为短剧剧集地址增加票据,并指定是否使用原片新密钥。
+func GetPlayletM3U8AuthParam(chapterID int64, urlPath, ip string, useNewKey bool, userId int64) string {
+ if signed := BuildH5M3U8PlayletURL(chapterID, urlPath, ip, useNewKey, userId); signed != "" {
+ return signed
+ }
+ return urlPath
+}
+
+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 serr := m3u8requestlogmod.InsertOneM3U8RequestLog(c, data); serr != 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..6f0dc76
--- /dev/null
+++ b/app/service/videoser/video.go
@@ -0,0 +1,484 @@
+package videoser
+
+import (
+ "context"
+ "regexp"
+ "strconv"
+ "strings"
+ "videoweb/app/api/proto"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/richtextcontentmod"
+ "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 (
+ videoListDefaultPageSize int64 = 10
+ videoRecommendLimit int64 = 20
+ videoMaxPageSize int64 = 100
+ // videoH5DetailPath 视频详情Web/H5页面路径,供SEO canonical使用
+ videoH5DetailPath = "/lieyan/av/detail"
+)
+
+var (
+ // videoListProjection 只读取列表需要展示的字段,降低MongoDB IO和网络传输
+ videoListProjection = bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.CreatorId, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.CreatorName, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Keyworld, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Fake.WatchTimes, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.VideoTime, Value: 1},
+ }
+
+ // videoListSort 按上架时间和ID倒序,保证普通分页排序稳定
+ videoListSort = bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+
+ videoListHint = bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Status, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+
+ videoRecommendHint = bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Status, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Tags, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
+ }
+
+ // videoInfoProjection 只读取视频详情接口需要展示和签名的字段
+ videoInfoProjection = bson.D{
+ {Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.CoverImg, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Keyworld, 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.Fake.WatchTimes, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.TaskId, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.FileId, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.Url, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.H265Url, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.Cover, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.Height, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.Width, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.VideoTime, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.PreMp4FileNameFs, Value: 1},
+ {Key: videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4, Value: 1},
+ }
+)
+
+type CategoryDataStruct struct {
+ VideoInfoData []proto.VideoInfoData
+ CategoryData proto.VideoCategoryInfo
+ VideoCount int64
+}
+
+type PublishDataStruct struct {
+ VideoInfoData []proto.VideoInfoData
+ CreaterData proto.VideoCreaterInfo
+ VideoCount int64
+}
+
+// VideoList 按上架时间倒序普通分页查询已上架视频
+func VideoList(ctx context.Context, req *proto.VideoReq) (*proto.VideoResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ pageNum, pageSize := normalizeVideoPage(req.PageNum, req.PageSize, videoListDefaultPageSize)
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
+ }
+ totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter, options.Count().SetHint(videoListHint))
+ if err != nil {
+ return nil, err
+ }
+ if totalCount == 0 {
+ return emptyVideoPage(pageNum, pageSize), nil
+ }
+ skip := (pageNum - 1) * pageSize
+ if skip >= totalCount {
+ resp := emptyVideoPage(pageNum, pageSize)
+ resp.TotalCount = totalCount
+ return resp, nil
+ }
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
+ SetProjection(videoListProjection).
+ SetSort(videoListSort).
+ SetHint(videoListHint).
+ SetSkip(skip).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.VideoItemResp, 0, len(list))
+ imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
+ for _, item := range list {
+ items = append(items, videoToListResp(item, imgCdn))
+ }
+ return &proto.VideoResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: items,
+ }, nil
+}
+
+// VideoInfo 查询视频详情
+func VideoInfo(ctx context.Context, req *proto.VideoInfoReq, userId int64, ip, ua string) (*proto.VideoInfoResp, *srvdef.SrvErr) {
+ if req == nil || req.Id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Id: req.Id,
+ videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
+ }
+ item, err := videoinformationmod.FindOneVideoInformationWithOptions(ctx, filter, options.FindOne().
+ SetProjection(videoInfoProjection))
+ if err != nil {
+ return nil, err
+ }
+ go IncWatchTimes(context.Background(), item.Id)
+ imgCdn := configser.GetAppImageCdn(ip)
+ resp := videoToInfoResp(item, imgCdn, ip, ua, true, userId)
+ recommendVideos, err := queryRecommendVideos(ctx, item, imgCdn)
+ if err != nil {
+ return nil, err
+ }
+ resp.RecommendVideos = recommendVideos
+ resp.Advitise = configser.GetAdvitise()
+ resp.SeoInfo = buildVideoDetailSeoInfo(item)
+ return resp, nil
+}
+
+// queryRecommendVideos 按当前视频标签查询相关推荐
+func queryRecommendVideos(ctx context.Context, current *videoinformationmod.VideoInformation, imgCdn string) ([]*proto.VideoItemResp, *srvdef.SrvErr) {
+ if current == nil || len(current.Tags) == 0 {
+ return make([]*proto.VideoItemResp, 0), nil
+ }
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
+ videoinformationmod.FN.VideoInformation.Tags: bson.M{"$in": current.Tags},
+ videoinformationmod.FN.VideoInformation.Id: bson.M{"$ne": current.Id},
+ }
+ list, err := videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
+ SetProjection(videoListProjection).
+ SetSort(videoListSort).
+ SetHint(videoRecommendHint).
+ SetLimit(videoRecommendLimit))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.VideoItemResp, 0, len(list))
+ for _, item := range list {
+ items = append(items, videoToListResp(item, imgCdn))
+ }
+ return items, nil
+}
+
+// normalizeVideoPage 规范化视频普通分页参数,限制单次查询量保护接口和数据库
+func normalizeVideoPage(pageNum, pageSize, defaultPageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultPageSize
+ }
+ if pageSize > videoMaxPageSize {
+ pageSize = videoMaxPageSize
+ }
+ return pageNum, pageSize
+}
+
+func emptyVideoPage(pageNum, pageSize int64) *proto.VideoResp {
+ return &proto.VideoResp{
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.VideoItemResp, 0),
+ }
+}
+
+// videoToListResp 将视频模型转换为列表项
+func videoToListResp(item *videoinformationmod.VideoInformation, imgCdn string) *proto.VideoItemResp {
+ return &proto.VideoItemResp{
+ Id: item.Id,
+ Title: item.Title,
+ AddedTime: item.AddedTime,
+ CreatorId: item.CreatorId,
+ CreatorName: item.CreatorName,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Keyworld: item.Keyworld,
+ Tags: item.Tags,
+ WatchTimes: item.Real.WatchTimes + item.Fake.WatchTimes,
+ VideoTime: item.Video.VideoTime,
+ }
+}
+
+// videoToInfoResp 将视频模型转换为详情响应,并给视频播放地址增加鉴权参数
+func videoToInfoResp(item *videoinformationmod.VideoInformation, imgCdn, ip, ua string, includeCurrentWatch bool, userId int64) *proto.VideoInfoResp {
+ video := proto.VideoData{
+ TaskId: item.Video.TaskId,
+ FileId: item.Video.FileId,
+ Url: item.Video.Url,
+ Cover: netutil.MergeURL(imgCdn, item.Video.Cover),
+ Height: item.Video.Height,
+ Width: item.Video.Width,
+ VideoTime: item.Video.VideoTime,
+ PreMp4FileNameFs: item.Video.PreMp4FileNameFs,
+ UnencryptedPreMp4: item.Video.UnencryptedPreMp4,
+ }
+ if video.Url != "" {
+ video.Url = GetM3U8AuthParam(item.Id, video.Url, ip, ua, userId)
+ }
+ watchTimes := item.Real.WatchTimes + item.Fake.WatchTimes
+ if includeCurrentWatch {
+ watchTimes++
+ }
+ resp := &proto.VideoInfoResp{
+ Id: item.Id,
+ Title: item.Title,
+ CoverImg: netutil.MergeURL(imgCdn, item.CoverImg),
+ Keyworld: item.Keyworld,
+ Tags: item.Tags,
+ TagsName: make([]string, 0, len(item.Tags)),
+ WatchTimes: watchTimes,
+ Video: video,
+ }
+ for _, tagId := range item.Tags {
+ tagInfo := configser.GetTag(tagId)
+ if tagInfo != nil {
+ resp.TagsName = append(resp.TagsName, tagInfo.Name)
+ }
+ }
+ return resp
+}
+
+// buildVideoDetailSeoInfo 组装视频详情页SEO信息
+//
+// canonical 统一指向Web/H5真实详情页:/lieyan/av/detail?id=视频ID
+func buildVideoDetailSeoInfo(item *videoinformationmod.VideoInformation) *proto.SeoInfo {
+ seo := configser.GetSiteSeoInfo()
+ if item == nil {
+ return &seo
+ }
+ if title := strings.TrimSpace(item.Title); title != "" {
+ seo.Title = title
+ }
+ if desc := truncateVideoSEODescription(strings.TrimSpace(item.Desc), 120); desc != "" {
+ seo.Description = desc
+ }
+ seo.Keywords = buildVideoSEOKeywords(item, seo.Keywords)
+ seo.CanonicalURL = buildVideoDetailCanonicalURL(seo.CanonicalURL, item.Id)
+ return &seo
+}
+
+// buildVideoDetailCanonicalURL 拼接视频详情页标准URL
+func buildVideoDetailCanonicalURL(baseURL string, id int64) string {
+ pagePath := videoH5DetailPath + "?id=" + strconv.FormatInt(id, 10)
+ baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
+ if baseURL == "" {
+ return pagePath
+ }
+ return baseURL + pagePath
+}
+
+// buildVideoSEOKeywords 合并视频关键词和全站关键词,并去重
+func buildVideoSEOKeywords(item *videoinformationmod.VideoInformation, siteKeywords []string) []string {
+ seen := make(map[string]struct{}, len(item.Keyworld)+len(siteKeywords)+1)
+ keywords := make([]string, 0, len(item.Keyworld)+len(siteKeywords)+1)
+ add := func(value string) {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ return
+ }
+ if _, ok := seen[value]; ok {
+ return
+ }
+ seen[value] = struct{}{}
+ keywords = append(keywords, value)
+ }
+ add(item.Title)
+ for _, keyword := range item.Keyworld {
+ add(keyword)
+ }
+ for _, keyword := range siteKeywords {
+ add(keyword)
+ }
+ return keywords
+}
+
+// truncateVideoSEODescription 限制meta description长度,避免详情长文本直接写进SEO描述
+func truncateVideoSEODescription(desc string, maxRunes int) string {
+ if maxRunes <= 0 || desc == "" {
+ return ""
+ }
+ runes := []rune(desc)
+ if len(runes) <= maxRunes {
+ return desc
+ }
+ return string(runes[:maxRunes])
+}
+
+func VideoDetails(ctx context.Context, videoId int64, userId int64, ip, ua string) (resp proto.VideoInfoData, serr *srvdef.SrvErr) {
+ imgCdn := configser.GetAppImageCdn(ip)
+ videoFilter := bson.M{
+ videoinformationmod.FN.VideoInformation.Id: videoId,
+ videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
+ }
+
+ videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, videoFilter)
+ if serr != nil {
+ log.Error("VideoDetails", log.Any("videoFilter", videoFilter), log.E(serr))
+ return
+ }
+ // 查询分类
+
+ resp = toVideoInfoData(videoInfo, imgCdn)
+ for _, tagId := range videoInfo.Tags {
+ tagInfo := configser.GetTag(tagId)
+ if tagInfo != nil {
+ resp.Tags = append(resp.Tags, proto.TagsInfoData{
+ Id: tagInfo.Id,
+ Name: tagInfo.Name,
+ })
+ }
+ }
+ if resp.WatchTimes > 0 {
+ resp.WatchTimes = resp.WatchTimes * 100
+ } else {
+ resp.WatchTimes = utils.GetRandom(2000, 10000)
+ }
+ if resp.Content != "" {
+ re := regexp.MustCompile(`data-src="([^"]+?\.m3u8)"`)
+ // 查找所有匹配项
+ matches := re.FindAllStringSubmatch(resp.Content, -1)
+ for _, match := range matches {
+ newUrl := GetM3U8AuthParam(videoId, match[1], ip, ua, userId)
+ resp.Content = strings.ReplaceAll(resp.Content, match[1], newUrl)
+ }
+ }
+
+ if len(resp.Video.Url) > 0 || len(resp.Nodes) > 0 {
+ applyVideoUrlAuth(&resp, videoId, ip, ua, userId)
+ }
+ go func() {
+ _ = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: videoId,
+ }, bson.M{"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.ShowTimes: 1}})
+ if len(videoInfo.Tags) > 0 {
+ tagIds := videoInfo.Tags
+ if len(tagIds) > 10 {
+ tagIds = tagIds[:10]
+ }
+ _ = videotagmod.UpdateManyVideoTagInfo(ctx, bson.M{
+ videotagmod.FN.VideoTagInfo.Id: bson.M{"$in": tagIds},
+ }, bson.M{"$inc": bson.M{
+ videotagmod.FN.VideoTagInfo.WatchTimes: 1,
+ }})
+ }
+ }()
+ return resp, nil
+}
+
+func RichtextData(ctx context.Context, richtextId int64) (proto.RichTextInfo, *srvdef.SrvErr) {
+ //cacheKey := fmt.Sprintf("rich_text_data_%d", richtextId)
+ var resp *proto.RichTextInfo
+ //if cacheData, exists := Cache.Get(cacheKey); exists {
+ // if item, ok := cacheData.(proto.RichTextInfo); ok {
+ // resp = &item
+ // }
+ //}
+ //if resp == nil || resp.Id == 0 {
+ filter := bson.M{
+ richtextcontentmod.FN.RichTextContentModel.Id: richtextId,
+ }
+
+ richtextInfo, serr := richtextcontentmod.FindOneRichTextContentModelModel(ctx, filter)
+ if serr != nil {
+ log.Error("VideoTagData", log.Any("videoFilter", filter), log.E(serr))
+ return proto.RichTextInfo{}, serr
+ }
+ data := &proto.RichTextInfo{
+ Id: richtextInfo.Id,
+ Title: richtextInfo.Title,
+ Content: richtextInfo.Content,
+ Desc: richtextInfo.Desc,
+ SeoInfo: richtextInfo.SeoInfo,
+ }
+ resp = data
+ // Cache.Set(cacheKey, data, 17*time.Minute)
+ //}
+
+ return *resp, nil
+}
+
+func IncWatchTimes(ctx context.Context, videoId int64) {
+ _ = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
+ videoinformationmod.FN.VideoInformation.Id: videoId,
+ }, bson.M{"$inc": bson.M{
+ videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
+ }})
+}
+
+func VideoTopicData(ctx context.Context, isAv bool, topicId int64, sortType int64, pageNum int64) (videoCount int64, videoInfos []proto.VideoInfoData, sortItem videoinformationmod.VideoSortItem, err *srvdef.SrvErr) {
+ imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
+ videoCount = int64(0)
+ sort := videoinformationmod.VideoSortType(sortType)
+ sortItem = sort.GetSortItem()
+ videoInfos = make([]proto.VideoInfoData, 0)
+
+ filter := bson.M{
+ videoinformationmod.FN.VideoInformation.MediaType: videoinformationmod.MediaType_VideoV2,
+ videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
+ }
+ opts := &options.FindOptions{}
+ opts.SetLimit(pubdef.PageSize).SetSkip((pageNum - 1) * pubdef.PageSize).SetSort(sort.GetSortBson())
+ videoCount, _ = videoinformationmod.CountVideoInformations(ctx, filter)
+ if videoCount == 0 {
+ return
+ }
+ videoList, err := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if err != nil {
+ log.Warn("VideoTopicData FindManyVideoInfomations", log.Any("filter", filter), log.E(err))
+ return
+ }
+
+ for i := 0; i < len(videoList); i++ {
+ info := toVideoInfoData(videoList[i], imgCdn)
+ for _, tid := range videoList[i].Tags {
+ tagInfo := configser.GetTag(tid)
+ if tagInfo != nil {
+ info.Tags = append(info.Tags, proto.TagsInfoData{
+ Id: tagInfo.Id,
+ Name: tagInfo.Name,
+ })
+ }
+ }
+ if info.WatchTimes > 0 {
+ info.WatchTimes = info.WatchTimes * 100
+ } else {
+ info.WatchTimes = utils.GetRandom(2000, 10000)
+ }
+ videoInfos = append(videoInfos, info)
+ }
+
+ return
+}
diff --git a/app/service/videoser/videocategory.go b/app/service/videoser/videocategory.go
new file mode 100644
index 0000000..3b7cb00
--- /dev/null
+++ b/app/service/videoser/videocategory.go
@@ -0,0 +1,15 @@
+package videoser
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videocategorymod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func GetVideoCategoryById(ctx context.Context, id int64) (*videocategorymod.VideoCategory, *srvdef.SrvErr) {
+ return videocategorymod.FindOneVideoCategory(ctx, bson.M{
+ videocategorymod.FN.VideoCategory.Id: id,
+ })
+}
diff --git a/app/service/videoser/videocreator.go b/app/service/videoser/videocreator.go
new file mode 100644
index 0000000..347550a
--- /dev/null
+++ b/app/service/videoser/videocreator.go
@@ -0,0 +1,15 @@
+package videoser
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videocreatermod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func GetVideoCreatorById(ctx context.Context, id int64) (*videocreatermod.VideoCreaterInfo, *srvdef.SrvErr) {
+ return videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Id: id,
+ })
+}
diff --git a/app/service/videoser/videotag.go b/app/service/videoser/videotag.go
new file mode 100644
index 0000000..641ed9f
--- /dev/null
+++ b/app/service/videoser/videotag.go
@@ -0,0 +1,15 @@
+package videoser
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videotagmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func GetVideoTagById(ctx context.Context, id int64) (*videotagmod.VideoTagInfo, *srvdef.SrvErr) {
+ return videotagmod.FindOneVideoTagInfo(ctx, bson.M{
+ videotagmod.FN.VideoTagInfo.Id: id,
+ })
+}
diff --git a/app/service/videoser/videotopic.go b/app/service/videoser/videotopic.go
new file mode 100644
index 0000000..7825082
--- /dev/null
+++ b/app/service/videoser/videotopic.go
@@ -0,0 +1,15 @@
+package videoser
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videotopicmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func GetVideoTopicById(ctx context.Context, id int64) (*videotopicmod.VideoTopicModel, *srvdef.SrvErr) {
+ return videotopicmod.FindOneVideoTopicModel(ctx, bson.M{
+ videotopicmod.FN.VideoTopicModel.Id: id,
+ })
+}
diff --git a/app/service/videoser/vtt.go b/app/service/videoser/vtt.go
new file mode 100644
index 0000000..2478088
--- /dev/null
+++ b/app/service/videoser/vtt.go
@@ -0,0 +1,158 @@
+package videoser
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ http2 "net/http"
+ "strings"
+ "videoweb/app/appg"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/http"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/m3u8"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/redis"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetVTTEx(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ vttName := ctx.Param("key")
+ var req m3u8.Req
+ if err = ctx.BindQuery(&req); err != nil {
+ return
+ }
+
+ req.Name = vttName
+ cfg := configser.GetConfigInfo()
+ cdnDomain := cfg.ImgCdn
+ originDomain := appg.Conf.OrigDomain
+
+ buff, err := TransformVttEx(req.Name, cdnDomain, originDomain)
+ if err != nil {
+ return
+ }
+ contentType = "application/vnd.apple.mpegurl"
+ reader = bytes.NewReader(buff)
+ contentLength = int64(len(buff))
+ extraHeaders = map[string]string{}
+ return
+ }
+ contentLength, contentType, reader, extraHeaders, err := fn(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, srvdef.DownloadM3u8File, err)
+ return
+ }
+ ctx.DataFromReader(http2.StatusOK, contentLength, contentType, reader, extraHeaders)
+}
+
+func TransformVttEx(name, cdnDomain, originDomain string) (buff []byte, err error) {
+ if name == "" {
+ err = errors.MakeWithMsg(errors.M3u8FileName, "m3u8 file name is empty")
+ return
+ }
+ if cdnDomain == "" || originDomain == "" {
+ err = errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
+ return
+ }
+ // 处理媒资库 桶1(v1) 桶2(v2) 桶3(v3)
+ mediaVerson := proxy.AppendsMediaS3Version(name)
+ if len(mediaVerson) > 0 {
+ name = mediaVerson + name
+ }
+
+ // 原始vtt
+ srcReader, err := readFromRedis(name, originDomain)
+ if err != nil {
+ return
+ }
+
+ raw, err := io.ReadAll(srcReader)
+ if err != nil {
+ return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt reader fail: "+err.Error())
+ }
+ // 替换为cdn路径
+ newData := rewriteVttWithCDN(raw, cdnDomain)
+ reader := bytes.NewReader(newData)
+ buff, err = io.ReadAll(reader)
+ if err != nil {
+ return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt fail: "+err.Error())
+ }
+ return
+}
+
+// 从redis缓存中读取
+func readFromRedis(name, domain string) (reader io.Reader, err error) {
+ vttKey := redisconst.MediaM3U8Key(name)
+ data, err := redis.GetBytes(vttKey)
+ if err == nil && len(data) > 0 {
+ reader = bytes.NewReader(data)
+ return
+ }
+ requestUrl := utils.JoinUrlPath(domain, name)
+ resp, err := http.GetLargeFile(requestUrl, nil)
+ if err != nil {
+ fmt.Println("requestUrl111", requestUrl)
+ return
+ }
+ fmt.Println("requestUrl", requestUrl, resp.StatusCode)
+ if resp.StatusCode != 200 {
+ msg := fmt.Sprintf("read vtt fail, url: %s, status code %d", requestUrl, resp.StatusCode)
+ err = errors.MakeWithMsg(errors.M3u8Read, msg)
+ return
+ }
+ if err := redis.Set(vttKey, resp.Data,
+ redisconst.MediaM3U8CacheExpire); err != nil {
+ log.Info("vtt:readFromRedis redis set err", log.E(err))
+ }
+ reader = bytes.NewReader(resp.Data)
+ return
+}
+
+// rewriteVttWithCDN 将 vtt 内 "path#xywh=..." 的 path 改为 cdnDomain/path
+func rewriteVttWithCDN(vttData []byte, cdnDomain string) []byte {
+ cdnDomain = strings.TrimSpace(cdnDomain)
+ cdnDomain = strings.TrimRight(cdnDomain, "/")
+
+ var out bytes.Buffer
+ scanner := bufio.NewScanner(bytes.NewReader(vttData))
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ trim := strings.TrimSpace(line)
+
+ // 只处理缩略图行:xxx#xywh=...
+ if strings.Contains(trim, "#xywh=") {
+ parts := strings.SplitN(trim, "#xywh=", 2)
+ if len(parts) == 2 {
+ p := strings.TrimSpace(parts[0]) // 原路径
+ xywh := parts[1]
+
+ // 已经是绝对URL则不改
+ if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") {
+ out.WriteString(p + "#xywh=" + xywh + "\n")
+ continue
+ }
+
+ // 处理相对路径,确保只有一个 /
+ p = strings.TrimLeft(p, "/")
+ newPath := cdnDomain + "/" + p
+ out.WriteString(newPath + "#xywh=" + xywh + "\n")
+ continue
+ }
+ }
+
+ out.WriteString(line + "\n")
+ }
+
+ // scanner.Err()
+ return out.Bytes()
+}
diff --git a/app/skd/ipstat/sync.go b/app/skd/ipstat/sync.go
new file mode 100644
index 0000000..3a7b00d
--- /dev/null
+++ b/app/skd/ipstat/sync.go
@@ -0,0 +1,49 @@
+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
+
+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
+ }
+
+ 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/sitemap/data.go b/app/skd/job/sitemap/data.go
new file mode 100644
index 0000000..04b7839
--- /dev/null
+++ b/app/skd/job/sitemap/data.go
@@ -0,0 +1,323 @@
+package sitemap
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type idWithTime struct {
+ Id int64
+ Slug string
+ LastMod time.Time
+}
+
+type staticPageEntry struct {
+ path string
+ changeFreq string
+ priority float32
+}
+
+func buildStaticURLs(webAddr string, entries []staticPageEntry) []URL {
+ urls := make([]URL, 0, len(entries))
+ now := time.Now().Format(time.DateOnly)
+ for _, entry := range entries {
+ urls = append(urls, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, entry.path),
+ LastMod: now,
+ ChangeFreq: entry.changeFreq,
+ Priority: entry.priority,
+ })
+ }
+ return urls
+}
+
+func entityPath(prefix, slug string, id int64) string {
+ if segment := utils.PathSegment(slug, id); segment != "" {
+ return strings.TrimSuffix(prefix, "/") + "/" + segment + "/"
+ }
+ if strings.HasSuffix(prefix, "/") {
+ return prefix
+ }
+ return prefix + "/"
+}
+
+func hgdjStaticPages() []staticPageEntry {
+ return []staticPageEntry{
+ {path: "/", changeFreq: "daily", priority: 1.0},
+ {path: "/list/ai-short-play/", changeFreq: "daily", priority: 0.9},
+ {path: "/list/ai-short-play/popular/", changeFreq: "daily", priority: 0.8},
+ {path: "/list/ai-short-play/original/", changeFreq: "daily", priority: 0.8},
+ {path: "/series/", changeFreq: "daily", priority: 0.9},
+ {path: "/series/popular/", changeFreq: "daily", priority: 0.8},
+ {path: "/series/original/", changeFreq: "daily", priority: 0.8},
+ {path: "/rank/", changeFreq: "daily", priority: 0.8},
+ {path: "/today-update/", changeFreq: "daily", priority: 0.8},
+ {path: "/topic/", changeFreq: "daily", priority: 0.8},
+ {path: "/application/", changeFreq: "weekly", priority: 0.6},
+ {path: "/home-road/", changeFreq: "weekly", priority: 0.6},
+ {path: "/download-app/", changeFreq: "monthly", priority: 0.5},
+ {path: "/nav/", changeFreq: "monthly", priority: 0.5},
+ {path: "/about-us/", changeFreq: "yearly", priority: 0.3},
+ {path: "/help-center/", changeFreq: "yearly", priority: 0.3},
+ }
+}
+
+// getURL 并发拉取短剧数据并写入 sitemap 分片,URL 规则对齐 jha-1043-hgdj-web。
+func getURL(sg *SitemapGenerator) {
+ webAddr := strings.TrimSpace(sg.WebAddr)
+ if webAddr == "" {
+ return
+ }
+
+ ctx := context.Background()
+ var (
+ wg sync.WaitGroup
+ listed listedPlaylets
+ themeItems []idWithTime
+ tagItems []idWithTime
+ categoryItems []idWithTime
+ )
+
+ wg.Add(4)
+ go func() {
+ defer wg.Done()
+ listed = getPlayletList(ctx)
+ }()
+ go func() {
+ defer wg.Done()
+ themeItems = getThemeList(ctx)
+ }()
+ go func() {
+ defer wg.Done()
+ tagItems = getTagList(ctx)
+ }()
+ go func() {
+ defer wg.Done()
+ categoryItems = getCategoryList(ctx)
+ }()
+ wg.Wait()
+
+ // 专题/标签没有上架短剧时不写入,避免空列表页被收录。
+ // 分类页无绑定短剧时会回退展示全部上架短剧,因此启用分类都写入。
+ themeItems = keepWithListedPlaylets(themeItems, listed.themeIDs)
+ tagItems = keepWithListedPlaylets(tagItems, listed.tagIDs)
+
+ pageURLs := buildStaticURLs(webAddr, hgdjStaticPages())
+ for _, item := range themeItems {
+ pageURLs = append(pageURLs, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/topic", item.Slug, item.Id)),
+ LastMod: formatLastMod(item.LastMod),
+ ChangeFreq: "daily",
+ Priority: 0.8,
+ })
+ }
+ for _, item := range categoryItems {
+ pageURLs = append(pageURLs, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/category", item.Slug, item.Id)),
+ LastMod: formatLastMod(item.LastMod),
+ ChangeFreq: "daily",
+ Priority: 0.8,
+ })
+ }
+ for _, item := range tagItems {
+ pageURLs = append(pageURLs, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath("/tag", item.Slug, item.Id)),
+ LastMod: formatLastMod(item.LastMod),
+ ChangeFreq: "weekly",
+ Priority: 0.6,
+ })
+ }
+ if len(pageURLs) > 0 {
+ _ = sg.writeSitemapFile("sitemap-pages.xml", pageURLs)
+ }
+
+ if urls := buildPlayletPathURLs(webAddr, listed.items, "/detail", "weekly", 0.7); len(urls) > 0 {
+ _ = sg.writeSitemapFiles("sitemap-playletdetail", urls)
+ }
+ if urls := buildPlayletPathURLs(webAddr, listed.items, "/play", "weekly", 0.6); len(urls) > 0 {
+ _ = sg.writeSitemapFiles("sitemap-playletplay", urls)
+ }
+}
+
+type listedPlaylets struct {
+ items []idWithTime
+ themeIDs map[int64]struct{}
+ tagIDs map[int64]struct{}
+ categoryIDs map[int64]struct{}
+}
+
+func keepWithListedPlaylets(items []idWithTime, listed map[int64]struct{}) []idWithTime {
+ if len(items) == 0 || len(listed) == 0 {
+ return nil
+ }
+ out := make([]idWithTime, 0, len(items))
+ for _, item := range items {
+ if _, ok := listed[item.Id]; ok {
+ out = append(out, item)
+ }
+ }
+ return out
+}
+
+func addListedIDs(dst map[int64]struct{}, ids []int64) {
+ for _, id := range ids {
+ if id > 0 {
+ dst[id] = struct{}{}
+ }
+ }
+}
+
+func buildPlayletPathURLs(webAddr string, items []idWithTime, prefix string, changeFreq string, priority float32) []URL {
+ urls := make([]URL, 0, len(items))
+ for _, item := range items {
+ lastMod := formatLastMod(item.LastMod)
+ if lastMod == "" {
+ lastMod = time.Now().Format(time.DateOnly)
+ }
+ urls = append(urls, URL{
+ Loc: utils.JoinUrlPathKeepSlash(webAddr, entityPath(prefix, item.Slug, item.Id)),
+ LastMod: lastMod,
+ ChangeFreq: changeFreq,
+ Priority: priority,
+ })
+ }
+ return urls
+}
+
+func formatLastMod(t time.Time) string {
+ if t.IsZero() {
+ return ""
+ }
+ return t.Format(time.DateOnly)
+}
+
+func getPlayletList(ctx context.Context) listedPlaylets {
+ result := listedPlaylets{
+ items: make([]idWithTime, 0),
+ themeIDs: make(map[int64]struct{}),
+ tagIDs: make(map[int64]struct{}),
+ categoryIDs: make(map[int64]struct{}),
+ }
+ filter := bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }
+ pageSize := int64(1000)
+ 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.ThemeIds: 1,
+ playletmod.FN.Playlet.TagIds: 1,
+ playletmod.FN.Playlet.CategoryIds: 1,
+ }))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 {
+ continue
+ }
+ lastMod := info.UpdatedAt
+ if lastMod.IsZero() {
+ lastMod = info.OnlineAt
+ }
+ result.items = append(result.items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: lastMod})
+ addListedIDs(result.themeIDs, info.ThemeIds)
+ addListedIDs(result.tagIDs, info.TagIds)
+ addListedIDs(result.categoryIDs, info.CategoryIds)
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return result
+}
+
+func getThemeList(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++ {
+ skip := i * pageSize
+ list, _ := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
+ SetSort(bson.M{playletthememod.FN.PlayletTheme.Rank: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 || strings.TrimSpace(info.Name) == "" {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items
+}
+
+func getTagList(ctx context.Context) []idWithTime {
+ filter := bson.M{
+ playlettagmod.FN.PlayletTag.Status: true,
+ }
+ pageSize := int64(1000)
+ items := make([]idWithTime, 0)
+ for i := int64(0); i < 100; i++ {
+ skip := i * pageSize
+ list, _ := playlettagmod.FindManyPlayletTag(ctx, filter, options.Find().
+ SetSort(bson.M{playlettagmod.FN.PlayletTag.Rank: -1}).
+ SetSkip(skip).
+ SetLimit(pageSize))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 || strings.TrimSpace(info.Name) == "" {
+ continue
+ }
+ items = append(items, idWithTime{Id: info.Id, Slug: info.Slug, LastMod: info.UpdatedAt})
+ }
+ if len(list) < int(pageSize) {
+ break
+ }
+ }
+ return items
+}
+
+func getCategoryList(ctx context.Context) []idWithTime {
+ 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))
+ for _, info := range list {
+ if info == nil || info.Id <= 0 || strings.TrimSpace(info.Name) == "" {
+ 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/generate.go b/app/skd/job/sitemap/generate.go
new file mode 100644
index 0000000..9bc9aba
--- /dev/null
+++ b/app/skd/job/sitemap/generate.go
@@ -0,0 +1,35 @@
+package sitemap
+
+import (
+ "strings"
+ "sync"
+ "videoweb/app/service/configser"
+ "videoweb/pub/common/log"
+)
+
+var generateMu sync.Mutex
+
+// GenerateSitemap 生成全站 sitemap 分片及索引文件。
+func GenerateSitemap() {
+ generateMu.Lock()
+ defer generateMu.Unlock()
+
+ webAddr := strings.TrimSpace(configser.GetConfigInfo().WebAddr)
+ if webAddr == "" {
+ log.Warn("GenerateSitemap: WebAddr is empty, skip")
+ return
+ }
+
+ generator := NewSitemapGenerator(webAddr, OutputDir)
+ if err := generator.Generate(); err != nil {
+ log.Error("GenerateSitemap failed", log.E(err))
+ return
+ }
+ if err := writeRobotsTXT(webAddr); err != nil {
+ log.Error("GenerateSitemap write robots.txt failed", log.E(err))
+ return
+ }
+ log.Info("GenerateSitemap done",
+ log.Any("files", len(generator.SitemapFiles)),
+ log.Any("webAddr", webAddr))
+}
diff --git a/app/skd/job/sitemap/job.go b/app/skd/job/sitemap/job.go
new file mode 100644
index 0000000..b9fa695
--- /dev/null
+++ b/app/skd/job/sitemap/job.go
@@ -0,0 +1,35 @@
+package sitemap
+
+import (
+ "os"
+ "time"
+ "videoweb/pub/common/log"
+)
+
+func StartSitemapJob(interval time.Duration, outDir string) {
+ if interval <= 0 {
+ interval = 6 * time.Hour
+ }
+ if outDir == "" {
+ outDir = OutputDir
+ }
+
+ go func() {
+ if mkErr := os.MkdirAll(outDir, 0755); mkErr != nil {
+ log.Error("StartSitemapJob mkdir failed", log.E(mkErr))
+ return
+ }
+ if mkErr := os.MkdirAll(PageOutputDir, 0755); mkErr != nil {
+ log.Error("StartSitemapJob mkdir page dir failed", log.E(mkErr))
+ return
+ }
+
+ GenerateSitemap()
+
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+ for range ticker.C {
+ GenerateSitemap()
+ }
+ }()
+}
diff --git a/app/skd/job/sitemap/sitemap.go b/app/skd/job/sitemap/sitemap.go
new file mode 100644
index 0000000..dd28d43
--- /dev/null
+++ b/app/skd/job/sitemap/sitemap.go
@@ -0,0 +1,263 @@
+package sitemap
+
+import (
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ stylesheetIndex = `` + "\n"
+ stylesheetURLSet = `` + "\n"
+
+ defaultMaxURLsPerFile = 40000
+
+ // OutputDir 分片 sitemap 输出目录(相对 app 进程 cwd)
+ OutputDir = "../h5/templates/page/sitemaps"
+ // PageOutputDir sitemap 索引输出目录,供 /sitemap.xml 静态读取
+ PageOutputDir = "../h5/templates/page"
+)
+
+type URL struct {
+ Loc string `xml:"loc"`
+ LastMod string `xml:"lastmod"`
+ ChangeFreq string `xml:"changefreq"`
+ Priority float32 `xml:"priority"`
+}
+
+type Sitemap struct {
+ XMLName xml.Name `xml:"urlset"`
+ Xmlns string `xml:"xmlns,attr"`
+ URLs []URL `xml:"url"`
+}
+
+type SitemapIndex struct {
+ XMLName xml.Name `xml:"sitemapindex"`
+ Xmlns string `xml:"xmlns,attr"`
+ Sitemaps []SitemapIndexEntry `xml:"sitemap"`
+}
+
+type SitemapIndexEntry struct {
+ Loc string `xml:"loc"`
+ LastMod string `xml:"lastmod"`
+}
+
+type SitemapGenerator struct {
+ MaxURLsPerFile int
+ OutputDir string
+ WebAddr string
+ SitemapFiles []string
+}
+
+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
+}
+
+func (sg *SitemapGenerator) Generate() error {
+ sg.SitemapFiles = make([]string, 0)
+
+ if err := os.MkdirAll(sg.OutputDir, 0755); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(PageOutputDir, 0755); err != nil {
+ return err
+ }
+
+ getURL(sg)
+
+ if err := sg.generateSitemapIndex(); err != nil {
+ return err
+ }
+ return copySitemapIndexToPageRoot()
+}
+
+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()
+
+ if _, err := file.WriteString(xml.Header); err != nil {
+ return err
+ }
+ if _, err := file.WriteString(stylesheetURLSet); err != nil {
+ return err
+ }
+ encoder := xml.NewEncoder(file)
+ encoder.Indent("", " ")
+
+ if err := encoder.Encode(sitemap); err != nil {
+ return err
+ }
+
+ sg.SitemapFiles = append(sg.SitemapFiles, filename)
+ return nil
+}
+
+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
+}
+
+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))
+ }
+ }
+}
+
+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)
+ for _, filename := range sg.SitemapFiles {
+ index.Sitemaps = append(index.Sitemaps, SitemapIndexEntry{
+ Loc: fmt.Sprintf("%s/sitemaps/%s", strings.TrimSuffix(sg.WebAddr, "/"), filename),
+ LastMod: now,
+ })
+ }
+
+ filePath := filepath.Join(sg.OutputDir, "sitemap.xml")
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ if _, err := file.WriteString(xml.Header); err != nil {
+ return err
+ }
+ if _, err := file.WriteString(stylesheetIndex); err != nil {
+ return err
+ }
+ encoder := xml.NewEncoder(file)
+ encoder.Indent("", " ")
+
+ return encoder.Encode(index)
+}
+
+func copySitemapIndexToPageRoot() error {
+ src := filepath.Join(OutputDir, "sitemap.xml")
+ dst := filepath.Join(PageOutputDir, "sitemap.xml")
+ data, err := os.ReadFile(src)
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(dst, data, 0644)
+}
+
+func writeRobotsTXT(webAddr string) error {
+ body := buildRobotsTXT(webAddr)
+ if body == "" {
+ return nil
+ }
+ if err := os.MkdirAll(PageOutputDir, 0755); err != nil {
+ return err
+ }
+ return os.WriteFile(filepath.Join(PageOutputDir, "robots.txt"), []byte(body), 0644)
+}
+
+func buildRobotsTXT(webAddr string) string {
+ base := strings.TrimSuffix(strings.TrimSpace(webAddr), "/")
+ if base == "" {
+ return ""
+ }
+ body := "User-agent: *\nAllow: /\n\n" +
+ "# 非 HTML 接口与工具路由不抓取\n" +
+ "Disallow: /api/\n" +
+ "Disallow: /ui/\n" +
+ "Disallow: /healthz\n" +
+ "Disallow: /h5/\n" +
+ "Disallow: /enkey\n" +
+ "Disallow: /newenkey\n\n" +
+ "# 登录、搜索和账户 HTML 页面允许抓取,以便爬虫读取页面内 noindex。\n\n" +
+ "Sitemap: " + base + "/sitemap.xml\n"
+ if host := hostFromWebAddr(base); host != "" {
+ body += "Host: " + host + "\n"
+ }
+ return body
+}
+
+func hostFromWebAddr(webAddr string) string {
+ webAddr = strings.TrimSpace(webAddr)
+ webAddr = strings.TrimPrefix(webAddr, "https://")
+ webAddr = strings.TrimPrefix(webAddr, "http://")
+ if i := strings.IndexByte(webAddr, '/'); i >= 0 {
+ webAddr = webAddr[:i]
+ }
+ return webAddr
+}
diff --git a/app/skd/seo/seo.go b/app/skd/seo/seo.go
new file mode 100644
index 0000000..0f71e0e
--- /dev/null
+++ b/app/skd/seo/seo.go
@@ -0,0 +1,13 @@
+package seo
+
+import (
+ "time"
+ "videoweb/app/skd/job/sitemap"
+ "videoweb/pub/common/log"
+)
+
+// Start 启动 sitemap 生成定时任务。
+func Start() {
+ sitemap.StartSitemapJob(6*time.Hour, sitemap.OutputDir)
+ log.Info("seo skd started")
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..6f7ae32
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,122 @@
+module videoweb
+
+go 1.26
+
+toolchain go1.26.0
+
+require (
+ github.com/DeanThompson/ginpprof v0.0.0-20201112072838-007b1e56b2e1
+ github.com/PuerkitoBio/purell v1.1.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/gosimple/slug v1.15.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.0.68
+ 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.1
+ 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.46.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/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // 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.19.5 // indirect
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
+ github.com/go-openapi/spec v0.20.4 // indirect
+ github.com/go-openapi/swag v0.19.15 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/golang/protobuf v1.5.4 // 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/unidecode v1.0.1 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/josharian/intern v1.0.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/mailru/easyjson v0.7.6 // 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/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/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/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
+ golang.org/x/arch v0.20.0 // indirect
+ golang.org/x/net v0.48.0 // indirect
+ golang.org/x/oauth2 v0.34.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.42.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
+ golang.org/x/tools v0.39.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..9106ab4
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,530 @@
+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.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+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.3.2/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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+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.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+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.0.68 h1:HM+zMsS0iOwREnRKieB+RmK3Sgthwf1Kftgi3GxIp7U=
+github.com/nyaruka/phonenumbers v1.0.68/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
+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/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
+github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
+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/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/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=
+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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+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/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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
+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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+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-20210420072515-93ed5bcd2bfe/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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.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.6/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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
+golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+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-20200227125254-8fa46927fb4f/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.0-20200615113413-eeeca48fe776/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..eb3b7a1
--- /dev/null
+++ b/project.md
@@ -0,0 +1,85 @@
+---
+项目名称: 黄果短剧
+一级部门: 北斗
+二级部门: 瑞升
+
+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-1043-hgdj-server.git
+
+最后审计人:
+
+最后审计时间:
+
+告警群:
+
+备注:
+
+---
+
+# 1. 项目说明
+
+- 黄果短剧
+- app 包含前端API
+- web 包含管理后端API
+
+
+# 2. 基础设施与中间件
+
+- MongoDB 主从
+- Redis 单节点
+- Elasticsearch 搜索(v7)
+
+
+# 3. 运维部署
+
+- Shell脚本部署(scp + supervisorctl)
+- Telegram Bot 部署通知
+
+
+# 4. 风险说明
+
+- Redis 热Key风险
+
+
+# 5. 历史事故(可选)
+
+-
+
+
+# 6. 交接说明(可选)
+
+- appid 代码固定:1043
+- ES的索引KEY: webhgdj_video_index_v3
+- TrackerAppId 埋点APPID需要注意
+- /wbapi/hgdj api路径及名称
diff --git a/pub/adcenter/adcenter.go b/pub/adcenter/adcenter.go
new file mode 100644
index 0000000..ec2f789
--- /dev/null
+++ b/pub/adcenter/adcenter.go
@@ -0,0 +1,250 @@
+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,
+ }
+
+ var serverResp *JtAdvertiseRes
+ url := apiDomain + "/openapi/getAdvertiseList"
+ bodyStr, _ := json.Marshal(req)
+ headers := map[string]string{}
+ headers["Content-Type"] = "application/json"
+ code, err := httputil.Client().POSTJsonWithJResp(&serverResp, url, headers, bodyStr)
+ if err != nil {
+ log.Error("JtAdvertiseThreeServer POSTWithJResp ", log.Any("url", url), log.E(err))
+ // 从最后请求成功的一次拿数据
+ return getFromLastTime(foreverCacheKey, err)
+ }
+ if code != http.StatusOK {
+ log.Error("JtAdvertiseThreeServer response status ", log.Any("code", code))
+ // 从最后请求成功的一次拿数据
+ return getFromLastTime(foreverCacheKey, errors.New("response status err"))
+ }
+
+ if serverResp.Code != 0 {
+ log.Error("JtAdvertiseThreeServer response code ", log.Any("code", serverResp.Code), log.Any("msg", serverResp.Msg))
+ // 从最后请求成功的一次拿数据
+ return getFromLastTime(foreverCacheKey, errors.New("response code 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
+}
+
+// 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..4f87259
--- /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 // 删除一个媒体
+ MessageTypePostAdd // 添加一个帖子
+ MessageTypePostDel // 删除一个帖子
+)
+
+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..6e4139c
--- /dev/null
+++ b/pub/common/common.go
@@ -0,0 +1,173 @@
+package common
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+var (
+ cryptSecret string
+ env consts.RunMod
+ //routineCount int64
+)
+
+// 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()
+ 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 = ua.SysType
+ }
+ if devType != "" {
+ devType = ua.DevType
+ }
+ log.Warn("Error:",
+ log.Any("UID", strconv.FormatInt(id, 10)),
+ log.Any("IP", ipaddr.ResolveClientIP(c)),
+ 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()
+ 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 = ua.SysType
+ }
+ if devType != "" {
+ devType = ua.DevType
+ }
+ log.Warn("Error:",
+ log.Any("UID", strconv.FormatInt(id, 10)),
+ log.Any("IP", ipaddr.ResolveClientIP(c)),
+ 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 = ipaddr.ResolveClientIP(ctx)
+ }
+ }
+ 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..77cb7b7
--- /dev/null
+++ b/pub/common/enum/consts/consts.go
@@ -0,0 +1,128 @@
+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"
+ CtxSystemName = "SYSTEM_NAME"
+ CtxSystemVersion = "SYSTEM_VERSION"
+ 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
+}
diff --git a/pub/common/enum/redisconst/const.go b/pub/common/enum/redisconst/const.go
new file mode 100644
index 0000000..aed2c6d
--- /dev/null
+++ b/pub/common/enum/redisconst/const.go
@@ -0,0 +1,217 @@
+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
+ appPlayletHomeCacheFmt = "app:playlet:home:%d:%s"
+ appPlayletRecommendListFmt = "app:playlet:recommend:list:%d:%s:%d:%d:%d"
+ appPlayletHomeCacheKeyFmt = "app:playlet:home:%d"
+ appPlayletHomeCacheVersionFmt = "app:playlet:home:version:%d"
+ AppPlayletHomeCacheExpire = 10 * time.Minute
+ AppPlayletRecommendListExpire = 10 * time.Minute
+ AppPlayletForwardHeatExpire = 3 * time.Second
+
+ //禁止登陆设置
+ 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
+
+ // 访问 IP 去重统计:每个 appId 每天一个 SET
+ ipStatKey = "ipstat:%d:%s"
+ IpStatExpire = 49 * time.Hour
+
+ // m3u8 / enkey 防盗链
+ m3u8DayRateLimitIp = "m3u8dayrate:ip:%s:%s"
+ m3u8DayRateLimitUser = "m3u8dayrate:user:%d:%s"
+ m3u8DailyMediaIpFmt = "m3u8:daily:media:ip:%s:%d:%s"
+ m3u8DailyMediaUserFmt = "m3u8:daily:media:user:%d:%d:%s"
+ m3u8UserVipExpireFmt = "m3u8:user:vipExpire:%d"
+ M3U8DayRateLimitMax = int64(500) // 非有效会员/未登录:按 IP 日限
+ M3U8DayRateLimitVipMax = int64(1000) // 有效会员:按用户日限
+ M3U8UserVipExpireCache = 5 * time.Minute
+)
+
+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)
+}
+
+func MediaM3U8Key(name string) string {
+ return fmt.Sprintf(mediaM3U8Fmt, name)
+}
+
+func AppPlayletHomeCacheKey() string {
+ return fmt.Sprintf(appPlayletHomeCacheKeyFmt, pubdef.AppId)
+}
+
+func AppPlayletHomeCacheVersionKey() string {
+ return fmt.Sprintf(appPlayletHomeCacheVersionFmt, pubdef.AppId)
+}
+
+func AppPlayletHomeCacheDataKey(version string) string {
+ return fmt.Sprintf(appPlayletHomeCacheFmt, pubdef.AppId, version)
+}
+
+func AppPlayletRecommendListCacheKey(version string, categoryId int64, pageNum int64, pageSize int64) string {
+ return fmt.Sprintf(appPlayletRecommendListFmt, pubdef.AppId, version, categoryId, pageNum, pageSize)
+}
+
+func AppPlayletForwardHeatKey(operator string, playletId int64) string {
+ operator = strings.ReplaceAll(operator, ":", "-")
+ return fmt.Sprintf("app:playlet:forward:heat:%s:%d", operator, playletId)
+}
+
+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)
+}
+
+func IpStatKey(appId int64, date string) string {
+ return fmt.Sprintf(ipStatKey, appId, date)
+}
+
+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)
+}
+
+func M3U8DayRateLimitUserKey(userId int64) string {
+ loc, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ loc = time.Local
+ }
+ day := time.Now().In(loc).Format("20060102")
+ return fmt.Sprintf(m3u8DayRateLimitUser, userId, day)
+}
+
+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
+}
+
+func normalizeM3U8Ip(ip string) string {
+ return strings.ReplaceAll(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)
+}
+
+func M3U8DailyMediaUserKey(userId int64, 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(m3u8DailyMediaUserFmt, userId, mediaId, day)
+}
+
+func M3U8UserVipExpireKey(userId int64) string {
+ return fmt.Sprintf(m3u8UserVipExpireFmt, userId)
+}
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..6af873c
--- /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 //
+ PostsNewArgsInvalid // 帖子参数错误
+ PostDupi // 帖子重复发送
+ PostNotExisted // 帖子不存在
+ PostLikeOnLiked // 重复点赞帖子
+ PostUndoLikeOnUnliked // 尚未点赞,不可取消点赞
+ PostFavOnFaved // 重复 收藏
+ PostUndoFavOnUnfaved // 尚未收藏,不可取消收藏
+ PostDisLikeOnDisLiked // 重复屏蔽帖子
+ PostUndoDisLikeOnUnliked // 尚未屏蔽,不可取消屏蔽
+ UserDoNotForSelf // 用户不能对自己进行此项操作
+ UserWatchDupi // 不可重复关注同一用户
+ UserNoWatch // 没有关注该用户,不可取消关注
+ PostRepleNewArgsInvalid // 帖子 回复 参数错误
+ PostReplyNotExisted // 评论不存在
+ PostReplyAlready // 评论不存在
+ NoWatchableUsers // 没有可以关注的用户池
+ PostReplyLikeOnLiked // 重复点赞帖子
+ PostReplyUndoLikeOnUnliked // 尚未点赞,不可取消点赞
+ PostPriceInvalid // 帖子定价异常,不可购买
+ PostIsBuyed // 帖子已购买,不可重复购买
+ NoBalance // 余额不足
+ BalanceErr // 编辑余额错误
+ NoAccount // 没账户
+ AccountDisabled // 账户已被封禁
+ BuySelf // 自己帖子,不用购买
+ RedisError // 服务器繁忙
+ Button // 落地页按钮
+ ReplyTimeShort // 回复间隔时间太短
+ OperateLog // 日志出错
+ RoleErr // 角色权限不够
+ DocExists // 查找的文档已存在
+ PostUnknownError // 未知错误
+ 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..71625f2
--- /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
+}
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..16e8137
--- /dev/null
+++ b/pub/common/ipaddr/ip.go
@@ -0,0 +1,412 @@
+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 统计。
+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("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
+}
+
+func shouldTrustForwardedHeaders(remoteIP, clientIP string) bool {
+ if isPrivateOrLoopbackIP(remoteIP) {
+ return true
+ }
+ 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..c471ec7
--- /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 返回短剧原片使用的新 M3U8 解密密钥。
+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..a4c060a
--- /dev/null
+++ b/pub/common/m3u8/transformer.go
@@ -0,0 +1,153 @@
+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 前缀。
+// 短剧水印未完成时仍播放原片,原片路径如 k12/...,需要保留原路径。
+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..ab6ae70
--- /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 len(relativePath) == 0 {
+ 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..afc1b35
--- /dev/null
+++ b/pub/common/playletcache/cache.go
@@ -0,0 +1,20 @@
+package playletcache
+
+import (
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/log"
+ "videoweb/pub/redis"
+)
+
+// ClearHomeCache 清除app首页短剧缓存
+func ClearHomeCache() {
+ if redis.Client() == nil {
+ return
+ }
+ if err := redis.Del(redisconst.AppPlayletHomeCacheKey()); err != nil {
+ log.Warn("清除app首页短剧缓存失败", log.E(err))
+ }
+ if err := redis.Client().Incr(redisconst.AppPlayletHomeCacheVersionKey()).Err(); err != nil {
+ log.Warn("更新app首页短剧缓存版本失败", log.E(err))
+ }
+}
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..078c6e0
--- /dev/null
+++ b/pub/common/response/base_page.go
@@ -0,0 +1,11 @@
+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/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..c3da0b7
--- /dev/null
+++ b/pub/common/srvdef/codedef.go
@@ -0,0 +1,338 @@
+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 (
+ // ErrESSearchError ES搜索失败,单独用于搜索接口,避免和通用ES操作错误混淆。
+ ErrESSearchError CodeType = DBLevel + 100
+)
+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 // 视频不需要支付
+ PostNoNeed2Pay // 帖子不需要支付
+ 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 // 文件上传失败
+ PosoStatusFaild // 帖子状态错误
+ 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 //系统配置错误
+ ComicsCateNotExist // 漫画分类不存在
+ ComicsTagNotExist // 漫画标签不存在
+ ComicsTopicNotExist // 漫画主题不存在
+ ErrComicsChapterNotExist // 漫画章节不存在
+ ComicsNotExist // 漫画不存在
+ ComicsCVNotExist // 漫画CV不存在
+ ComicsMediaNotExist // 漫画视频不存在
+ ComicsRankNotExist // 排行榜不存在
+ VomicsTopicNotExist // 有声漫不存在
+ ComicsIsBlack // 漫画在黑名单中
+ CommentTextIsEmpty // 评论内容为空
+ ComicsIsVip // 漫画需要会员
+ ComicsIsNeedPay // 漫画需要付费
+ ComicsChapterExist // 漫画章节已存在
+ ObjectNotExist // 数据不存在
+ GameWtdrErroTip3 // 请填写银行名字或升级App版本
+ UserDoTaskErr // 用户任务出错
+ GetPrizeFail // 获取奖品失败
+ NovelIsNeedPay // 小说需要付费
+ NovelIsVip // 小说需要vip
+ LotteryActivityNotEnable // 抽奖活动没有启用
+ CurrTimeNotActivity // 当前时间没有活动
+ AIOrderStripsCountLimit // 今天AI脱衣提交次数达到上限
+ NotWtdrChannel // 没有匹配到提现通过
+ ExcelExportErr // 文件导出错误
+ ComicsNotJoinActivity // 这本漫画没有参与活动
+ 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 // 不是会员禁止发布
+ PublistVIPExpired // 会员已过期禁止发布
+ PublistContentErrDenied // 包含敏感词汇
+ PostRepeatBuy // 帖子重复购买
+ UserNotEnoughMoney // 用户的钻石数量不足
+ PostStatusError // 订单状态错误
+ PostNotAllowComplaint // 不允许举报
+ UserNotEnoughScore // 用户的钻石数量不足
+ ErrUserAccExist // 账号已存在
+ UserAccountNotBind // 账号未注册
+ VerifyOKPostCountLimit //上架的帖子数量达到限制
+ ErrUserBeBanned //ErrUserBeBanned 用户已经禁言 1004
+ ErrCustomerBanned //ErrCustomerBanned 客服已休息 1005
+ ErrVersionUpdate //ErrVersionUpdate 版本强制更新 1006
+ DarkAlleyNotExist // 暗巷攻略信息不存在
+)
diff --git a/pub/common/srvdef/msgdef.go b/pub/common/srvdef/msgdef.go
new file mode 100644
index 0000000..9c33923
--- /dev/null
+++ b/pub/common/srvdef/msgdef.go
@@ -0,0 +1,62 @@
+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",
+ ErrESSearchError: "elastic search 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",
+ PublistNotVIPPermissionDenied: "vip required to publish",
+ PublistVIPExpired: "vip expired",
+ DarkAlleyNotExist: "dark alley does not exist",
+ 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..000eec6
--- /dev/null
+++ b/pub/common/srvdef/tipdef.go
@@ -0,0 +1,167 @@
+package srvdef
+
+var codeAndTip = map[CodeType]string{
+ Success: "成功",
+ Failure: "失敗",
+ ErrCardNotExists: "沒有會員卡信息",
+ ErrMadouCoinNotExists: "沒有金幣信息",
+ ErrGetRechargeType: "充值方式獲取錯誤",
+ ErrRchgOrderNotExist: "充值訂單不存在",
+ ErrBuyVIPCardFail: "購買會員卡失敗",
+ ErrOperationTooFrequently: "用戶操作太頻繁",
+ PublistNotVIPPermissionDenied: "请开通VIP后发布",
+ PublistVIPExpired: "VIP已过期,请续费后发布",
+ DarkAlleyNotExist: "信息不存在",
+ 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: "视频不需要支付",
+ PostNoNeed2Pay: "帖子不需要支付",
+ 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操作错误",
+ ErrESSearchError: "搜索服务暂不可用",
+ 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: "没有卡券信息",
+ VerifyOKPostCountLimit: "上架的帖子数量达到限制",
+ CouponCanNotUse: "卡券无法使用",
+ LotteryActivityNotEnable: "抽奖活动没有启用",
+ CurrTimeNotActivity: "当前时间没有活动",
+ AIOrderStripsCountLimit: "今天AI脱衣提交次数达到上限",
+ NotWtdrChannel: "没有匹配到提现通过",
+ ComicsNotJoinActivity: "这本漫画没有参与活动",
+ 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/statcenter/registration_policy.go b/pub/common/statcenter/registration_policy.go
new file mode 100644
index 0000000..bd2beb2
--- /dev/null
+++ b/pub/common/statcenter/registration_policy.go
@@ -0,0 +1,5 @@
+package statcenter
+
+// IOSRegistrationDistrictCode 原生 iOS 商店包用户的固定渠道码。
+// 未配置时不覆盖用户原 DistrictCode。
+const IOSRegistrationDistrictCode = ""
diff --git a/pub/common/statcenter/stat.go b/pub/common/statcenter/stat.go
new file mode 100644
index 0000000..64bd6e9
--- /dev/null
+++ b/pub/common/statcenter/stat.go
@@ -0,0 +1,227 @@
+package statcenter
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+
+ "github.com/shopspring/decimal"
+)
+
+type KFKTopic string
+
+const (
+ STAT_APPID int32 = pubdef.AppId
+ USER_REG KFKTopic = "user_register" // 用户注册
+ USER_RECH KFKTopic = "user_recharge" // 用户充值(成功)
+ USER_RECH_ALL KFKTopic = "user_recharge_all" // 全部充值订单
+ USER_ACCE KFKTopic = "user_access" // 用户访问
+ USER_BINDING KFKTopic = "user_binding" // 用户绑定(邮箱/正式注册)
+ USER_INVITE KFKTopic = "user_invite" // 用户邀请绑定
+ VipJob KFKTopic = "vip" // VIP 购买(兼容旧任务)
+ RechargeAll KFKTopic = "rechargeAll" // 全部充值(兼容旧任务名)
+ RegisterLightJob KFKTopic = "registerLight" // H5 轻量注册
+ ConsumeRecordJob KFKTopic = "consume_record" // 产品消费流水
+ CardSellJob KFKTopic = "card_sell" // 会员卡销售流水
+ AI_SELL_JOB KFKTopic = "ai_sell" // AI 消费(本项目无此业务)
+)
+
+const (
+ StatVipCard = iota // 会员卡
+ StatValueAddSer // 增值/其他消费
+ StatVideoUnlock // 视频解锁
+)
+
+const (
+ CurrencyTypeGold = iota // 金币
+ CurrencyTypeCash // 现金
+)
+
+// StatcenterSyncReq 统计中心增量同步请求
+type StatcenterSyncReq struct {
+ Job string `json:"job" bson:"job"` // 同步任务类型
+ UserId uint64 `json:"userId" bson:"userId"` // 用户(注册)
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台Id(访问/流水)
+ SuccessTime time.Time `json:"successTime"` // 游标时间(购买会员、提现、充值)
+ MaxSize int64 `json:"maxSize" bson:"maxSize"` // 最大条数
+}
+
+// StatcenterSyncResp 统计中心增量同步响应
+type StatcenterSyncResp struct {
+ Code int `json:"code"` // 200正常 其他异常
+ Msg string `json:"msg"` // 错误消息
+ Job string `json:"job" bson:"job"` // 类型
+ VisitList []UserVisitMsg `json:"visitList"` // 日活记录(旧字段,兼容)
+ AccessList []UserVisitMsg `json:"accessList"` // 日活记录
+ RegisterList []UserRegisterMsg `json:"registerList"` // 注册记录
+ BindingList []UserBindingMsg `json:"bindingList"` // 用户绑定记录
+ InviteList []UserInviteBindMsg `json:"inviteList"` // 邀请绑定
+ ConsumeList []ConsumeRecordMsg `json:"consumeList"` // 消费流水
+ RechargeList []UserRechargeMsg `json:"rechargeList"` // 充值记录
+ AllRechargeList []UserRechargeMsg `json:"allRechargeList"` // 全部支付订单
+ VipList []UserBuyVIPMsg `json:"buyVipList"` // VIP购买记录
+ CardSellList []CardSellMsg `json:"cardSellList"` // 会员卡销售流水
+ AiSellList []AiSellMsg `json:"aiSellList"` // AI销售流水
+}
+
+// 用户访问消息
+type UserVisitMsg struct {
+ UserId uint64 `json:"userId" bson:"userId"` // 用户ID
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ DevType string `json:"devType,omitempty" bson:"devType"` // 设备型号
+ IP string `json:"ip,omitempty" bson:"ip"` // IP
+ Version string `json:"version,omitempty" bson:"version"` // APP版本号
+ DevID string `json:"devID,omitempty" bson:"devID"` // 设备ID
+ VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间
+ IsDirect bool `json:"isDirect,omitempty"` // true:是直推用户
+ DistrictCode string `json:"districtCode,omitempty"` // 渠道码
+ RegisterTime time.Time `json:"registerTime,omitempty"` // 用户注册时间
+}
+
+// 用户注册消息
+type UserRegisterMsg struct {
+ UserId uint64 `json:"userId" bson:"userId" binding:"required"` // 用户ID
+ AppId int32 `json:"appId" bson:"appId" binding:"required"` // appID
+ SysType string `json:"sysType" bson:"sysType" binding:"required"` // 操作系统类型 安卓 IOS
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ Mobile string `json:"mobile" bson:"mobile"` // 手机号
+ IP string `json:"ip,omitempty" bson:"ip"` // IP
+ Name string `json:"name" bson:"name"` // 名称
+ IsDirect bool `json:"isDirect" bson:"isDirect" binding:"required"` // true:是直推用户
+ DistrictCode string `json:"districtCode" bson:"districtCode" ` // 渠道码
+ PromSeqe string `json:"promSeqe,omitempty" bson:"promSeqe"` // 全名代理
+ PromCode string `json:"promCode,omitempty" bson:"promCode"` // 推广序列
+ PUC string `json:"puc" bson:"puc"` // 应用中心推广
+ RegisterTime time.Time `json:"registerTime,omitempty" bson:"registerTime" binding:"required"` // 注册时间
+}
+
+// UserBindingMsg 用户绑定(本项目对应邮箱正式注册)
+type UserBindingMsg struct {
+ UserId uint64 `json:"userId"` // 用户ID
+ AppId int32 `json:"appId"` // appID
+ PlatformId string `json:"platformId"` // 原始平台流水Id
+ SysType string `json:"sysType"` // 操作系统类型
+ DevType string `json:"devType"` // 设备类型
+ Mobile string `json:"mobile"` // 手机号/绑定账号(本项目填邮箱)
+ BindingTime *time.Time `json:"bindingTime"` // 绑定时间
+}
+
+// UserInviteBindMsg 邀请绑定
+type UserInviteBindMsg struct {
+ UserId uint64 `json:"userId" bson:"userId" binding:"required"`
+ AppId int32 `json:"appId" bson:"appId" binding:"required"`
+ ParentPromCode string `json:"parentPromCode,omitempty" bson:"promSeqe"`
+ InviteTime time.Time `json:"inviteTime,omitempty" bson:"inviteTime" binding:"required"`
+}
+
+// ConsumeRecordMsg 消费流水
+type ConsumeRecordMsg struct {
+ AppID int32 `json:"appID" bson:"appID"`
+ UID uint64 `json:"uid" bson:"uid"`
+ Type int `json:"type" bson:"type"` // 消费类型
+ CurrencyType int `json:"currencyType" bson:"currencyType"` // 0金币 1人民币
+ Money decimal.Decimal `json:"money" bson:"money"` // 人民币 元
+ Amount decimal.Decimal `json:"amount" bson:"amount"` // 金币
+ Uniq string `json:"uniq" bson:"uniq"`
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+}
+
+// AiSellMsg AI 销售流水
+type AiSellMsg struct {
+ AppID int32 `json:"appID" bson:"appID"`
+ UID uint64 `json:"uid" bson:"uid"`
+ UniqID string `json:"uniqID" bson:"uniqID"`
+ Amount int64 `json:"amount" bson:"amount"`
+ TranType string `json:"tranType" bson:"tranType"`
+ SysType string `json:"sysType" bson:"sysType"`
+ DevType string `json:"devType" bson:"devType"`
+ CurrencyType string `json:"currencyType" bson:"currencyType"`
+ TranCreatedAt time.Time `json:"tranCreatedAt" bson:"tranCreatedAt"`
+ IsRepurchase string `json:"isRepurchase" bson:"isRepurchase"`
+}
+
+// CardSellMsg 会员卡销售流水
+type CardSellMsg struct {
+ AppID int32 `json:"appID" bson:"appID"`
+ UID uint64 `json:"uid" bson:"uid"`
+ UniqID string `json:"uniqID" bson:"uniqID"`
+ Amount int64 `json:"amount" bson:"amount"`
+ TranTypeInt int64 `json:"tranTypeInt" bson:"tranTypeInt"`
+ TranType string `json:"tranType" bson:"tranType"`
+ SysType string `json:"sysType" bson:"sysType"`
+ CurrencyType int `json:"currencyType" bson:"currencyType"` // 0金币 1现金
+ TranCreatedAt time.Time `json:"tranCreatedAt" bson:"tranCreatedAt"`
+ Product Product `json:"product" bson:"product"`
+}
+
+// Product 售卡产品信息
+type Product struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ DiscountedPrice int64 `json:"discountedPrice"`
+}
+
+// 用户充值消息
+type UserRechargeMsg struct {
+ UserId uint64 `json:"userId" bson:"userId" binding:"required"`
+ AppId int32 `json:"appId" bson:"appId" binding:"required"`
+ PlatformId string `json:"platformId" bson:"platformId"`
+ SysType string `json:"sysType" bson:"sysType" binding:"required"`
+ DevType string `json:"devType" bson:"devType"`
+ ChannelName string `json:"channelName" bson:"channelName"`
+ CID string `json:"cid" bson:"cid"`
+ Type string `bson:"type" json:"type"`
+ OrderId string `json:"orderId" bson:"orderId" binding:"required"`
+ OID string `json:"oid" bson:"oid"`
+ Money int64 `json:"money" bson:"money" binding:"required"`
+ PayMoney int64 `json:"payMoney" bson:"payMoney" binding:"required"`
+ Status int `json:"status" bson:"status"`
+ Rate string `json:"rate"`
+ SuccessAt time.Time `json:"successAt,omitempty" bson:"successAt"`
+ PaymentAt time.Time `json:"paymentAt"`
+ ProductType int `json:"productType" bson:"productType"`
+ ChanShareMod int `json:"chanShareMod" bson:"chanShareMod"`
+ IsNewOrOldUser bool `json:"isNewOrOldUser" bson:"isNewOrOldUser"`
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ DistrictCode string `json:"districtCode" bson:"districtCode"`
+ RegisterTime time.Time `json:"registerTime,omitempty" bson:"registerTime"`
+}
+
+type BuyVIPType int64
+
+const (
+ BuyVIPTypeMoney BuyVIPType = 0 + iota // 现金
+ BuyVIPTypeGold // 金币
+)
+
+// 用户购买VIP
+type UserBuyVIPMsg struct {
+ AppId int32 `json:"appId" bson:"appId"`
+ UID uint64 `json:"uid" bson:"uid"`
+ PlatformId string `json:"platformId" bson:"platformId"`
+ Name string `json:"name" bson:"name"`
+ Money int64 `json:"money" bson:"money"`
+ Amount int64 `json:"amount" bson:"amount"`
+ BuyVIPType BuyVIPType `json:"buyVIPType" bson:"buyVIPType"`
+ OID string `json:"oid" bson:"oid"`
+ DeviceType string `json:"deviceType" bson:"deviceType"`
+ Status int `json:"status" bson:"status"`
+ SuccessAt time.Time `json:"successTime"`
+ IsDirect bool `json:"isDirect" bson:"isDirect"`
+ DistrictCode string `json:"districtCode" bson:"districtCode"`
+ PromCode string `json:"promCode" bson:"promCode"`
+}
+
+// MapOrderStatusToStat 将本项目订单状态映射为统计中心约定:2失败 3成功
+func MapOrderStatusToStat(status pubdef.VipOrderStatus) int {
+ switch status {
+ case pubdef.VipOrderSuccess:
+ return 3
+ case pubdef.VipOrderFailed:
+ return 2
+ default:
+ return int(status)
+ }
+}
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/ua.go b/pub/common/useragent/ua.go
new file mode 100644
index 0000000..8dbc399
--- /dev/null
+++ b/pub/common/useragent/ua.go
@@ -0,0 +1,166 @@
+package useragent
+
+import (
+ "net/url"
+ "strings"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+type UA struct {
+ UserAgent string
+ DevID string
+ DevType string
+ SysType string
+ Ver string
+ BuildID string
+ SystemName string
+ SystemVersion string
+ DeviceBrand string
+ DeviceModel string
+ SessionID string
+}
+
+func IsH5(sysType string) bool {
+ if sysType == consts.H5IOS || sysType == consts.H5Android || sysType == consts.H5PC || sysType == consts.H5 || sysType == consts.Light {
+ return true
+ }
+ return false
+}
+
+func (s UA) IsH5() bool {
+ return IsH5(s.SysType)
+}
+
+func UserAgent(c *gin.Context) {
+ var uastr string
+ ua := UA{}
+ ua.SessionID = c.GetHeader("sid")
+ userAgent := c.GetHeader("X-User-Agent") // 兼容 H5 UserAgent
+ if userAgent == "" {
+ userAgent = c.Request.UserAgent()
+ }
+ c.Set(consts.CtxUAH5, c.Request.UserAgent())
+ if userAgent != "" {
+ 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 {
+ segment := strings.TrimSpace(v)
+ if segment == "" {
+ continue
+ }
+ vss := strings.SplitN(segment, "=", 2)
+ if len(vss) < 2 {
+ continue
+ }
+ key := strings.TrimSpace(vss[0])
+ value := strings.TrimSpace(vss[1])
+ lowerKey := strings.ToLower(key)
+ switch lowerKey {
+ case "devid":
+ ua.DevID = value
+ c.Set(consts.CtxDevId, ua.DevID)
+ case "devtype":
+ ua.DevType = value
+ c.Set(consts.CtxDevType, ua.DevType)
+ case "systype":
+ inputSysType := strings.ToLower(strings.TrimSpace(value))
+ switch {
+ case strings.Contains(inputSysType, "h5_android"):
+ ua.SysType = consts.H5Android
+ case strings.Contains(inputSysType, "h5_ios"):
+ ua.SysType = consts.H5IOS
+ case strings.Contains(inputSysType, "h5_pc"):
+ ua.SysType = consts.H5PC
+ case strings.Contains(inputSysType, "android"):
+ ua.SysType = consts.Android
+ case strings.Contains(inputSysType, "ios"):
+ ua.SysType = consts.IOS
+ case strings.Contains(inputSysType, "h5"):
+ ua.SysType = consts.H5
+ case strings.Contains(inputSysType, "pc"):
+ ua.SysType = consts.PC
+ case strings.Contains(inputSysType, "light"):
+ ua.SysType = consts.Light
+ }
+ c.Set(consts.CtxSysType, ua.SysType)
+ case "ver":
+ ua.Ver = value
+ c.Set(consts.CtxVersion, ua.Ver)
+ case "buildid":
+ ua.BuildID = value
+ c.Set(consts.CtxBuildId, ua.BuildID)
+ case "system_name", "system-name", "systemname":
+ if ua.SystemName == "" {
+ ua.SystemName = value
+ }
+ case "system_version", "system-version", "systemversion":
+ if ua.SystemVersion == "" {
+ ua.SystemVersion = value
+ }
+ case "device_brand", "device-brand", "devicebrand":
+ if ua.DeviceBrand == "" {
+ ua.DeviceBrand = value
+ }
+ case "device_model", "device-model", "devicemodel":
+ if ua.DeviceModel == "" {
+ ua.DeviceModel = value
+ }
+ case "sid":
+ if ua.SessionID == "" {
+ ua.SessionID = value
+ }
+ default:
+ log.Warn("user-agent unknown", log.Any("ua", userAgent), log.Any("segment", segment))
+ }
+ }
+ } else {
+ log.Warn("user-agent empty")
+ }
+ ua.UserAgent = uastr
+ if ua.DeviceBrand == "" {
+ brand := strings.TrimSpace(c.GetHeader("device_brand"))
+ if brand == "" {
+ brand = strings.TrimSpace(c.GetHeader("device-brand"))
+ }
+ ua.DeviceBrand = brand
+ }
+ if ua.DeviceModel == "" {
+ model := strings.TrimSpace(c.GetHeader("device_model"))
+ if model == "" {
+ model = strings.TrimSpace(c.GetHeader("device-model"))
+ }
+ ua.DeviceModel = model
+ }
+ if ua.SystemName == "" {
+ name := strings.TrimSpace(c.GetHeader("system_name"))
+ if name == "" {
+ name = strings.TrimSpace(c.GetHeader("system-name"))
+ }
+ ua.SystemName = name
+ }
+ if ua.SystemVersion == "" {
+ version := strings.TrimSpace(c.GetHeader("system_version"))
+ if version == "" {
+ version = strings.TrimSpace(c.GetHeader("system-version"))
+ }
+ ua.SystemVersion = version
+ }
+ if ua.SystemName == "" {
+ ua.SystemName = ua.SysType
+ }
+ if ua.SystemVersion == "" {
+ ua.SystemVersion = ua.Ver
+ }
+ c.Set(consts.CtxSystemName, ua.SystemName)
+ c.Set(consts.CtxSystemVersion, ua.SystemVersion)
+ 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..6515214
--- /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))
+// }
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..ae11c8c
--- /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
+ }
+}
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..9ccdbb7
--- /dev/null
+++ b/pub/common/utils/path.go
@@ -0,0 +1,89 @@
+package utils
+
+import (
+ "path"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/PuerkitoBio/purell"
+)
+
+// PathSegment 路由路径段:有 slug 用 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)
+}
+
+// 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
+}
+
+func JoinUrlPathV2(host string, uri string) string {
+ return path.Join(host, uri)
+}
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..027de31
--- /dev/null
+++ b/pub/common/utils/slug_source.go
@@ -0,0 +1,271 @@
+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{
+ "中文字幕", "中文翻譯", "中文翻译", "无码流出", "無碼流出",
+ "无码破解", "無碼破解", "无修正", "無修正", "完整版", "抢先版",
+ "高清版", "字幕版", "HDYS", "魔改电视剧", "魔改电影", "HDDJ", "91DJ",
+ "AI魔改",
+}
+
+// 技术规格、版本标记,单独出现在标题开头时去掉。
+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..5c3976c
--- /dev/null
+++ b/pub/common/utils/strings.go
@@ -0,0 +1,129 @@
+package utils
+
+import (
+ "encoding/json"
+ "math/rand/v2"
+ "strings"
+ "videoweb/pub/common/netutil"
+
+ "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: "否",
+}
+
+// ApplyImgCdn 尝试解析节点 Content JSON,若含 "img" 字段则对其拼接 CDN 后重新序列化。
+// 不符合 {"img": "..."} 结构的 Content 原样返回。
+func ApplyImgCdn(cdn, content string) string {
+ if content == "" || cdn == "" {
+ return content
+ }
+ var m map[string]interface{}
+ if err := json.Unmarshal([]byte(content), &m); err != nil {
+ return content // 非 JSON 对象,原样返回
+ }
+ imgVal, ok := m["img"]
+ if !ok {
+ return content // 无 img 字段,原样返回
+ }
+ imgStr, ok := imgVal.(string)
+ if !ok || imgStr == "" {
+ return content
+ }
+ m["img"] = netutil.MergeURL(cdn, imgStr)
+ b, err := json.Marshal(m)
+ if err != nil {
+ return content
+ }
+ return string(b)
+}
+
+// 截取指定长度的字符串
+func GetPrefix(s string, n int) string {
+ runes := []rune(s)
+ if len(runes) <= n {
+ return s
+ }
+ return string(runes[:n])
+}
+
+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 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..9167454
--- /dev/null
+++ b/pub/common/utils/time.go
@@ -0,0 +1,286 @@
+package utils
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+var (
+ LocBJ, _ = time.LoadLocation("Asia/Shanghai")
+)
+
+func init() {
+ time.Local = LocBJ
+}
+
+func Now() time.Time {
+ return time.Now().Local()
+}
+
+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(time.Local).Date()
+ return time.Date(year, month, 1, 0, 0, 0, 0, t.Location())
+}
+
+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 {
+ bt := BeginOfTime(t)
+ 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())
+}
diff --git a/pub/common/utils/time_test.go b/pub/common/utils/time_test.go
new file mode 100644
index 0000000..dae7c5b
--- /dev/null
+++ b/pub/common/utils/time_test.go
@@ -0,0 +1,130 @@
+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_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")
+ }
+ }
+}
+
+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..48d4e3b
--- /dev/null
+++ b/pub/common/v10/validate.go
@@ -0,0 +1,178 @@
+package v10
+
+import (
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/nyaruka/phonenumbers"
+
+ "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()
+ }
+}
+
+// 参数解析 例如:3
+func ParamIsNumber(str string) bool {
+ reg := regexp.MustCompile(`^(\d+)$`)
+ return reg.MatchString(str)
+}
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..460f373
--- /dev/null
+++ b/pub/dataReport/handle.go
@@ -0,0 +1,246 @@
+package dataReport
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ "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(eventId, event string, districtCode string, ua useragent.UA, uid, ip string) CommonField {
+ userSidKey := redisconst.UserSidKey(pubdef.AppId)
+ sid, _ := redis.Get(userSidKey)
+ if sid == nil {
+ id := GenerateGlobalEventID()
+ sid = &id
+ redis.Set(userSidKey, sid, redisconst.UserSidKeyExpire)
+ }
+ if eventId == "" {
+ eventId = GenerateGlobalEventID()
+ }
+ device := ua.SysType
+ if device == "" {
+ device = "PC"
+ }
+ deviceID := ua.DevID
+ if deviceID == "" {
+ deviceID = ip
+ }
+ deviceModel := ua.DeviceModel
+ if deviceModel == "" {
+ deviceModel = ua.DevType
+ }
+ sidVal := ua.SessionID
+ if sidVal == "" {
+ sidVal = *sid
+ }
+ return CommonField{
+ UID: uid,
+ EventID: eventId,
+ Event: event,
+ Channel: strings.TrimSpace(districtCode),
+ AppID: pubdef.TrackerAppId(),
+ ClientTS: fmt.Sprintf("%v", time.Now().Unix()),
+ Device: device,
+ DeviceID: deviceID,
+ UserAgent: ua.UserAgent,
+ DeviceBrand: ua.DeviceBrand,
+ DeviceModel: deviceModel,
+ SystemName: ua.SystemName,
+ SystemVersion: ua.SystemVersion,
+ IP: ip,
+ SID: sidVal,
+ }
+}
+
+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..94ba2b0
--- /dev/null
+++ b/pub/dataReport/type.go
@@ -0,0 +1,175 @@
+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"
+ EventTypeVideoComment = "video_comment"
+ EventTypeVideoStatusChange = "video_status_change" // 视频状态变更
+
+)
+
+// 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
+ 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
+ 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"` // 状态标识
+ }
+)
+
+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/esmanager/client.go b/pub/esmanager/client.go
new file mode 100644
index 0000000..8fe6ead
--- /dev/null
+++ b/pub/esmanager/client.go
@@ -0,0 +1,192 @@
+package esmanager
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "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 {
+ existsRes, err := m.client.Indices.Exists([]string{indexName})
+ if err != nil {
+ return err
+ }
+ defer existsRes.Body.Close()
+ if existsRes.StatusCode == http.StatusOK {
+ return nil
+ }
+ if existsRes.StatusCode != http.StatusNotFound {
+ body, _ := io.ReadAll(existsRes.Body)
+ return errors.New(fmt.Sprintf("%+v body:%s", existsRes, string(body)))
+ }
+ res, err := m.client.Indices.Create(indexName, func(request *esapi.IndicesCreateRequest) {
+ request.Body = marshalSearchM(setting)
+ })
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusCreated {
+ return nil
+ }
+ body, _ := io.ReadAll(res.Body)
+ if res.StatusCode == http.StatusBadRequest && strings.Contains(string(body), "resource_already_exists_exception") {
+ return nil
+ }
+ return errors.New(fmt.Sprintf("%+v body:%s", res, string(body)))
+}
+
+// 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()
+ if res.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", res))
+ }
+ 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()),
+ m.client.Bulk.WithRefresh("true"),
+ )
+ 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))
+ }
+ 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..186df25
--- /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
+}
diff --git a/pub/esmanager/manager.go b/pub/esmanager/manager.go
new file mode 100644
index 0000000..9eb2cf8
--- /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
+}
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/analyticsdailystat/analytics.go b/pub/models/analyticsdailystat/analytics.go
new file mode 100644
index 0000000..75d247b
--- /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
+}
diff --git a/pub/models/analyticsdailystat/tag.go b/pub/models/analyticsdailystat/tag.go
new file mode 100755
index 0000000..d98856c
--- /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"
+}
diff --git a/pub/models/analyticsdailystat/type.go b/pub/models/analyticsdailystat/type.go
new file mode 100644
index 0000000..943e3ba
--- /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
diff --git a/pub/models/analyticsstat/analytics.go b/pub/models/analyticsstat/analytics.go
new file mode 100644
index 0000000..d4c55ce
--- /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
+}
diff --git a/pub/models/analyticsstat/tag.go b/pub/models/analyticsstat/tag.go
new file mode 100755
index 0000000..b7418b6
--- /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"
+}
diff --git a/pub/models/analyticsstat/type.go b/pub/models/analyticsstat/type.go
new file mode 100644
index 0000000..e455267
--- /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
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/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..cb514fb
--- /dev/null
+++ b/pub/models/cfg/tag.go
@@ -0,0 +1,88 @@
+package cfg
+
+type TagSysCfg struct {
+ AppAddr string
+ OfficialAppAddr 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
+ CustomerServiceHost string
+ WeiChart string
+ DHNewUrlAddr1 string
+ DHNewUrlAddr2 string
+ DHNewUrlAddr3 string
+ DHForeverUrlAddr string
+ DHTitle string
+ DHKeyworld string
+ DHDescription string
+ DHSiteName string
+ ChannelCode string
+ EnableH265 string
+}
+
+var FN struct {
+ SysCfg TagSysCfg
+}
+
+var TN struct {
+ SysCfg string
+}
+
+func init() {
+ FN.SysCfg.AppAddr = "appAddr"
+ FN.SysCfg.OfficialAppAddr = "officialAppAddr"
+ 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.CustomerServiceHost = "customerServiceHost"
+ FN.SysCfg.WeiChart = "weiChart"
+ FN.SysCfg.DHNewUrlAddr1 = "dHNewUrlAddr1"
+ FN.SysCfg.DHNewUrlAddr2 = "dHNewUrlAddr2"
+ FN.SysCfg.DHNewUrlAddr3 = "dHNewUrlAddr3"
+ FN.SysCfg.DHForeverUrlAddr = "dHForeverUrlAddr"
+ FN.SysCfg.DHTitle = "dhTitle"
+ FN.SysCfg.DHKeyworld = "dhKeyworld"
+ FN.SysCfg.DHDescription = "dhDescription"
+ FN.SysCfg.DHSiteName = "dhSiteName"
+ FN.SysCfg.ChannelCode = "channelCode"
+ FN.SysCfg.EnableH265 = "enableH265"
+ TN.SysCfg = "sys_cfg"
+}
diff --git a/pub/models/cfg/type.go b/pub/models/cfg/type.go
new file mode 100644
index 0000000..4c965c3
--- /dev/null
+++ b/pub/models/cfg/type.go
@@ -0,0 +1,58 @@
+package cfg
+
+// @table:sys_cfg
+type SysCfg struct {
+ Id int64 `json:"id" bson:"_id"`
+ UnencryptedCdn string `json:"unencryptedCdn" bson:"unencryptedCdn"` // 不加密资源使用的cdn
+ SitemapDefaultImages []string `json:"sitemapDefaultImages" bson:"sitemapDefaultImages"` // Sitemap里面默认封面图
+ ImgAppCdn string `json:"imgAppCdn" bson:"imgAppCdn"` // 图片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使用)
+ ImgAppUsCdn string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片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
+ CustomerServiceHost string `json:"customerServiceHost" bson:"customerServiceHost"` // 在线客服
+ //网站管理
+ 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下载地址(落地页)
+ OfficialAppAddr string `json:"officialAppAddr" bson:"officialAppAddr"` //官网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"` //应用导航
+ //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"` // 所有使用中的域名
+ AdvitiesTopicNum int64 `json:"advitiesTopicNum" bson:"advitiesTopicNum"` // 广告间隔
+ 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网站名称
+ ActressBackGround string `json:"actressBackGround" bson:"actressBackGround"` // 女优通用背景图
+ ChannelCode string `json:"channelCode" bson:"channelCode"` // 渠道码
+ EnableH265 bool `json:"enableH265" bson:"enableH265"` // 是否允许客户端使用 H265 地址播放
+}
diff --git a/pub/models/cointransmod/tag.go b/pub/models/cointransmod/tag.go
new file mode 100644
index 0000000..1365a31
--- /dev/null
+++ b/pub/models/cointransmod/tag.go
@@ -0,0 +1,40 @@
+package cointransmod
+
+type TagCoinTrans struct {
+ Id string
+ UserId string
+ UserName string
+ FlowDir string
+ TranType string
+ Amount string
+ BalanceBefore string
+ BalanceAfter string
+ OrderNo string
+ RelatedId string
+ Remark string
+ CreatedAt string
+}
+
+var FN struct {
+ CoinTrans TagCoinTrans
+}
+var TN struct {
+ CoinTrans string
+}
+
+func init() {
+ FN.CoinTrans.Id = "_id"
+ FN.CoinTrans.UserId = "userId"
+ FN.CoinTrans.UserName = "userName"
+ FN.CoinTrans.FlowDir = "flowDir"
+ FN.CoinTrans.TranType = "tranType"
+ FN.CoinTrans.Amount = "amount"
+ FN.CoinTrans.BalanceBefore = "balanceBefore"
+ FN.CoinTrans.BalanceAfter = "balanceAfter"
+ FN.CoinTrans.OrderNo = "orderNo"
+ FN.CoinTrans.RelatedId = "relatedId"
+ FN.CoinTrans.Remark = "remark"
+ FN.CoinTrans.CreatedAt = "createdAt"
+
+ TN.CoinTrans = "coin_trans"
+}
diff --git a/pub/models/cointransmod/trans.go b/pub/models/cointransmod/trans.go
new file mode 100644
index 0000000..566414c
--- /dev/null
+++ b/pub/models/cointransmod/trans.go
@@ -0,0 +1,80 @@
+package cointransmod
+
+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 coll *mongos.Collection
+
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.CoinTrans)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ // 按用户+时间倒序查流水(最常用)
+ {Keys: bson.D{
+ {Key: FN.CoinTrans.UserId, Value: 1},
+ {Key: FN.CoinTrans.CreatedAt, Value: -1},
+ }},
+ // 按交易类型筛选
+ {Keys: bson.D{{Key: FN.CoinTrans.TranType, Value: 1}}},
+ // 按订单号查(幂等校验)
+ {Keys: bson.D{{Key: FN.CoinTrans.OrderNo, Value: 1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(cointransmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneCoinTrans 写入一条流水记录
+func InsertOneCoinTrans(ctx context.Context, doc *CoinTrans) *srvdef.SrvErr {
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Error("cointransmod:InsertOneCoinTrans failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindManyCoinTrans 分页查询流水列表
+func FindManyCoinTrans(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (CoinTransList, *srvdef.SrvErr) {
+ var data CoinTransList
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("cointransmod:FindManyCoinTrans failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountCoinTrans 统计流水条数
+func CountCoinTrans(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter)
+ if err != nil {
+ log.Warn("cointransmod:CountCoinTrans failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// StatSyncConsumeList 按 ID 游标增量拉取金币消费流水(统计中心)
+func StatSyncConsumeList(ctx context.Context, startId int64, size int64) (CoinTransList, error) {
+ opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.D{{Key: FN.CoinTrans.Id, Value: 1}})
+ filter := bson.M{
+ FN.CoinTrans.Id: bson.M{"$gt": startId},
+ FN.CoinTrans.FlowDir: FlowOut,
+ }
+ list := CoinTransList{}
+ if err := coll.Find(ctx, &list, filter, opt); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
diff --git a/pub/models/cointransmod/type.go b/pub/models/cointransmod/type.go
new file mode 100644
index 0000000..70dd5db
--- /dev/null
+++ b/pub/models/cointransmod/type.go
@@ -0,0 +1,62 @@
+package cointransmod
+
+import "time"
+
+// FlowDir 收支方向
+type FlowDir int8
+
+const (
+ FlowIn FlowDir = 1 // 收入(充值、赠送)
+ FlowOut FlowDir = -1 // 支出(消费、解锁)
+)
+
+func (f FlowDir) Desc() string {
+ if f == FlowIn {
+ return "收入"
+ }
+ return "支出"
+}
+
+// TranType 交易类型
+type TranType int32
+
+const (
+ TranTypeCoinRecharge TranType = 1 // 金币充值(购买金币)
+ TranTypeVipGiftCoin TranType = 2 // 购买会员赠送金币
+ TranTypeDarkAlley TranType = 3 // 历史遗留交易类型(已下线)
+ TranTypeVideoUnlock TranType = 4 // 视频解锁
+ TranTypeAdminAdjust TranType = 5 // 管理员手动调整
+)
+
+var TranTypeNameMap = map[TranType]string{
+ TranTypeCoinRecharge: "金币充值",
+ TranTypeVipGiftCoin: "会员赠送",
+ TranTypeDarkAlley: "历史交易",
+ TranTypeVideoUnlock: "视频解锁",
+ TranTypeAdminAdjust: "管理员调整",
+}
+
+func (t TranType) Desc() string {
+ if name, ok := TranTypeNameMap[t]; ok {
+ return name
+ }
+ return "未知"
+}
+
+// @table: coin_trans
+type CoinTrans struct {
+ Id int64 `json:"id" bson:"_id"` // 唯一ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ UserName string `json:"userName" bson:"userName"` // 用户昵称(快照)
+ FlowDir FlowDir `json:"flowDir" bson:"flowDir"` // 收支方向:1收入 -1支出
+ TranType TranType `json:"tranType" bson:"tranType"` // 交易类型
+ Amount int64 `json:"amount" bson:"amount"` // 本次交易金币数(正整数)
+ BalanceBefore int64 `json:"balanceBefore" bson:"balanceBefore"` // 交易前余额
+ BalanceAfter int64 `json:"balanceAfter" bson:"balanceAfter"` // 交易后余额
+ OrderNo string `json:"orderNo" bson:"orderNo"` // 关联订单号(可为空)
+ RelatedId int64 `json:"relatedId" bson:"relatedId"` // 关联内容ID(视频等,可为空)
+ Remark string `json:"remark" bson:"remark"` // 备注描述
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 交易时间
+}
+
+type CoinTransList []*CoinTrans
diff --git a/pub/models/commentlikemod/like.go b/pub/models/commentlikemod/like.go
new file mode 100644
index 0000000..ac1d23e
--- /dev/null
+++ b/pub/models/commentlikemod/like.go
@@ -0,0 +1,90 @@
+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"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 comment_like 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.CommentLike)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.CommentLike.UserId, Value: 1},
+ {Key: FN.CommentLike.CommentId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.CommentLike.CommentId, Value: 1},
+ {Key: FN.CommentLike.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.CommentLike.ObjectType, Value: 1},
+ {Key: FN.CommentLike.ObjectId, Value: 1},
+ {Key: FN.CommentLike.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(commentlikemod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// InsertOneCommentLike 插入一条评论点赞关系
+func InsertOneCommentLike(ctx context.Context, doc *CommentLike) (bool, *srvdef.SrvErr) {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.CommentLike.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) {
+ return false, nil
+ }
+ log.Warn("插入评论点赞关系失败", log.Any("doc", doc), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// DeleteOneCommentLike 删除一条评论点赞关系
+func DeleteOneCommentLike(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("删除评论点赞关系失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindManyCommentLike 按条件查询评论点赞关系
+func FindManyCommentLike(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (CommentLikeList, *srvdef.SrvErr) {
+ data := make(CommentLikeList, 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, "", "")
+ }
+ return data, nil
+}
diff --git a/pub/models/commentlikemod/tag.go b/pub/models/commentlikemod/tag.go
new file mode 100644
index 0000000..4fae5a2
--- /dev/null
+++ b/pub/models/commentlikemod/tag.go
@@ -0,0 +1,32 @@
+package commentlikemod
+
+// TagCommentLike 保存 comment_like 集合字段名
+type TagCommentLike struct {
+ Id string
+ UserId string
+ CommentId string
+ ObjectId string
+ ObjectType string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ CommentLike TagCommentLike
+}
+
+var TN struct {
+ CommentLike string
+}
+
+func init() {
+ FN.CommentLike.Id = "_id"
+ FN.CommentLike.UserId = "userId"
+ FN.CommentLike.CommentId = "commentId"
+ FN.CommentLike.ObjectId = "objectId"
+ FN.CommentLike.ObjectType = "objectType"
+ FN.CommentLike.CreatedAt = "createdAt"
+ FN.CommentLike.UpdatedAt = "updatedAt"
+
+ TN.CommentLike = "comment_like"
+}
diff --git a/pub/models/commentlikemod/type.go b/pub/models/commentlikemod/type.go
new file mode 100644
index 0000000..1d8cb4f
--- /dev/null
+++ b/pub/models/commentlikemod/type.go
@@ -0,0 +1,19 @@
+package commentlikemod
+
+import "time"
+
+// CommentLike @table: comment_like
+//
+// CommentLike 评论点赞关系模型
+type CommentLike struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 点赞记录ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ CommentId int64 `json:"commentId" bson:"commentId"` // 评论ID
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 评论对象ID
+ ObjectType int64 `json:"objectType" bson:"objectType"` // 评论对象类型 10-短剧 11-短剧剧集
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// CommentLikeList 评论点赞关系列表
+type CommentLikeList []*CommentLike
diff --git a/pub/models/commentmod/comment.go b/pub/models/commentmod/comment.go
new file mode 100644
index 0000000..be1ea81
--- /dev/null
+++ b/pub/models/commentmod/comment.go
@@ -0,0 +1,226 @@
+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.ParentsId, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.UserId, Value: 1},
+ {Key: FN.Comment.ObjectType, 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.ParentsId, Value: 1},
+ {Key: FN.Comment.CountLike, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ParentsId, Value: 1},
+ {Key: FN.Comment.Level, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: 1},
+ {Key: FN.Comment.Id, 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
+}
+
+// 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..f830294
--- /dev/null
+++ b/pub/models/commentmod/tag.go
@@ -0,0 +1,54 @@
+package commentmod
+
+type TagComment struct {
+ CreatedAt string
+ Id string
+ Level string
+ ObjectId string
+ ParentsId string
+ ReplyCommentId string
+ ReplyUserName string
+ Text string
+ TopRank string
+ UpdatedAt string
+ UserId string
+ UserName string
+ UserAvatar 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.ReplyCommentId = "replyCommentId"
+ FN.Comment.ReplyUserName = "replyUserName"
+ FN.Comment.Text = "text"
+ FN.Comment.TopRank = "topRank"
+ FN.Comment.UpdatedAt = "updatedAt"
+ FN.Comment.UserId = "userId"
+ FN.Comment.UserName = "userName"
+ FN.Comment.UserAvatar = "userAvatar"
+ FN.Comment.ReviewStatus = "reviewStatus"
+ FN.Comment.Ip = "ip"
+ FN.Comment.CommentCount = "commentCount"
+ FN.Comment.CountLike = "countLike"
+ FN.Comment.ObjectName = "objectName"
+ FN.Comment.ObjectType = "objectType"
+ TN.Comment = "_comment"
+}
diff --git a/pub/models/commentmod/type.go b/pub/models/commentmod/type.go
new file mode 100644
index 0000000..0a6460c
--- /dev/null
+++ b/pub/models/commentmod/type.go
@@ -0,0 +1,31 @@
+package commentmod
+
+import (
+ "time"
+)
+
+// 评论管理
+// @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 int64 `json:"objectType" bson:"objectType"` // 对象类型
+ ObjectName string `json:"objectName" bson:"objectName"` // 对象名
+ UserId int64 `json:"userId" bson:"userId"` // 评论用户ID
+ UserName string `json:"userName" bson:"userName"` // 用户名称
+ UserAvatar string `json:"userAvatar" bson:"userAvatar"` // 评论用户头像
+ Level int64 `json:"level" bson:"level"` // 评论级别,1-一级评论 2-二级评论
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID,一级评论为0
+ ReplyCommentId int64 `json:"replyCommentId" bson:"replyCommentId"` // 被回复评论ID,一级评论为0
+ 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..dc48243
--- /dev/null
+++ b/pub/models/contactusmod/contactus.go
@@ -0,0 +1,100 @@
+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 coll *mongos.Collection
+
+// Init 初始化 contactus 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.ContactUs)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUs.UserId, Value: 1},
+ {Key: FN.ContactUs.CreatedAt, Value: -1},
+ {Key: FN.ContactUs.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUs.Status, Value: 1},
+ {Key: FN.ContactUs.CreatedAt, Value: -1},
+ {Key: FN.ContactUs.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUs.QuestionType, Value: 1},
+ {Key: FN.ContactUs.CreatedAt, Value: -1},
+ {Key: FN.ContactUs.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.ContactUs.CreatedAt, Value: -1},
+ {Key: FN.ContactUs.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(contactusmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneContactUs 插入一条联系我们数据
+//
+// 当 doc.Id 为空时,会使用 mongos.Lid.ContactUs 自动生成自增ID
+func InsertOneContactUs(ctx context.Context, doc *ContactUs) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.ContactUs.Get()
+ }
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("联系我们记录写入失败", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindManyContactUs 按条件查询多条联系我们数据
+func FindManyContactUs(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (ContactUsList, *srvdef.SrvErr) {
+ var data ContactUsList
+ 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, "", "")
+ }
+ return data, nil
+}
+
+// CountContactUs 统计满足条件的联系我们数量
+func CountContactUs(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
+}
+
+// UpdateManyContactUsWithResult 批量更新联系我们数据,并返回命中数量和修改数量
+func UpdateManyContactUsWithResult(ctx context.Context, filter bson.M, update bson.M) (matchedCount int64, modifiedCount int64, serr *srvdef.SrvErr) {
+ result, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("联系我们批量更新失败", log.E(err), log.Any("filter", filter))
+ return 0, 0, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.MatchedCount, result.ModifiedCount, nil
+}
diff --git a/pub/models/contactusmod/tag.go b/pub/models/contactusmod/tag.go
new file mode 100644
index 0000000..6e76124
--- /dev/null
+++ b/pub/models/contactusmod/tag.go
@@ -0,0 +1,40 @@
+package contactusmod
+
+// TagContactUs 保存contactus集合字段名
+type TagContactUs struct {
+ Id string
+ UserId string
+ Email string
+ QuestionType string
+ Title string
+ Content string
+ Attachments string
+ Status string
+ Remark string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ ContactUs TagContactUs
+}
+
+var TN struct {
+ ContactUs string
+}
+
+func init() {
+ FN.ContactUs.Id = "_id"
+ FN.ContactUs.UserId = "userId"
+ FN.ContactUs.Email = "email"
+ FN.ContactUs.QuestionType = "questionType"
+ FN.ContactUs.Title = "title"
+ FN.ContactUs.Content = "content"
+ FN.ContactUs.Attachments = "attachments"
+ FN.ContactUs.Status = "status"
+ FN.ContactUs.Remark = "remark"
+ FN.ContactUs.CreatedAt = "createdAt"
+ FN.ContactUs.UpdatedAt = "updatedAt"
+
+ TN.ContactUs = "contactus"
+}
diff --git a/pub/models/contactusmod/type.go b/pub/models/contactusmod/type.go
new file mode 100644
index 0000000..d50cd53
--- /dev/null
+++ b/pub/models/contactusmod/type.go
@@ -0,0 +1,34 @@
+package contactusmod
+
+import "time"
+
+// ContactUs @table: contactus
+//
+// ContactUs 联系我们集合模型
+type ContactUs struct {
+ // ID
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ // 用户ID
+ UserId int64 `json:"userId" bson:"userId"`
+ // 邮箱
+ Email string `json:"email" bson:"email"`
+ // 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ QuestionType int32 `json:"questionType" bson:"questionType"`
+ // 标题
+ Title string `json:"title" bson:"title"`
+ // 内容
+ Content string `json:"content" bson:"content"`
+ // 附件地址,可存放多张图片或文件地址
+ Attachments []string `json:"attachments" bson:"attachments"`
+ // 处理状态,0-待处理、1-已处理
+ Status int32 `json:"status" bson:"status"`
+ // 处理备注
+ Remark string `json:"remark" bson:"remark,omitempty"`
+ // 创建时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ // 更新时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
+}
+
+// ContactUsList 联系我们列表
+type ContactUsList []*ContactUs
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/eshelper.go b/pub/models/esmod/eshelper.go
new file mode 100644
index 0000000..2f2baee
--- /dev/null
+++ b/pub/models/esmod/eshelper.go
@@ -0,0 +1,58 @@
+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 = "webhgdj_video_index_v3"
+ playletIndex = "webhgdj_playlet_index_v1"
+)
+
+func GetVideoIndex() string {
+ return videoIndex
+}
+
+func GetPlayletIndex() string {
+ return playletIndex
+}
+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..1a8d3eb
--- /dev/null
+++ b/pub/models/esmod/esop.go
@@ -0,0 +1,277 @@
+package esmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "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
+ }
+ initPlayletIndex(playletIndex)
+}
+
+// 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 {
+ log.Warn("初始化视频ES索引失败,跳过索引创建", log.Any("索引", esIndex), log.E(err))
+ }
+}
+
+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.OperateType: esmanager.M{
+ "type": "integer",
+ },
+ 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.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.PlayName: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESPlaylet.Description: esmanager.M{
+ "type": "keyword",
+ "index": false,
+ },
+ FN.ESPlaylet.Slug: esmanager.M{
+ "type": "keyword",
+ },
+ 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.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",
+ },
+ 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,
+ },
+ },
+ },
+ },
+ },
+ }
+ if err := esmanager.Instance.InitIndices(esIndex, setting); err != nil {
+ log.Warn("初始化短剧ES索引失败,跳过索引创建", log.Any("索引", esIndex), log.E(err))
+ }
+}
diff --git a/pub/models/esmod/recordop.go b/pub/models/esmod/recordop.go
new file mode 100644
index 0000000..49ea08c
--- /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
+}
diff --git a/pub/models/esmod/tag.go b/pub/models/esmod/tag.go
new file mode 100755
index 0000000..e8b2e1f
--- /dev/null
+++ b/pub/models/esmod/tag.go
@@ -0,0 +1,295 @@
+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
+ OperateType string
+ ShowOnBanner string
+ Banner string
+ OnlineAt string
+ ThemeIds string
+ ThemeNames string
+ Title string
+ PlayName string
+ Description string
+ Slug string
+ Desc string
+ Anchor string
+ Cover string
+ TagIds string
+ TagNames string
+ TotalEpisode string
+ UpdateStatus string
+ CountBrowse string
+ HeatValue string
+ CountCollect string
+ CountLike string
+ UpdatedAt string
+ CreatedAt string
+ SeoInfo string
+}
+
+type TagESActress struct {
+ Id string
+ Alias string
+ Avatar string
+ Meas string
+ Office string
+ DebutDate string
+ BornDate string
+ Region string
+ Score string
+ Works 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
+}
+type TagESPost struct {
+ Id string
+ CreateId string
+ Title string
+ Cover string
+ Thumb string
+ TagIds string
+ Kind string
+ Nodes string
+ VideoId string
+ Status string
+ Remark string
+ Rank string
+ AddedTime string
+ Real struct {
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ WeekWatchs string
+ MonthWatchs string
+ TodayWatchs string
+ }
+ CreatedAt string
+ UpdatedAt string
+ Keyworlds string
+ FirstTag string
+ Desc string
+ IsHot string
+ Sort string
+}
+
+var FN struct {
+ ESRecord TagESRecord
+ ESVideo TagESVideo
+ ESPlaylet TagESPlaylet
+ ESActress TagESActress
+ ESPost TagESPost
+}
+
+var TN struct {
+ ESRecord string
+ ESVideo string
+ ESPlaylet string
+ ESActress string
+ ESPost 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",
+ OperateType: "operateType",
+ ShowOnBanner: "showOnBanner",
+ Banner: "banner",
+ OnlineAt: "onlineAt",
+ ThemeIds: "themeIds",
+ ThemeNames: "themeNames",
+ Title: "title",
+ PlayName: "playName",
+ Description: "description",
+ Slug: "slug",
+ Desc: "desc",
+ Anchor: "anchor",
+ Cover: "cover",
+ TagIds: "tagIds",
+ TagNames: "tagNames",
+ TotalEpisode: "totalEpisode",
+ UpdateStatus: "updateStatus",
+ CountBrowse: "countBrowse",
+ HeatValue: "heatValue",
+ CountCollect: "countCollect",
+ CountLike: "countLike",
+ UpdatedAt: "updatedAt",
+ CreatedAt: "createdAt",
+ SeoInfo: "seoInfo",
+ }
+
+ FN.ESActress = TagESActress{
+ Id: "_id",
+ Alias: "alias",
+ Avatar: "avatar",
+ Meas: "meas",
+ Office: "office",
+ DebutDate: "debutDate",
+ BornDate: "bornDate",
+ Region: "region",
+ Score: "score",
+ Works: "works",
+ 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",
+ }
+
+ FN.ESPost = TagESPost{
+ Id: "_id",
+ CreateId: "createId",
+ Title: "title",
+ Cover: "cover",
+ Thumb: "thumb",
+ TagIds: "tagIds",
+ Kind: "kind",
+ Nodes: "nodes",
+ VideoId: "videoId",
+ Status: "status",
+ Remark: "remark",
+ Rank: "rank",
+ AddedTime: "addedTime",
+ 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"},
+ CreatedAt: "createdAt",
+ UpdatedAt: "updatedAt",
+ Keyworlds: "keyworlds",
+ FirstTag: "firstTag",
+ Desc: "desc",
+ IsHot: "isHot",
+ Sort: "sort",
+ }
+
+ TN.ESRecord = "es_record"
+ TN.ESVideo = "es_video"
+ TN.ESPlaylet = "es_playlet"
+ TN.ESActress = "es_actress"
+ TN.ESPost = "es_post"
+}
diff --git a/pub/models/esmod/type.go b/pub/models/esmod/type.go
new file mode 100644
index 0000000..898f6ec
--- /dev/null
+++ b/pub/models/esmod/type.go
@@ -0,0 +1,134 @@
+//go:generate gen_bson_tag -m esmod
+//esmod essearch数据结构定义
+
+package esmod
+
+import (
+ "time"
+ "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 videoinformationmod.InformationSatus `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"` // 视频
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类Id
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ Real videoinformationmod.Flow `json:"real" bson:"real"` // 真实流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+}
+
+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"` // 是否爆剧(true-是,false-否)
+ IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 是否推荐 true-推荐,false-不推荐
+ OperateType playletmod.OperateType `json:"operateType" bson:"operateType"` // 运营类型 1-短剧 2-漫剧
+ ShowOnBanner bool `json:"showOnBanner" bson:"showOnBanner"` // 是否显示在首页banner
+ Banner string `json:"banner" bson:"banner"` // banner图
+ OnlineAt time.Time `json:"onlineAt" bson:"onlineAt"` // 上架时间
+ ThemeIds []int64 `json:"themeIds" bson:"themeIds"` // 题材ID集合
+ ThemeNames []string `json:"themeNames" bson:"themeNames"` // 题材名称集合
+ Title string `json:"title" bson:"title"` // 标题
+ PlayName string `json:"playName" bson:"playName"` // 剧名
+ Description string `json:"description" bson:"description"` // SEO Description
+ 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"` // 封面
+ 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"` // 展示点赞数
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+}
+
+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"`
+}
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/feedbackmod/feedback.go b/pub/models/feedbackmod/feedback.go
new file mode 100644
index 0000000..461955c
--- /dev/null
+++ b/pub/models/feedbackmod/feedback.go
@@ -0,0 +1,93 @@
+package feedbackmod
+
+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 coll *mongos.Collection
+
+// Init 初始化 feedback 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.Feedback)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Feedback.UserId, Value: 1},
+ {Key: FN.Feedback.CreatedAt, Value: -1},
+ {Key: FN.Feedback.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Feedback.Status, Value: 1},
+ {Key: FN.Feedback.CreatedAt, Value: -1},
+ {Key: FN.Feedback.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Feedback.CreatedAt, Value: -1},
+ {Key: FN.Feedback.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(feedbackmod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneFeedback 插入一条用户意见反馈数据
+//
+// 当 doc.Id 为空时,会使用 mongos.Lid.Feedback 自动生成自增ID
+func InsertOneFeedback(ctx context.Context, doc *Feedback) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.Feedback.Get()
+ }
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("用户意见反馈写入失败", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindManyFeedback 按条件查询多条用户意见反馈数据
+func FindManyFeedback(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (FeedbackList, *srvdef.SrvErr) {
+ var data FeedbackList
+ 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, "", "")
+ }
+ return data, nil
+}
+
+// CountFeedback 统计满足条件的用户意见反馈数量
+func CountFeedback(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
+}
+
+// UpdateManyFeedbackWithResult 批量更新用户意见反馈数据,并返回命中数量和修改数量
+func UpdateManyFeedbackWithResult(ctx context.Context, filter bson.M, update bson.M) (matchedCount int64, modifiedCount int64, serr *srvdef.SrvErr) {
+ result, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("用户意见反馈批量更新失败", log.E(err), log.Any("filter", filter))
+ return 0, 0, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.MatchedCount, result.ModifiedCount, nil
+}
diff --git a/pub/models/feedbackmod/tag.go b/pub/models/feedbackmod/tag.go
new file mode 100644
index 0000000..918a998
--- /dev/null
+++ b/pub/models/feedbackmod/tag.go
@@ -0,0 +1,38 @@
+package feedbackmod
+
+// TagFeedback 保存feedback集合字段名
+type TagFeedback struct {
+ Id string
+ UserId string
+ FeedbackType string
+ Content string
+ Screenshots string
+ Contact string
+ Status string
+ Remark string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ Feedback TagFeedback
+}
+
+var TN struct {
+ Feedback string
+}
+
+func init() {
+ FN.Feedback.Id = "_id"
+ FN.Feedback.UserId = "userId"
+ FN.Feedback.FeedbackType = "feedbackType"
+ FN.Feedback.Content = "content"
+ FN.Feedback.Screenshots = "screenshots"
+ FN.Feedback.Contact = "contact"
+ FN.Feedback.Status = "status"
+ FN.Feedback.Remark = "remark"
+ FN.Feedback.CreatedAt = "createdAt"
+ FN.Feedback.UpdatedAt = "updatedAt"
+
+ TN.Feedback = "feedback"
+}
diff --git a/pub/models/feedbackmod/type.go b/pub/models/feedbackmod/type.go
new file mode 100644
index 0000000..22b54ff
--- /dev/null
+++ b/pub/models/feedbackmod/type.go
@@ -0,0 +1,32 @@
+package feedbackmod
+
+import "time"
+
+// Feedback @table: feedback
+//
+// Feedback 用户意见反馈集合模型
+type Feedback struct {
+ // ID
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ // 用户ID
+ UserId int64 `json:"userId" bson:"userId"`
+ // 问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他
+ FeedbackType int32 `json:"feedbackType" bson:"feedbackType"`
+ // 描述内容
+ Content string `json:"content" bson:"content"`
+ // 问题截图,可存放多张图片地址
+ Screenshots []string `json:"screenshots" bson:"screenshots"`
+ // 联系方式,可为空
+ Contact string `json:"contact" bson:"contact,omitempty"`
+ // 处理状态,0-待处理、1-已处理
+ Status int32 `json:"status" bson:"status"`
+ // 处理备注
+ Remark string `json:"remark" bson:"remark,omitempty"`
+ // 创建时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
+ // 更新时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
+}
+
+// FeedbackList 用户意见反馈列表
+type FeedbackList []*Feedback
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/hotsearchmod/hotsearch.go b/pub/models/hotsearchmod/hotsearch.go
new file mode 100644
index 0000000..24747f9
--- /dev/null
+++ b/pub/models/hotsearchmod/hotsearch.go
@@ -0,0 +1,105 @@
+package hotsearchmod
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+ "unicode"
+ "unicode/utf8"
+ "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.HotSearch)
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{{Key: FN.HotSearch.Keyword, Value: 1}},
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.HotSearch.Count, Value: -1},
+ {Key: FN.HotSearch.UpdatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(hotsearchmod:Init) Create HotSearch Index failed! err:%+v:", err))
+ }
+}
+
+// NormalizeKeyword 去掉首尾空白、压缩中间空格,并限制长度。
+func NormalizeKeyword(raw string) string {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return ""
+ }
+ fields := strings.FieldsFunc(raw, unicode.IsSpace)
+ raw = strings.Join(fields, " ")
+ if utf8.RuneCountInString(raw) > MaxKeywordRunes {
+ raw = string([]rune(raw)[:MaxKeywordRunes])
+ }
+ return raw
+}
+
+// IncrByKeyword 按搜索词累加次数,不存在则插入。
+func IncrByKeyword(ctx context.Context, keyword string) *srvdef.SrvErr {
+ keyword = NormalizeKeyword(keyword)
+ if keyword == "" {
+ return nil
+ }
+ now := time.Now()
+ filter := bson.M{FN.HotSearch.Keyword: keyword}
+ update := bson.M{
+ "$inc": bson.M{FN.HotSearch.Count: 1},
+ "$set": bson.M{FN.HotSearch.UpdatedAt: now},
+ "$setOnInsert": bson.M{
+ FN.HotSearch.Id: mongos.Lid.HotSearch.Get(),
+ FN.HotSearch.Keyword: keyword,
+ FN.HotSearch.CreatedAt: now,
+ },
+ }
+ if _, err := db.UpsertOne(ctx, filter, update); err != nil {
+ if _, retryErr := db.UpdateOne(ctx, filter, bson.M{
+ "$inc": bson.M{FN.HotSearch.Count: 1},
+ "$set": bson.M{FN.HotSearch.UpdatedAt: now},
+ }); retryErr != nil {
+ log.Warn("hotsearchmod:IncrByKeyword failed", log.E(err), log.E(retryErr), log.Any("keyword", keyword))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ }
+ return nil
+}
+
+func FindTopHotSearch(ctx context.Context, limit int64) (HotSearchList, *srvdef.SrvErr) {
+ if limit <= 0 {
+ limit = TopLimit
+ }
+ opts := options.Find().
+ SetSort(bson.D{
+ {Key: FN.HotSearch.Count, Value: -1},
+ {Key: FN.HotSearch.UpdatedAt, Value: -1},
+ {Key: FN.HotSearch.Id, Value: -1},
+ }).
+ SetLimit(limit).
+ SetProjection(bson.D{
+ {Key: FN.HotSearch.Id, Value: 1},
+ {Key: FN.HotSearch.Keyword, Value: 1},
+ {Key: FN.HotSearch.Count, Value: 1},
+ })
+ data := make(HotSearchList, 0)
+ if err := db.Find(ctx, &data, bson.M{}, opts); err != nil {
+ log.Warn("hotsearchmod:FindTopHotSearch db.Find failed", log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
diff --git a/pub/models/hotsearchmod/hotsearch_test.go b/pub/models/hotsearchmod/hotsearch_test.go
new file mode 100644
index 0000000..e08de5a
--- /dev/null
+++ b/pub/models/hotsearchmod/hotsearch_test.go
@@ -0,0 +1,31 @@
+package hotsearchmod
+
+import "testing"
+
+func TestNormalizeKeyword(t *testing.T) {
+ tests := []struct {
+ name string
+ raw string
+ want string
+ }{
+ {name: "空串", raw: " ", want: ""},
+ {name: "压缩空白", raw: " 霸道 总裁\t ", want: "霸道 总裁"},
+ {name: "截断长度", raw: stringsRepeat("测", 60), want: stringsRepeat("测", MaxKeywordRunes)},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := NormalizeKeyword(tt.raw); got != tt.want {
+ t.Fatalf("NormalizeKeyword() = %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
+
+func stringsRepeat(s string, n int) string {
+ out := make([]rune, 0, n)
+ r := []rune(s)[0]
+ for i := 0; i < n; i++ {
+ out = append(out, r)
+ }
+ return string(out)
+}
diff --git a/pub/models/hotsearchmod/tag.go b/pub/models/hotsearchmod/tag.go
new file mode 100644
index 0000000..b641e51
--- /dev/null
+++ b/pub/models/hotsearchmod/tag.go
@@ -0,0 +1,26 @@
+package hotsearchmod
+
+type TagHotSearch struct {
+ Id string
+ Keyword string
+ Count string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ HotSearch TagHotSearch
+}
+
+var TN struct {
+ HotSearch string
+}
+
+func init() {
+ FN.HotSearch.Id = "_id"
+ FN.HotSearch.Keyword = "keyword"
+ FN.HotSearch.Count = "count"
+ FN.HotSearch.CreatedAt = "createdAt"
+ FN.HotSearch.UpdatedAt = "updatedAt"
+ TN.HotSearch = "hot_search"
+}
diff --git a/pub/models/hotsearchmod/type.go b/pub/models/hotsearchmod/type.go
new file mode 100644
index 0000000..780f102
--- /dev/null
+++ b/pub/models/hotsearchmod/type.go
@@ -0,0 +1,20 @@
+package hotsearchmod
+
+import "time"
+
+const (
+ MaxKeywordRunes = 50
+ TopLimit = 10
+)
+
+// HotSearch 热搜词
+// @table:hot_search
+type HotSearch struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Keyword string `json:"keyword" bson:"keyword"` // 搜索词
+ Count int64 `json:"count" bson:"count"` // 搜索次数
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 首次记录时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 最近搜索时间
+}
+
+type HotSearchList []*HotSearch
diff --git a/pub/models/ipblackmod/ipblack.go b/pub/models/ipblackmod/ipblack.go
new file mode 100644
index 0000000..98de34b
--- /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
+}
diff --git a/pub/models/ipblackmod/tag.go b/pub/models/ipblackmod/tag.go
new file mode 100755
index 0000000..6bf762a
--- /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"
+}
diff --git a/pub/models/ipblackmod/type.go b/pub/models/ipblackmod/type.go
new file mode 100644
index 0000000..8264bf8
--- /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
diff --git a/pub/models/ipdailystatmod/ipdailystat.go b/pub/models/ipdailystatmod/ipdailystat.go
new file mode 100644
index 0000000..023d831
--- /dev/null
+++ b/pub/models/ipdailystatmod/ipdailystat.go
@@ -0,0 +1,59 @@
+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)
+
+ 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)
+}
+
+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
+}
+
+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..c9d72b4
--- /dev/null
+++ b/pub/models/ipdailystatmod/type.go
@@ -0,0 +1,12 @@
+package ipdailystatmod
+
+import "time"
+
+// IpDailyStat 每日访客 IP 去重统计。
+// @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..21af904
--- /dev/null
+++ b/pub/models/models.go
@@ -0,0 +1,99 @@
+package models
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/announcementmod"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/models/commentlikemod"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/contactusmod"
+ "videoweb/pub/models/detailnavmod"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/feedbackmod"
+ "videoweb/pub/models/homebannermod"
+ "videoweb/pub/models/hotsearchmod"
+ "videoweb/pub/models/ipblackmod"
+ "videoweb/pub/models/ipdailystatmod"
+ "videoweb/pub/models/m3u8requestlogmod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/oplog"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptercollectmod"
+ "videoweb/pub/models/playletchapterlikemod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletchapterunlockmod"
+ "videoweb/pub/models/playletcollectmod"
+ "videoweb/pub/models/playletlikemod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/models/playletwatchhistorymod"
+ "videoweb/pub/models/playletwatchprogressmod"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/models/richtextcontentmod"
+ "videoweb/pub/models/syncfilemod"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/models/userlogin"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/uservisitlog"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/pub/models/videotopicmod"
+ "videoweb/pub/models/vipordermod"
+)
+
+func Init() {
+ log.Info("models init start..")
+ ctx := context.Background()
+ cfg.Init(ctx)
+ mwacc.Init(ctx)
+ oplog.Init(ctx)
+ m3u8requestlogmod.Init(ctx)
+
+ syncfilemod.Init(ctx)
+ richtextcontentmod.Init(ctx)
+ announcementmod.Init(ctx)
+ hotsearchmod.Init(ctx)
+ homebannermod.Init(ctx)
+ commentmod.Init(ctx)
+ commentlikemod.Init(ctx)
+ ipblackmod.Init(ctx)
+ ipdailystatmod.Init(ctx)
+ esmod.Init(ctx)
+ detailnavmod.Init(ctx)
+ eventmod.Init(ctx)
+ translationcategorymod.Init(ctx)
+ translationmod.Init(ctx)
+ playletcategorymod.Init(ctx)
+ playletthememod.Init(ctx)
+ playlettagmod.Init(ctx)
+ playletmod.Init(ctx)
+ playletchaptermod.Init(ctx)
+ playletchapterlikemod.Init(ctx)
+ playletchaptercollectmod.Init(ctx)
+ playletchapterunlockmod.Init(ctx)
+ playletlikemod.Init(ctx)
+ playletcollectmod.Init(ctx)
+ playletwatchhistorymod.Init(ctx)
+ playletwatchprogressmod.Init(ctx)
+ feedbackmod.Init(ctx)
+ videocategorymod.Init(ctx)
+ videocreatermod.Init(ctx)
+ videotagmod.Init(ctx)
+ videoinformationmod.Init(ctx)
+ videotopicmod.Init(ctx)
+ rechargepackagemod.Init(ctx)
+ cointransmod.Init(ctx)
+ contactusmod.Init(ctx)
+ vipordermod.Init(ctx)
+ usermod.Init(ctx)
+ userlogin.Init(ctx)
+ uservisitlog.Init(ctx)
+ log.Info("models init end")
+}
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/playletcategorymod/category.go b/pub/models/playletcategorymod/category.go
new file mode 100644
index 0000000..88e693f
--- /dev/null
+++ b/pub/models/playletcategorymod/category.go
@@ -0,0 +1,126 @@
+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.Slug, 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..25a3391
--- /dev/null
+++ b/pub/models/playletcategorymod/tag.go
@@ -0,0 +1,46 @@
+package playletcategorymod
+
+// TagPlayletCategory 保存 playlet_category 集合字段名。
+type TagPlayletCategory struct {
+ Id string
+ Name string
+ Slug string
+ Desc 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.Desc = "desc"
+ 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..ccc42e7
--- /dev/null
+++ b/pub/models/playletcategorymod/type.go
@@ -0,0 +1,26 @@
+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
+ Desc string `json:"desc" bson:"desc"` // 描述
+ 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/playletchaptercollectmod/collect.go b/pub/models/playletchaptercollectmod/collect.go
new file mode 100644
index 0000000..188a035
--- /dev/null
+++ b/pub/models/playletchaptercollectmod/collect.go
@@ -0,0 +1,115 @@
+package playletchaptercollectmod
+
+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_chapter_collect 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletChapterCollect)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterCollect.UserId, Value: 1},
+ {Key: FN.PlayletChapterCollect.ChapterId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterCollect.ChapterId, Value: 1},
+ {Key: FN.PlayletChapterCollect.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterCollect.PlayletId, Value: 1},
+ {Key: FN.PlayletChapterCollect.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterCollect.UserId, Value: 1},
+ {Key: FN.PlayletChapterCollect.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletchaptercollectmod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletChapterCollect 插入一条短剧剧集收藏关系
+func InsertOnePlayletChapterCollect(ctx context.Context, doc *PlayletChapterCollect) (bool, *srvdef.SrvErr) {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletChapterCollect.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) {
+ return false, nil
+ }
+ log.Warn("插入短剧剧集收藏关系失败", log.Any("doc", doc), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// DeleteOnePlayletChapterCollect 删除一条短剧剧集收藏关系
+func DeleteOnePlayletChapterCollect(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("删除短剧剧集收藏关系失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DeleteManyPlayletChapterCollect 按条件批量删除短剧剧集收藏关系
+func DeleteManyPlayletChapterCollect(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
+}
+
+// FindManyPlayletChapterCollect 按条件查询短剧剧集收藏关系
+func FindManyPlayletChapterCollect(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletChapterCollectList, *srvdef.SrvErr) {
+ data := make(PlayletChapterCollectList, 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, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletChapterCollect 统计满足条件的短剧剧集收藏关系数量
+func CountPlayletChapterCollect(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
+}
diff --git a/pub/models/playletchaptercollectmod/tag.go b/pub/models/playletchaptercollectmod/tag.go
new file mode 100644
index 0000000..a76d74d
--- /dev/null
+++ b/pub/models/playletchaptercollectmod/tag.go
@@ -0,0 +1,30 @@
+package playletchaptercollectmod
+
+// TagPlayletChapterCollect 保存 playlet_chapter_collect 集合字段名
+type TagPlayletChapterCollect struct {
+ Id string
+ UserId string
+ PlayletId string
+ ChapterId string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletChapterCollect TagPlayletChapterCollect
+}
+
+var TN struct {
+ PlayletChapterCollect string
+}
+
+func init() {
+ FN.PlayletChapterCollect.Id = "_id"
+ FN.PlayletChapterCollect.UserId = "userId"
+ FN.PlayletChapterCollect.PlayletId = "playletId"
+ FN.PlayletChapterCollect.ChapterId = "chapterId"
+ FN.PlayletChapterCollect.CreatedAt = "createdAt"
+ FN.PlayletChapterCollect.UpdatedAt = "updatedAt"
+
+ TN.PlayletChapterCollect = "playlet_chapter_collect"
+}
diff --git a/pub/models/playletchaptercollectmod/type.go b/pub/models/playletchaptercollectmod/type.go
new file mode 100644
index 0000000..d5ddbac
--- /dev/null
+++ b/pub/models/playletchaptercollectmod/type.go
@@ -0,0 +1,18 @@
+package playletchaptercollectmod
+
+import "time"
+
+// PlayletChapterCollect @table: playlet_chapter_collect
+//
+// PlayletChapterCollect 短剧剧集收藏关系模型
+type PlayletChapterCollect struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 收藏记录ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ ChapterId int64 `json:"chapterId" bson:"chapterId"` // 短剧剧集ID
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletChapterCollectList 短剧剧集收藏关系列表
+type PlayletChapterCollectList []*PlayletChapterCollect
diff --git a/pub/models/playletchapterlikemod/like.go b/pub/models/playletchapterlikemod/like.go
new file mode 100644
index 0000000..a34f9b8
--- /dev/null
+++ b/pub/models/playletchapterlikemod/like.go
@@ -0,0 +1,105 @@
+package playletchapterlikemod
+
+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_chapter_like 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletChapterLike)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterLike.UserId, Value: 1},
+ {Key: FN.PlayletChapterLike.ChapterId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterLike.ChapterId, Value: 1},
+ {Key: FN.PlayletChapterLike.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterLike.PlayletId, Value: 1},
+ {Key: FN.PlayletChapterLike.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterLike.UserId, Value: 1},
+ {Key: FN.PlayletChapterLike.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletchapterlikemod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletChapterLike 插入一条短剧剧集点赞关系
+func InsertOnePlayletChapterLike(ctx context.Context, doc *PlayletChapterLike) (bool, *srvdef.SrvErr) {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletChapterLike.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) {
+ return false, nil
+ }
+ log.Warn("插入短剧剧集点赞关系失败", log.Any("doc", doc), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// DeleteOnePlayletChapterLike 删除一条短剧剧集点赞关系
+func DeleteOnePlayletChapterLike(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("删除短剧剧集点赞关系失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DeleteManyPlayletChapterLike 按条件批量删除短剧剧集点赞关系
+func DeleteManyPlayletChapterLike(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
+}
+
+// FindManyPlayletChapterLike 按条件查询短剧剧集点赞关系
+func FindManyPlayletChapterLike(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletChapterLikeList, *srvdef.SrvErr) {
+ data := make(PlayletChapterLikeList, 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, "", "")
+ }
+ return data, nil
+}
diff --git a/pub/models/playletchapterlikemod/tag.go b/pub/models/playletchapterlikemod/tag.go
new file mode 100644
index 0000000..91b7a72
--- /dev/null
+++ b/pub/models/playletchapterlikemod/tag.go
@@ -0,0 +1,30 @@
+package playletchapterlikemod
+
+// TagPlayletChapterLike 保存 playlet_chapter_like 集合字段名
+type TagPlayletChapterLike struct {
+ Id string
+ UserId string
+ PlayletId string
+ ChapterId string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletChapterLike TagPlayletChapterLike
+}
+
+var TN struct {
+ PlayletChapterLike string
+}
+
+func init() {
+ FN.PlayletChapterLike.Id = "_id"
+ FN.PlayletChapterLike.UserId = "userId"
+ FN.PlayletChapterLike.PlayletId = "playletId"
+ FN.PlayletChapterLike.ChapterId = "chapterId"
+ FN.PlayletChapterLike.CreatedAt = "createdAt"
+ FN.PlayletChapterLike.UpdatedAt = "updatedAt"
+
+ TN.PlayletChapterLike = "playlet_chapter_like"
+}
diff --git a/pub/models/playletchapterlikemod/type.go b/pub/models/playletchapterlikemod/type.go
new file mode 100644
index 0000000..c5a13f6
--- /dev/null
+++ b/pub/models/playletchapterlikemod/type.go
@@ -0,0 +1,18 @@
+package playletchapterlikemod
+
+import "time"
+
+// PlayletChapterLike @table: playlet_chapter_like
+//
+// PlayletChapterLike 短剧剧集点赞关系模型
+type PlayletChapterLike struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 点赞记录ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ ChapterId int64 `json:"chapterId" bson:"chapterId"` // 短剧剧集ID
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletChapterLikeList 短剧剧集点赞关系列表
+type PlayletChapterLikeList []*PlayletChapterLike
diff --git a/pub/models/playletchaptermod/chapter.go b/pub/models/playletchaptermod/chapter.go
new file mode 100644
index 0000000..273c7d3
--- /dev/null
+++ b/pub/models/playletchaptermod/chapter.go
@@ -0,0 +1,247 @@
+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
+
+const duplicatePlayletChapterMsg = "短剧剧集已存在"
+
+// Init 初始化 playlet_chapter 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletChapter)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ commonIndexes := []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.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, commonIndexes); err != nil {
+ panic(fmt.Sprintf("(playletchaptermod:initIndex) Create Index failed! err:%+v:", err))
+ }
+
+ uniqueIndexes := []mongo.IndexModel{
+ {
+ // 反向键序避免和线上已有 playletId_1_currentEpisode_1 普通索引冲突,同时兜住同一短剧同一集数的并发重复写入。
+ Keys: bson.D{
+ {Key: FN.PlayletChapter.CurrentEpisode, Value: 1},
+ {Key: FN.PlayletChapter.PlayletId, Value: 1},
+ },
+ Options: options.Index().
+ SetName("uniq_playlet_chapter_episode").
+ SetUnique(true),
+ },
+ {
+ 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},
+ }),
+ },
+ }
+ for _, index := range uniqueIndexes {
+ if _, err := coll.CreateIndex(ctx, []mongo.IndexModel{index}); err != nil {
+ if isPlayletChapterDuplicateIndexErr(err) {
+ log.Warn("短剧章节唯一索引创建失败,请先清理历史重复章节数据", log.E(err), log.Any("index", index.Keys))
+ continue
+ }
+ panic(fmt.Sprintf("(playletchaptermod:initIndex) Create Unique Index failed! err:%+v:", err))
+ }
+ }
+}
+
+// isPlayletChapterDuplicateIndexErr 判断唯一索引创建时是否命中历史重复数据。
+func isPlayletChapterDuplicateIndexErr(err error) bool {
+ if err == nil {
+ return false
+ }
+ return mongo.IsDuplicateKeyError(err) || strings.Contains(err.Error(), "E11000 duplicate key error")
+}
+
+// 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 {
+ if mongo.IsDuplicateKeyError(err) {
+ return duplicatePlayletChapterErr()
+ }
+ 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 {
+ if mongo.IsDuplicateKeyError(err) {
+ return duplicatePlayletChapterErr()
+ }
+ log.Warn("playletchaptermod:InsertManyPlayletChapter failed", log.Any("count", len(data)), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// duplicatePlayletChapterErr 返回章节唯一约束冲突的统一业务错误。
+func duplicatePlayletChapterErr() *srvdef.SrvErr {
+ return srvdef.New(srvdef.ErrDbInputExist, duplicatePlayletChapterMsg, duplicatePlayletChapterMsg)
+}
+
+// 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 {
+ if mongo.IsDuplicateKeyError(err) {
+ return duplicatePlayletChapterErr()
+ }
+ 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
+}
+
+// UpdateManyPlayletChapter 批量更新章节数据
+func UpdateManyPlayletChapter(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ return duplicatePlayletChapterErr()
+ }
+ 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/playletchapterunlockmod/tag.go b/pub/models/playletchapterunlockmod/tag.go
new file mode 100644
index 0000000..6848160
--- /dev/null
+++ b/pub/models/playletchapterunlockmod/tag.go
@@ -0,0 +1,34 @@
+package playletchapterunlockmod
+
+// TagPlayletChapterUnlock 保存 playlet_chapter_unlock 集合字段名。
+type TagPlayletChapterUnlock struct {
+ Id string
+ UserId string
+ PlayletId string
+ ChapterId string
+ Coins string
+ OrderNo string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletChapterUnlock TagPlayletChapterUnlock
+}
+
+var TN struct {
+ PlayletChapterUnlock string
+}
+
+func init() {
+ FN.PlayletChapterUnlock.Id = "_id"
+ FN.PlayletChapterUnlock.UserId = "userId"
+ FN.PlayletChapterUnlock.PlayletId = "playletId"
+ FN.PlayletChapterUnlock.ChapterId = "chapterId"
+ FN.PlayletChapterUnlock.Coins = "coins"
+ FN.PlayletChapterUnlock.OrderNo = "orderNo"
+ FN.PlayletChapterUnlock.CreatedAt = "createdAt"
+ FN.PlayletChapterUnlock.UpdatedAt = "updatedAt"
+
+ TN.PlayletChapterUnlock = "playlet_chapter_unlock"
+}
diff --git a/pub/models/playletchapterunlockmod/type.go b/pub/models/playletchapterunlockmod/type.go
new file mode 100644
index 0000000..d9b374c
--- /dev/null
+++ b/pub/models/playletchapterunlockmod/type.go
@@ -0,0 +1,19 @@
+package playletchapterunlockmod
+
+import "time"
+
+// PlayletChapterUnlock @table: playlet_chapter_unlock
+//
+// PlayletChapterUnlock 记录用户已用金币解锁的短剧剧集;一条记录代表该用户永久可观看该剧集。
+type PlayletChapterUnlock struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 解锁记录ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ ChapterId int64 `json:"chapterId" bson:"chapterId"` // 短剧剧集ID
+ Coins int64 `json:"coins" bson:"coins"` // 解锁消耗金币
+ OrderNo string `json:"orderNo" bson:"orderNo"` // 解锁流水号
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+type PlayletChapterUnlockList []*PlayletChapterUnlock
diff --git a/pub/models/playletchapterunlockmod/unlock.go b/pub/models/playletchapterunlockmod/unlock.go
new file mode 100644
index 0000000..5629c65
--- /dev/null
+++ b/pub/models/playletchapterunlockmod/unlock.go
@@ -0,0 +1,107 @@
+package playletchapterunlockmod
+
+import (
+ "context"
+ "fmt"
+ "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/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+// Init 初始化 playlet_chapter_unlock 集合和索引。
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletChapterUnlock)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterUnlock.UserId, Value: 1},
+ {Key: FN.PlayletChapterUnlock.ChapterId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletChapterUnlock.UserId, Value: 1},
+ {Key: FN.PlayletChapterUnlock.PlayletId, Value: 1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletchapterunlockmod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletChapterUnlock 插入解锁记录。
+//
+// 返回 inserted=false 表示该用户已经解锁过该剧集;调用方应将其视为幂等成功,不可继续扣费。
+func InsertOnePlayletChapterUnlock(ctx context.Context, doc *PlayletChapterUnlock) (bool, *srvdef.SrvErr) {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletChapterUnlock.Get()
+ }
+ now := utils.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ return false, nil
+ }
+ log.Warn("插入短剧剧集解锁记录失败", log.Any("doc", doc), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// ExistsPlayletChapterUnlock 判断用户是否已解锁指定剧集。
+func ExistsPlayletChapterUnlock(ctx context.Context, userId int64, chapterId int64) (bool, *srvdef.SrvErr) {
+ if userId <= 0 || chapterId <= 0 {
+ return false, nil
+ }
+ var doc PlayletChapterUnlock
+ err := coll.FindOneEx(ctx, &doc, bson.M{
+ FN.PlayletChapterUnlock.UserId: userId,
+ FN.PlayletChapterUnlock.ChapterId: chapterId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: FN.PlayletChapterUnlock.Id, Value: 1},
+ }))
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ log.Warn("查询短剧剧集解锁记录失败", log.E(err), log.Any("userId", userId), log.Any("chapterId", chapterId))
+ return false, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return doc.Id > 0, nil
+}
+
+// DeleteManyPlayletChapterUnlock 按条件批量删除剧集解锁记录。
+func DeleteManyPlayletChapterUnlock(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
+}
+
+// FindManyPlayletChapterUnlock 按条件查询多条剧集解锁记录。
+func FindManyPlayletChapterUnlock(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletChapterUnlockList, *srvdef.SrvErr) {
+ data := make(PlayletChapterUnlockList, 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, "", "")
+ }
+ return data, nil
+}
diff --git a/pub/models/playletcollectmod/collect.go b/pub/models/playletcollectmod/collect.go
new file mode 100644
index 0000000..1358549
--- /dev/null
+++ b/pub/models/playletcollectmod/collect.go
@@ -0,0 +1,115 @@
+package playletcollectmod
+
+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_collect 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletCollect)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletCollect.UserId, Value: 1},
+ {Key: FN.PlayletCollect.PlayletId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletCollect.PlayletId, Value: 1},
+ {Key: FN.PlayletCollect.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletCollect.UserId, Value: 1},
+ {Key: FN.PlayletCollect.CreatedAt, Value: -1},
+ {Key: FN.PlayletCollect.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletcollectmod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletCollect 插入一条短剧收藏关系
+func InsertOnePlayletCollect(ctx context.Context, doc *PlayletCollect) (bool, *srvdef.SrvErr) {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletCollect.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) {
+ return false, nil
+ }
+ log.Warn("插入短剧收藏关系失败", log.Any("doc", doc), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// ExistsPlayletCollect 查询短剧收藏关系是否存在
+func ExistsPlayletCollect(ctx context.Context, filter bson.M) (bool, *srvdef.SrvErr) {
+ var doc PlayletCollect
+ if err := coll.FindOneEx(ctx, &doc, filter, options.FindOne().SetProjection(bson.D{
+ {Key: FN.PlayletCollect.Id, Value: 1},
+ })); err != nil {
+ if err == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ log.Warn("查询短剧收藏关系失败", log.E(err), log.Any("filter", filter))
+ return false, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return true, nil
+}
+
+// DeleteOnePlayletCollect 删除一条短剧收藏关系
+func DeleteOnePlayletCollect(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("删除短剧收藏关系失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindManyPlayletCollect 按条件查询短剧收藏关系列表
+func FindManyPlayletCollect(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletCollectList, *srvdef.SrvErr) {
+ data := make(PlayletCollectList, 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, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletCollect 统计满足条件的短剧收藏关系数量
+func CountPlayletCollect(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
+}
diff --git a/pub/models/playletcollectmod/tag.go b/pub/models/playletcollectmod/tag.go
new file mode 100644
index 0000000..9d54a0b
--- /dev/null
+++ b/pub/models/playletcollectmod/tag.go
@@ -0,0 +1,28 @@
+package playletcollectmod
+
+// TagPlayletCollect 保存 playlet_collect 集合字段名
+type TagPlayletCollect struct {
+ Id string
+ UserId string
+ PlayletId string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletCollect TagPlayletCollect
+}
+
+var TN struct {
+ PlayletCollect string
+}
+
+func init() {
+ FN.PlayletCollect.Id = "_id"
+ FN.PlayletCollect.UserId = "userId"
+ FN.PlayletCollect.PlayletId = "playletId"
+ FN.PlayletCollect.CreatedAt = "createdAt"
+ FN.PlayletCollect.UpdatedAt = "updatedAt"
+
+ TN.PlayletCollect = "playlet_collect"
+}
diff --git a/pub/models/playletcollectmod/type.go b/pub/models/playletcollectmod/type.go
new file mode 100644
index 0000000..a715685
--- /dev/null
+++ b/pub/models/playletcollectmod/type.go
@@ -0,0 +1,17 @@
+package playletcollectmod
+
+import "time"
+
+// PlayletCollect @table: playlet_collect
+//
+// PlayletCollect 短剧收藏关系模型
+type PlayletCollect struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 收藏记录ID
+ 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"` // 修改时间
+}
+
+// PlayletCollectList 短剧收藏关系列表
+type PlayletCollectList []*PlayletCollect
diff --git a/pub/models/playletlikemod/like.go b/pub/models/playletlikemod/like.go
new file mode 100644
index 0000000..6ccd587
--- /dev/null
+++ b/pub/models/playletlikemod/like.go
@@ -0,0 +1,88 @@
+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},
+ {Key: FN.PlayletLike.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletlikemod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// InsertOnePlayletLike 插入一条短剧点赞关系
+func InsertOnePlayletLike(ctx context.Context, doc *PlayletLike) (bool, *srvdef.SrvErr) {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.PlayletLike.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) {
+ return false, nil
+ }
+ log.Warn("插入短剧点赞关系失败", log.Any("doc", doc), log.E(err))
+ return false, srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return true, nil
+}
+
+// ExistsPlayletLike 查询短剧点赞关系是否存在
+func ExistsPlayletLike(ctx context.Context, filter bson.M) (bool, *srvdef.SrvErr) {
+ var doc PlayletLike
+ if err := coll.FindOneEx(ctx, &doc, filter, options.FindOne().SetProjection(bson.D{
+ {Key: FN.PlayletLike.Id, Value: 1},
+ })); err != nil {
+ if err == mongo.ErrNoDocuments {
+ return false, nil
+ }
+ log.Warn("查询短剧点赞关系失败", log.E(err), log.Any("filter", filter))
+ return false, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return true, nil
+}
+
+// DeleteOnePlayletLike 删除一条短剧点赞关系
+func DeleteOnePlayletLike(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("删除短剧点赞关系失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
diff --git a/pub/models/playletlikemod/tag.go b/pub/models/playletlikemod/tag.go
new file mode 100644
index 0000000..677e519
--- /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..4b4aad9
--- /dev/null
+++ b/pub/models/playletlikemod/type.go
@@ -0,0 +1,17 @@
+package playletlikemod
+
+import "time"
+
+// PlayletLike @table: playlet_like
+//
+// PlayletLike 短剧点赞关系模型
+type PlayletLike struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 点赞记录ID
+ 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..db060b0
--- /dev/null
+++ b/pub/models/playletmod/category_bind.go
@@ -0,0 +1,153 @@
+package playletmod
+
+import "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)
+}
+
+// ApplyCategoryBinds 写入分类绑定并同步 categoryIds。
+func ApplyCategoryBinds(item *Playlet, binds []PlayletCategoryBind) {
+ if item == nil {
+ return
+ }
+ item.CategoryBinds = NormalizeCategoryBinds(binds)
+ item.CategoryIds = CategoryIdsFromBinds(item.CategoryBinds)
+}
+
+// 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},
+ }
+}
diff --git a/pub/models/playletmod/playlet.go b/pub/models/playletmod/playlet.go
new file mode 100644
index 0000000..7b0e0a5
--- /dev/null
+++ b/pub/models/playletmod/playlet.go
@@ -0,0 +1,331 @@
+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.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.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.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},
+ },
+ },
+ {
+ 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.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.Slug, 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},
+ }),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.OperateType, Value: 1},
+ {Key: FN.Playlet.Status, Value: 1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Playlet.ContentType, Value: 1},
+ {Key: FN.Playlet.CreatedAt, Value: -1},
+ {Key: FN.Playlet.Id, Value: -1},
+ },
+ Options: options.Index().
+ 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
+}
+
+// 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
+}
+
+// FindManyPlayletByCategoryRank 按指定分类的排序分页查询短剧。
+func FindManyPlayletByCategoryRank(ctx context.Context, filter bson.M, categoryId int64, skip, limit int64, sort bson.D, projection bson.D) (PlayletList, *srvdef.SrvErr) {
+ if skip < 0 {
+ skip = 0
+ }
+ if limit <= 0 {
+ limit = 10
+ }
+ if sort == nil {
+ sort = CategoryRankSort()
+ }
+ pipeline := []bson.M{
+ {"$match": filter},
+ {"$addFields": CategoryRankAddFields(categoryId)},
+ {"$sort": sort},
+ {"$skip": skip},
+ {"$limit": limit},
+ }
+ pipeline = append(pipeline, bson.M{"$unset": categoryRankField})
+ if len(projection) > 0 {
+ proj := bson.M{}
+ for _, field := range projection {
+ proj[field.Key] = field.Value
+ }
+ pipeline = append(pipeline, bson.M{"$project": proj})
+ }
+ list := make(PlayletList, 0)
+ if err := AggregatePlaylet(ctx, &list, pipeline); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// AggregatePlaylet 聚合查询短剧
+func AggregatePlaylet(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("playletmod:AggregatePlaylet failed", log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletmod/tag.go b/pub/models/playletmod/tag.go
new file mode 100644
index 0000000..0bec3db
--- /dev/null
+++ b/pub/models/playletmod/tag.go
@@ -0,0 +1,120 @@
+package playletmod
+
+// TagPlaylet 保存 playlet 集合字段名
+type TagPlaylet struct {
+ Id string
+ MediaCenterId string
+ MediaSource string
+ MediaCreatedAt string
+ MediaUpdatedAt string
+ MediaSyncTime string
+ Status string
+ IsFeatured string
+ IsOriginal string
+ IsRecommend string
+ ShowOnBanner string
+ Banner string
+ ReviewAt string
+ OnlineAt string
+ CategoryIds string
+ CategoryBinds string
+ ThemeIds string
+ Title string
+ PlayName string
+ Description string
+ Slug string
+ Desc string
+ Anchor string
+ Cover string
+ CompressedCover string
+ Preview string
+ TagIds string
+ ContentType string
+ OperateType string
+ PayType string
+ SingleCoins string
+ TotalEpisode string
+ UpdateStatus string
+ TopRank string
+ RecommendRank string
+ CountComment string
+ CountBuy string
+ CountBrowse string
+ HeatValue string
+ CountCollect string
+ CountLike string
+ VirtualCountLike string
+ CountDisLike string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title 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.MediaSource = "mediaSource"
+ 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.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.PlayName = "playName"
+ FN.Playlet.Description = "description"
+ 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.ContentType = "contentType"
+ FN.Playlet.OperateType = "operateType"
+ FN.Playlet.PayType = "payType"
+ FN.Playlet.SingleCoins = "singleCoins"
+ FN.Playlet.TotalEpisode = "totalEpisode"
+ 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.HeatValue = "heatValue"
+ 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.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..0a98cac
--- /dev/null
+++ b/pub/models/playletmod/type.go
@@ -0,0 +1,149 @@
+package playletmod
+
+import (
+ "strconv"
+ "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
+)
+
+// OperateType 运营类型
+type OperateType int32
+
+const (
+ OperateTypePlaylet OperateType = 1 // 短剧
+ OperateTypeComic OperateType = 2 // 漫剧
+)
+
+// NormalizeOperateType 历史数据未写入时按短剧处理
+func NormalizeOperateType(t OperateType) OperateType {
+ if t == OperateTypeComic {
+ return OperateTypeComic
+ }
+ return OperateTypePlaylet
+}
+
+// IsValidOperateType 校验运营类型
+func IsValidOperateType(t OperateType) bool {
+ return t == OperateTypePlaylet || t == OperateTypeComic
+}
+
+const (
+ UpdateStatusSerial = 1 // 连载中
+ UpdateStatusDone = 2 // 已完结
+)
+
+const (
+ MediaSourceDefault = "" // 普通(嘉华)媒资
+ MediaSourceHG = "hg" // 黄果媒资
+)
+
+// BuildPlayNameDescription 按剧名和连载状态生成 SEO Description。
+func BuildPlayNameDescription(desc, playName string, updateStatus int, totalEpisode int64) string {
+ playName = strings.TrimSpace(playName)
+ if playName == "" {
+ return ""
+ }
+ desc = strings.TrimSpace(desc)
+ siteName := pubdef.AppName
+ if updateStatus == UpdateStatusDone {
+ return desc + siteName + "提供《" + playName + "》全集在线观看、剧情介绍及完整选集,并持续更新相关热门短剧。"
+ }
+ return desc + siteName + "提供《" + playName + "》最新剧集、剧情介绍及在线观看,当前更新至第" + strconv.FormatInt(totalEpisode, 10) + "集,后续内容持续更新。"
+}
+
+// Playlet @table: playlet
+//
+// Playlet 连载短剧集合模型
+type Playlet struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ MediaCenterId uint `json:"mediaCenterId" bson:"mediaCenterId"` // 媒资中心资源ID
+ MediaSource string `json:"mediaSource" bson:"mediaSource"` // 媒资来源,空值为普通媒资
+ 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
+ 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 同步
+ CategoryBinds []PlayletCategoryBind `json:"categoryBinds" bson:"categoryBinds"` // 分类及该分类下的排序
+ ThemeIds []int64 `json:"themeIds" bson:"themeIds"` // 题材ID集合
+ Title string `json:"title" bson:"title"` // 标题
+ PlayName string `json:"playName" bson:"playName"` // 剧名,后台维护
+ Description string `json:"description" bson:"description"` // SEO Description,按剧名规则自动生成
+ 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
+ ContentType ContentType `json:"contentType" bson:"contentType"` // 内容类型: 0短剧 1普通短剧/影集 2漫画短剧 3动漫 4漫画
+ OperateType OperateType `json:"operateType" bson:"operateType"` // 运营类型: 1短剧 2漫剧
+ PayType PayType `json:"payType" bson:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins" bson:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ TotalEpisode int64 `json:"totalEpisode" bson:"totalEpisode"` // 总集数
+ 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"` // 浏览数
+ HeatValue int64 `json:"heatValue" bson:"heatValue"` // 热度值
+ 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相关信息
+ 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"` // 修改时间
+}
+
+// PlayletList 短剧列表
+type PlayletList []*Playlet
+
+// 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..7fcf214
--- /dev/null
+++ b/pub/models/playlettagmod/playlettag.go
@@ -0,0 +1,126 @@
+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.Slug, 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..39bf6ca
--- /dev/null
+++ b/pub/models/playlettagmod/tag.go
@@ -0,0 +1,40 @@
+package playlettagmod
+
+// TagPlayletTag 保存 playlet_tag 集合字段名
+type TagPlayletTag struct {
+ Id string
+ Name string
+ Slug 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.Slug = "slug"
+ 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..9e5a3b6
--- /dev/null
+++ b/pub/models/playlettagmod/type.go
@@ -0,0 +1,23 @@
+package playlettagmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// PlayletTag @table: playlet_tag
+//
+// PlayletTag 短剧标签集合模型
+type PlayletTag 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"` // 上架状态
+ 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..d8de4ec
--- /dev/null
+++ b/pub/models/playletthememod/tag.go
@@ -0,0 +1,44 @@
+package playletthememod
+
+// TagPlayletTheme 保存 playlet_theme 集合字段名
+type TagPlayletTheme struct {
+ Id string
+ Name string
+ Slug string
+ Icon string
+ Status string
+ IsRecommend 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.IsRecommend = "isRecommend"
+ 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..0a39707
--- /dev/null
+++ b/pub/models/playletthememod/theme.go
@@ -0,0 +1,143 @@
+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.IsRecommend, Value: 1},
+ {Key: FN.PlayletTheme.Status, Value: 1},
+ {Key: FN.PlayletTheme.Rank, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTheme.Name, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletTheme.Slug, 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
+}
+
+// UpdateManyPlayletTheme 批量更新题材数据
+func UpdateManyPlayletTheme(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("playletthememod:UpdateManyPlayletTheme failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/playletthememod/type.go b/pub/models/playletthememod/type.go
new file mode 100644
index 0000000..57c389d
--- /dev/null
+++ b/pub/models/playletthememod/type.go
@@ -0,0 +1,25 @@
+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"` // 上架状态
+ IsRecommend bool `json:"isRecommend" bson:"isRecommend"` // 是否推荐
+ 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/playletwatchhistorymod/history.go b/pub/models/playletwatchhistorymod/history.go
new file mode 100644
index 0000000..58add32
--- /dev/null
+++ b/pub/models/playletwatchhistorymod/history.go
@@ -0,0 +1,109 @@
+package playletwatchhistorymod
+
+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
+
+const playletWatchHistoryTTLSeconds int32 = 90 * 24 * 60 * 60
+
+// Init 初始化 playlet_watch_history 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletWatchHistory)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletWatchHistory.UserId, Value: 1},
+ {Key: FN.PlayletWatchHistory.PlayletId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletWatchHistory.UserId, Value: 1},
+ {Key: FN.PlayletWatchHistory.UpdatedAt, Value: -1},
+ {Key: FN.PlayletWatchHistory.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletWatchHistory.UpdatedAt, Value: 1},
+ },
+ Options: options.Index().SetExpireAfterSeconds(playletWatchHistoryTTLSeconds),
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletwatchhistorymod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// FindManyPlayletWatchHistory 按条件查询用户短剧观看历史
+func FindManyPlayletWatchHistory(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletWatchHistoryList, *srvdef.SrvErr) {
+ data := make(PlayletWatchHistoryList, 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, "", "")
+ }
+ return data, nil
+}
+
+// CountPlayletWatchHistory 统计满足条件的短剧观看历史数量
+func CountPlayletWatchHistory(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
+}
+
+// SavePlayletWatchHistory 保存用户短剧观看历史
+func SavePlayletWatchHistory(ctx context.Context, userId int64, playletId int64, chapterId int64, currentEpisode int64) *srvdef.SrvErr {
+ now := time.Now()
+ filter := bson.M{
+ FN.PlayletWatchHistory.UserId: userId,
+ FN.PlayletWatchHistory.PlayletId: playletId,
+ }
+ update := bson.M{
+ "$set": bson.M{
+ FN.PlayletWatchHistory.LastChapterId: chapterId,
+ FN.PlayletWatchHistory.LastEpisode: currentEpisode,
+ FN.PlayletWatchHistory.UpdatedAt: now,
+ },
+ "$setOnInsert": bson.M{
+ FN.PlayletWatchHistory.Id: mongos.Lid.PlayletWatchHistory.Get(),
+ FN.PlayletWatchHistory.UserId: userId,
+ FN.PlayletWatchHistory.PlayletId: playletId,
+ FN.PlayletWatchHistory.CreatedAt: now,
+ },
+ }
+ if _, err := coll.UpsertOne(ctx, filter, update); err != nil {
+ log.Warn("保存短剧观看历史失败", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// DeleteManyPlayletWatchHistory 删除满足条件的短剧观看历史
+func DeleteManyPlayletWatchHistory(ctx context.Context, filter bson.M, opts ...*options.DeleteOptions) (int64, *srvdef.SrvErr) {
+ count, err := coll.DeleteMany(ctx, filter, opts...)
+ if err != nil {
+ log.Warn("删除短剧观看历史失败", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
diff --git a/pub/models/playletwatchhistorymod/tag.go b/pub/models/playletwatchhistorymod/tag.go
new file mode 100644
index 0000000..132d6dc
--- /dev/null
+++ b/pub/models/playletwatchhistorymod/tag.go
@@ -0,0 +1,32 @@
+package playletwatchhistorymod
+
+// TagPlayletWatchHistory 保存 playlet_watch_history 集合字段名
+type TagPlayletWatchHistory struct {
+ Id string
+ UserId string
+ PlayletId string
+ LastChapterId string
+ LastEpisode string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletWatchHistory TagPlayletWatchHistory
+}
+
+var TN struct {
+ PlayletWatchHistory string
+}
+
+func init() {
+ FN.PlayletWatchHistory.Id = "_id"
+ FN.PlayletWatchHistory.UserId = "userId"
+ FN.PlayletWatchHistory.PlayletId = "playletId"
+ FN.PlayletWatchHistory.LastChapterId = "lastChapterId"
+ FN.PlayletWatchHistory.LastEpisode = "lastEpisode"
+ FN.PlayletWatchHistory.CreatedAt = "createdAt"
+ FN.PlayletWatchHistory.UpdatedAt = "updatedAt"
+
+ TN.PlayletWatchHistory = "playlet_watch_history"
+}
diff --git a/pub/models/playletwatchhistorymod/type.go b/pub/models/playletwatchhistorymod/type.go
new file mode 100644
index 0000000..e0bbb8b
--- /dev/null
+++ b/pub/models/playletwatchhistorymod/type.go
@@ -0,0 +1,19 @@
+package playletwatchhistorymod
+
+import "time"
+
+// PlayletWatchHistory @table: playlet_watch_history
+//
+// PlayletWatchHistory 用户短剧观看历史模型,用于观看历史列表和清空历史。
+type PlayletWatchHistory struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 观看历史记录ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ LastChapterId int64 `json:"lastChapterId" bson:"lastChapterId"` // 最近观看剧集ID
+ LastEpisode int64 `json:"lastEpisode" bson:"lastEpisode"` // 最近观看集数
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletWatchHistoryList 用户短剧观看历史列表
+type PlayletWatchHistoryList []*PlayletWatchHistory
diff --git a/pub/models/playletwatchprogressmod/progress.go b/pub/models/playletwatchprogressmod/progress.go
new file mode 100644
index 0000000..943738d
--- /dev/null
+++ b/pub/models/playletwatchprogressmod/progress.go
@@ -0,0 +1,135 @@
+package playletwatchprogressmod
+
+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_watch_progress 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.PlayletWatchProgress)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletWatchProgress.UserId, Value: 1},
+ {Key: FN.PlayletWatchProgress.PlayletId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletWatchProgress.UserId, Value: 1},
+ {Key: FN.PlayletWatchProgress.UpdatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.PlayletWatchProgress.PlayletId, Value: 1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(playletwatchprogressmod:initIndex) 创建索引失败 err:%+v:", err))
+ }
+}
+
+// FindManyPlayletWatchProgress 按条件查询用户短剧观看进度
+func FindManyPlayletWatchProgress(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletWatchProgressList, *srvdef.SrvErr) {
+ data := make(PlayletWatchProgressList, 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, "", "")
+ }
+ return data, nil
+}
+
+// SavePlayletWatchProgress 保存用户短剧观看进度,并在达到已看条件时维护已看集数集合。
+func SavePlayletWatchProgress(ctx context.Context, userId int64, playletId int64, chapterId int64, currentEpisode int64, markWatched bool) (*PlayletWatchProgress, *srvdef.SrvErr) {
+ filter := bson.M{
+ FN.PlayletWatchProgress.UserId: userId,
+ FN.PlayletWatchProgress.PlayletId: playletId,
+ }
+ doc, err := findOnePlayletWatchProgress(ctx, filter)
+ if err != nil && err.Code != srvdef.ErrNoData {
+ return nil, err
+ }
+ now := time.Now()
+ if doc == nil {
+ doc = &PlayletWatchProgress{
+ Id: mongos.Lid.PlayletWatchProgress.Get(),
+ UserId: userId,
+ PlayletId: playletId,
+ WatchedEpisodes: make([]int64, 0),
+ CreatedAt: now,
+ }
+ }
+ doc.LastChapterId = chapterId
+ doc.LastEpisode = currentEpisode
+ doc.UpdatedAt = now
+ if markWatched && !hasEpisode(doc.WatchedEpisodes, currentEpisode) {
+ doc.WatchedEpisodes = append(doc.WatchedEpisodes, currentEpisode)
+ }
+ doc.WatchedCount = int64(len(doc.WatchedEpisodes))
+ update := bson.M{
+ "$set": bson.M{
+ FN.PlayletWatchProgress.LastChapterId: doc.LastChapterId,
+ FN.PlayletWatchProgress.LastEpisode: doc.LastEpisode,
+ FN.PlayletWatchProgress.WatchedEpisodes: doc.WatchedEpisodes,
+ FN.PlayletWatchProgress.WatchedCount: doc.WatchedCount,
+ FN.PlayletWatchProgress.UpdatedAt: doc.UpdatedAt,
+ },
+ "$setOnInsert": bson.M{
+ FN.PlayletWatchProgress.Id: doc.Id,
+ FN.PlayletWatchProgress.UserId: doc.UserId,
+ FN.PlayletWatchProgress.PlayletId: doc.PlayletId,
+ FN.PlayletWatchProgress.CreatedAt: doc.CreatedAt,
+ },
+ }
+ if _, e := coll.UpsertOne(ctx, filter, update); e != nil {
+ log.Warn("保存短剧观看进度失败", log.E(e), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return doc, nil
+}
+
+func findOnePlayletWatchProgress(ctx context.Context, filter bson.M) (*PlayletWatchProgress, *srvdef.SrvErr) {
+ var doc PlayletWatchProgress
+ if err := coll.FindOneEx(ctx, &doc, filter, options.FindOne().SetProjection(bson.D{
+ {Key: FN.PlayletWatchProgress.Id, Value: 1},
+ {Key: FN.PlayletWatchProgress.UserId, Value: 1},
+ {Key: FN.PlayletWatchProgress.PlayletId, Value: 1},
+ {Key: FN.PlayletWatchProgress.WatchedEpisodes, Value: 1},
+ {Key: FN.PlayletWatchProgress.WatchedCount, Value: 1},
+ {Key: FN.PlayletWatchProgress.CreatedAt, Value: 1},
+ })); err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ log.Warn("查询短剧观看进度失败", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &doc, nil
+}
+
+func hasEpisode(episodes []int64, target int64) bool {
+ for _, episode := range episodes {
+ if episode == target {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pub/models/playletwatchprogressmod/tag.go b/pub/models/playletwatchprogressmod/tag.go
new file mode 100644
index 0000000..d1f55be
--- /dev/null
+++ b/pub/models/playletwatchprogressmod/tag.go
@@ -0,0 +1,36 @@
+package playletwatchprogressmod
+
+// TagPlayletWatchProgress 保存 playlet_watch_progress 集合字段名
+type TagPlayletWatchProgress struct {
+ Id string
+ UserId string
+ PlayletId string
+ LastChapterId string
+ LastEpisode string
+ WatchedEpisodes string
+ WatchedCount string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ PlayletWatchProgress TagPlayletWatchProgress
+}
+
+var TN struct {
+ PlayletWatchProgress string
+}
+
+func init() {
+ FN.PlayletWatchProgress.Id = "_id"
+ FN.PlayletWatchProgress.UserId = "userId"
+ FN.PlayletWatchProgress.PlayletId = "playletId"
+ FN.PlayletWatchProgress.LastChapterId = "lastChapterId"
+ FN.PlayletWatchProgress.LastEpisode = "lastEpisode"
+ FN.PlayletWatchProgress.WatchedEpisodes = "watchedEpisodes"
+ FN.PlayletWatchProgress.WatchedCount = "watchedCount"
+ FN.PlayletWatchProgress.CreatedAt = "createdAt"
+ FN.PlayletWatchProgress.UpdatedAt = "updatedAt"
+
+ TN.PlayletWatchProgress = "playlet_watch_progress"
+}
diff --git a/pub/models/playletwatchprogressmod/type.go b/pub/models/playletwatchprogressmod/type.go
new file mode 100644
index 0000000..8a3b689
--- /dev/null
+++ b/pub/models/playletwatchprogressmod/type.go
@@ -0,0 +1,21 @@
+package playletwatchprogressmod
+
+import "time"
+
+// PlayletWatchProgress @table: playlet_watch_progress
+//
+// PlayletWatchProgress 用户短剧观看进度模型,用于收藏列表计算未看集数和恢复上次观看位置。
+type PlayletWatchProgress struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 观看进度记录ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
+ LastChapterId int64 `json:"lastChapterId" bson:"lastChapterId"` // 最近观看剧集ID
+ LastEpisode int64 `json:"lastEpisode" bson:"lastEpisode"` // 最近观看集数
+ WatchedEpisodes []int64 `json:"watchedEpisodes" bson:"watchedEpisodes"` // 已看集数集合
+ WatchedCount int64 `json:"watchedCount" bson:"watchedCount"` // 已看集数冗余值
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
+}
+
+// PlayletWatchProgressList 用户短剧观看进度列表
+type PlayletWatchProgressList []*PlayletWatchProgress
diff --git a/pub/models/rechargepackagemod/package.go b/pub/models/rechargepackagemod/package.go
new file mode 100644
index 0000000..258eacb
--- /dev/null
+++ b/pub/models/rechargepackagemod/package.go
@@ -0,0 +1,128 @@
+package rechargepackagemod
+
+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 初始化 recharge_package 集合和常用查询索引
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.RechargePackage)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.RechargePackage.PackageType, Value: 1},
+ {Key: FN.RechargePackage.Status, Value: 1},
+ {Key: FN.RechargePackage.Sort, Value: 1},
+ {Key: FN.RechargePackage.Id, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.RechargePackage.CreatedAt, Value: -1},
+ {Key: FN.RechargePackage.Id, Value: -1},
+ },
+ },
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(rechargepackagemod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+func InsertOneRechargePackage(ctx context.Context, doc *RechargePackage) *srvdef.SrvErr {
+ if doc.Id == 0 {
+ doc.Id = mongos.Lid.RechargePackage.Get()
+ }
+ now := time.Now()
+ if doc.CreatedAt.IsZero() {
+ doc.CreatedAt = now
+ }
+ doc.UpdatedAt = now
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Warn("购买套餐插入失败", log.Any("func", "InsertOneRechargePackage"), log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneRechargePackage 按条件查询单条购买套餐。
+func FindOneRechargePackage(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*RechargePackage, *srvdef.SrvErr) {
+ var doc RechargePackage
+ if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("购买套餐单条查询失败", log.Any("func", "FindOneRechargePackage"), log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+// FindManyRechargePackage 按条件查询购买套餐列表。
+func FindManyRechargePackage(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (RechargePackageList, *srvdef.SrvErr) {
+ data := make(RechargePackageList, 0)
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("购买套餐列表查询失败", log.Any("func", "FindManyRechargePackage"), log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// CountRechargePackage 统计购买套餐数量。
+func CountRechargePackage(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.Any("func", "CountRechargePackage"), log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneRechargePackage 更新单条购买套餐。
+func UpdateOneRechargePackage(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := coll.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("购买套餐单条更新失败", log.Any("func", "UpdateOneRechargePackage"), log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyRechargePackage 批量更新购买套餐。
+func UpdateManyRechargePackage(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ if _, err := coll.UpdateMany(ctx, filter, update); err != nil {
+ log.Warn("购买套餐批量更新失败", log.Any("func", "UpdateManyRechargePackage"), log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// DeleteOneRechargePackage 删除单条购买套餐。
+func DeleteOneRechargePackage(ctx context.Context, filter bson.M) *srvdef.SrvErr {
+ count, err := coll.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("购买套餐删除失败", log.Any("func", "DeleteOneRechargePackage"), log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ if count <= 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/rechargepackagemod/tag.go b/pub/models/rechargepackagemod/tag.go
new file mode 100644
index 0000000..aeab656
--- /dev/null
+++ b/pub/models/rechargepackagemod/tag.go
@@ -0,0 +1,47 @@
+package rechargepackagemod
+
+type TagRechargePackage struct {
+ Id string // 套餐ID
+ PackageType string // 套餐类型
+ Name string // 展示标题
+ SubTitle string // 展示副标题
+ Price string // 售价,单位分
+ OriginalPrice string // 原价,单位分
+ DurationDays string // 会员有效天数
+ BaseCoin string // 基础金币数
+ GiftCoin string // 赠送金币数
+ Sort string // 排序
+ Status string // 状态
+ Remark string // 后台备注
+ CreatedAt string // 创建时间
+ UpdatedAt string // 更新时间
+}
+
+// FN recharge_package 集合字段名,统一用于构造 Mongo 查询,避免手写字段名。
+var FN struct {
+ RechargePackage TagRechargePackage
+}
+
+// TN recharge_package 集合名称。
+var TN struct {
+ RechargePackage string
+}
+
+func init() {
+ FN.RechargePackage.Id = "_id"
+ FN.RechargePackage.PackageType = "packageType"
+ FN.RechargePackage.Name = "name"
+ FN.RechargePackage.SubTitle = "subTitle"
+ FN.RechargePackage.Price = "price"
+ FN.RechargePackage.OriginalPrice = "originalPrice"
+ FN.RechargePackage.DurationDays = "durationDays"
+ FN.RechargePackage.BaseCoin = "baseCoin"
+ FN.RechargePackage.GiftCoin = "giftCoin"
+ FN.RechargePackage.Sort = "sort"
+ FN.RechargePackage.Status = "status"
+ FN.RechargePackage.Remark = "remark"
+ FN.RechargePackage.CreatedAt = "createdAt"
+ FN.RechargePackage.UpdatedAt = "updatedAt"
+
+ TN.RechargePackage = "recharge_package"
+}
diff --git a/pub/models/rechargepackagemod/type.go b/pub/models/rechargepackagemod/type.go
new file mode 100644
index 0000000..0a8d824
--- /dev/null
+++ b/pub/models/rechargepackagemod/type.go
@@ -0,0 +1,40 @@
+package rechargepackagemod
+
+import "time"
+
+// PackageType 购买套餐类型。
+type PackageType int32
+
+const (
+ PackageTypeVip PackageType = 1 // 会员畅享
+ PackageTypeCoin PackageType = 2 // 金币充值
+)
+
+// Status 购买套餐状态。
+type Status int32
+
+const (
+ StatusEnabled Status = 1 // 启用
+ StatusDisabled Status = 2 // 停用
+)
+
+// RechargePackage 购买套餐表,用于配置购买套餐页的会员畅享和金币充值列表
+type RechargePackage struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 套餐ID
+ PackageType PackageType `json:"packageType" bson:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
+ Name string `json:"name" bson:"name"` // 展示标题,如:1个月会员、660金币
+ SubTitle string `json:"subTitle" bson:"subTitle"` // 展示副标题,如:30 × 24 小时会员、基础600 + 赠送60
+ Price int64 `json:"price" bson:"price"` // 售价,单位分
+ OriginalPrice int64 `json:"originalPrice" bson:"originalPrice"` // 原价,单位分
+ DurationDays int64 `json:"durationDays" bson:"durationDays"` // 会员有效天数,金币套餐为0
+ BaseCoin int64 `json:"baseCoin" bson:"baseCoin"` // 基础金币数,会员套餐为0
+ GiftCoin int64 `json:"giftCoin" bson:"giftCoin"` // 赠送金币数
+ Sort int32 `json:"sort" bson:"sort"` // 排序,越小越靠前
+ Status Status `json:"status" bson:"status"` // 状态:1-启用 2-停用
+ Remark string `json:"remark" bson:"remark"` // 后台备注
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+
+// RechargePackageList 购买套餐列表
+type RechargePackageList []*RechargePackage
diff --git a/pub/models/richtextcontentmod/richtextcontent.go b/pub/models/richtextcontentmod/richtextcontent.go
new file mode 100644
index 0000000..cd03ff0
--- /dev/null
+++ b/pub/models/richtextcontentmod/richtextcontent.go
@@ -0,0 +1,153 @@
+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},
+ },
+ },
+ }
+ 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, err.Error(), "")
+ }
+ 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/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..ad75278
--- /dev/null
+++ b/pub/models/skdrecord/type.go
@@ -0,0 +1,26 @@
+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" // 按天统计访问信息
+)
+
+type SkdRecordList []*SkdRecord
diff --git a/pub/models/syncfilemod/sync.go b/pub/models/syncfilemod/sync.go
new file mode 100644
index 0000000..7ac7898
--- /dev/null
+++ b/pub/models/syncfilemod/sync.go
@@ -0,0 +1,108 @@
+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
+}
+
+// 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..dd3864f
--- /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"
+}
diff --git a/pub/models/syncfilemod/type.go b/pub/models/syncfilemod/type.go
new file mode 100644
index 0000000..7c7ce16
--- /dev/null
+++ b/pub/models/syncfilemod/type.go
@@ -0,0 +1,28 @@
+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 (
+ PlayletWatermarkJob = "playletWatermarkJob" // 短剧水印任务完成同步
+)
+
+var SyncJobDescribe = map[string]string{
+ PlayletWatermarkJob: "短剧水印任务完成同步",
+}
diff --git a/pub/models/translationcategorymod/category.go b/pub/models/translationcategorymod/category.go
new file mode 100644
index 0000000..3e6895c
--- /dev/null
+++ b/pub/models/translationcategorymod/category.go
@@ -0,0 +1,112 @@
+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},
+ },
+ },
+ }
+ 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..2e69619
--- /dev/null
+++ b/pub/models/translationcategorymod/tag.go
@@ -0,0 +1,42 @@
+package translationcategorymod
+
+type TagTranslationCategory struct {
+ CategoryType string
+ CreatedAt string
+ Id string
+ Name string
+ Rank string
+ Status string
+ UpdatedAt string
+ UrlValue string
+ Icon string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title 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"
+ FN.TranslationCategory.SeoInfo.Description = "seoInfo.webDescription"
+ FN.TranslationCategory.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.TranslationCategory.SeoInfo.Title = "seoInfo.webTitle"
+ TN.TranslationCategory = "translation_category"
+}
diff --git a/pub/models/translationcategorymod/type.go b/pub/models/translationcategorymod/type.go
new file mode 100644
index 0000000..187e70a
--- /dev/null
+++ b/pub/models/translationcategorymod/type.go
@@ -0,0 +1,21 @@
+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"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ 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..807c0c7
--- /dev/null
+++ b/pub/models/translationmod/translation.go
@@ -0,0 +1,112 @@
+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},
+ },
+ },
+ }
+ 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..ec215e1
--- /dev/null
+++ b/pub/models/usermod/tag.go
@@ -0,0 +1,125 @@
+package usermod
+
+type TagUser struct {
+ Id string // ID
+ Type string // 类型
+ VipType string // Vip类型
+ 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版本
+ SessionID string // 会话ID
+ DeviceBrand string // 设备品牌
+ DeviceModel string // 设备型号
+ SystemName string // 系统名称
+ SystemVersion string // 系统版本
+ Invites string // 我邀请的人的个数(我邀请他人)
+ AgentInviteCode string // 被邀请的inviteCode
+ Password string // 登陆密码
+ Recharge string // 当前充值余额,金币个数
+ RechargeTotal string // 累计充值额,金币个数
+ ConsumptionTotal string // 金币总消费数量
+ ModifyUserNameAt string // 上一次修改用户时间
+ Auth string // 是否认证
+ ConsecutiveLogin string // 连续登陆天数
+ DistrictCode string // 商区码
+ IsDirect string // 是否是直推 true:是直推用户
+ IsRegister string // 是否已注册
+ Score string // 积分
+ ScoreTotal string // 累计积分
+ ScoreLvName string // 积分等级名称
+ ScoreInvites string // 邀请人获得的积分累计
+ RegisterTime string //
+ CreatedAt string // 创建时间
+ UpdatedAt string // 更新时间
+ CurrentVipExpire string // 当前vip过期时间
+ SysMsgAt string // 获取系统消息时间
+ ShowCardId string // 最新会员卡id
+ ShowCardName string // 最新会员卡名称
+ OriVer string // 注册时的app版本
+ AccountNo string // 账号
+ Email string // 邮箱
+ MerchantExpiredAt string // 历史遗留字段
+ PrivacyMode string // 隐私模式
+}
+
+var FN struct {
+ User TagUser
+}
+var TN struct {
+ User string
+}
+
+func init() {
+ FN.User.Id = "_id"
+ FN.User.Type = "type"
+ FN.User.VipType = "VipType"
+ 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.SessionID = "sessionId"
+ FN.User.DeviceBrand = "deviceBrand"
+ FN.User.DeviceModel = "deviceModel"
+ FN.User.SystemName = "systemName"
+ FN.User.SystemVersion = "systemVersion"
+ 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.IsDirect = "isDirect"
+ 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.ShowCardId = "showCardId"
+ FN.User.ShowCardName = "showCardName"
+ FN.User.OriVer = "oriVer"
+ FN.User.AccountNo = "accountNo"
+ FN.User.Email = "email"
+ FN.User.MerchantExpiredAt = "merchantExpiredAt"
+ FN.User.PrivacyMode = "privacyMode"
+ TN.User = "user"
+}
diff --git a/pub/models/usermod/type.go b/pub/models/usermod/type.go
new file mode 100644
index 0000000..3b36708
--- /dev/null
+++ b/pub/models/usermod/type.go
@@ -0,0 +1,95 @@
+//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"` // 账号
+ Email string `json:"email" bson:"email"` // 邮箱
+ Type pubdef.UserType `json:"type" bson:"type"` // 类型
+ VipType pubdef.VipType `json:"vipType" bson:"VipType"` // Vip类型
+ 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版本
+ SessionID string `json:"sessionId" bson:"sessionId"` // 会话ID
+ DeviceBrand string `json:"deviceBrand" bson:"deviceBrand"` // 设备品牌
+ DeviceModel string `json:"deviceModel" bson:"deviceModel"` // 设备型号
+ SystemName string `json:"systemName" bson:"systemName"` // 系统名称
+ SystemVersion string `json:"systemVersion" bson:"systemVersion"` // 系统版本
+ 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"` // 商区码
+ IsDirect bool `json:"isDirect" bson:"isDirect"` // 是否是直推 true:是直推用户
+ 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"` // 获取系统消息时间
+ ShowCardId int64 `json:"showCardId" bson:"showCardId"` // 最新会员卡id
+ ShowCardName string `json:"showCardName" bson:"showCardName"` // 最新会员卡名称
+ OriVer string `json:"oriVer" bson:"oriVer"` // 注册时的app版本
+ MerchantExpiredAt time.Time `json:"merchantExpiredAt" bson:"merchantExpiredAt"` // 历史遗留字段
+ PrivacyMode bool `json:"privacyMode" bson:"privacyMode"` // 隐私模式:true-开启,false-关闭(默认)
+
+}
+
+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..cf48f8a
--- /dev/null
+++ b/pub/models/usermod/type_update.go
@@ -0,0 +1,62 @@
+// Code generated by gen g update. DO NOT EDIT.
+package usermod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+type UserUpdate struct {
+ Type *pubdef.UserType `json:"type" bson:"type,omitempty"` // 类型
+ VipType *pubdef.VipType `json:"vipType" bson:"VipType,omitempty"` // Vip类型
+ 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版本
+ SessionID *string `json:"sessionId" bson:"sessionId,omitempty"` // 会话ID
+ DeviceBrand *string `json:"deviceBrand" bson:"deviceBrand,omitempty"` // 设备品牌
+ DeviceModel *string `json:"deviceModel" bson:"deviceModel,omitempty"` // 设备型号
+ SystemName *string `json:"systemName" bson:"systemName,omitempty"` // 系统名称
+ SystemVersion *string `json:"systemVersion" bson:"systemVersion,omitempty"` // 系统版本
+ 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"` // 商区码
+ IsDirect *bool `json:"isDirect" bson:"isDirect,omitempty"` // 是否是直推 true:是直推用户
+ 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"` // 最新会员卡名称
+ 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..a65b9d1
--- /dev/null
+++ b/pub/models/usermod/user.go
@@ -0,0 +1,406 @@
+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.Type, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.NickName, Value: "text"},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.User.Status, Value: 1},
+ {Key: FN.User.Type, Value: 1},
+ },
+ },
+ }
+ 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 查找一个用户;opts 可用于调用方按需设置 projection,避免读取完整用户文档。
+func FindOneUser(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*User, *srvdef.SrvErr) {
+ var userInfo User
+ if err := db.FindOneEx(ctx, &userInfo, filter, opts...); 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
+}
+
+// StatcenterSyncListByUserID 按 UserID 游标拉取注册用户(新统计中心)
+func StatcenterSyncListByUserID(ctx context.Context, userID int64, size int64) (UserList, error) {
+ opt := (&options.FindOptions{})
+ opt.SetLimit(size)
+ opt.SetSort(bson.D{{Key: FN.User.Id, Value: 1}})
+ filter := bson.M{
+ FN.User.Id: bson.M{"$gt": userID},
+ }
+ userList := UserList{}
+ err := db.Find(ctx, &userList, filter, opt)
+ if err != nil {
+ return nil, err
+ }
+ return userList, nil
+}
+
+// StatcenterSyncBindUserList 增量拉取已正式注册(绑定邮箱)用户
+func StatcenterSyncBindUserList(ctx context.Context, bindAt time.Time, size int64) (UserList, error) {
+ opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{FN.User.UpdatedAt: 1})
+ filter := bson.M{
+ FN.User.IsRegister: true,
+ FN.User.Email: bson.M{"$nin": []interface{}{"", nil}},
+ FN.User.UpdatedAt: bson.M{"$gt": bindAt},
+ }
+ userList := UserList{}
+ if err := db.Find(ctx, &userList, filter, opt); err != nil {
+ return nil, err
+ }
+ return userList, nil
+}
+
+// StatcenterSyncInviteList 增量拉取带上级邀请码的用户
+func StatcenterSyncInviteList(ctx context.Context, inviteAt time.Time, size int64) (UserList, error) {
+ opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{FN.User.CreatedAt: 1})
+ filter := bson.M{
+ FN.User.AgentInviteCode: bson.M{"$nin": []interface{}{"", nil}},
+ FN.User.CreatedAt: bson.M{"$gt": inviteAt},
+ }
+ userList := UserList{}
+ if err := db.Find(ctx, &userList, filter, opt); 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/uservisitlog/dbhelper.go b/pub/models/uservisitlog/dbhelper.go
new file mode 100644
index 0000000..9584dc3
--- /dev/null
+++ b/pub/models/uservisitlog/dbhelper.go
@@ -0,0 +1,5 @@
+package uservisitlog
+
+import "videoweb/pub/mongos"
+
+var visitColl *mongos.Collection
diff --git a/pub/models/uservisitlog/dbop.go b/pub/models/uservisitlog/dbop.go
new file mode 100644
index 0000000..293a329
--- /dev/null
+++ b/pub/models/uservisitlog/dbop.go
@@ -0,0 +1,77 @@
+package uservisitlog
+
+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"
+)
+
+// Init 初始化访问日志表
+func Init(ctx context.Context) {
+ visitColl = mongos.Shop.Coll(TN.UserVisitLog)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.UserVisitLog.UserId, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.UserVisitLog.VisitAt, Value: -1},
+ },
+ Options: options.Index().SetExpireAfterSeconds(5 * 24 * 60 * 60),
+ },
+ }
+ if _, err := visitColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(uservisitlog:initIndex) Create uservisitlog Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneVisitLog 插入一条访问记录
+func InsertOneVisitLog(ctx context.Context, data *UserVisitLog) *srvdef.SrvErr {
+ if _, err := visitColl.InsertOne(ctx, data); err != nil {
+ log.Info("visitColl:InsertOneVisitLog db.InsertOne failed",
+ log.E(err), log.Any("data", data))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindManyVisitLogs 查找一批访问记录
+func FindManyVisitLogs(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VisitLogList, *srvdef.SrvErr) {
+ data := make(VisitLogList, 0)
+ if err := visitColl.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("visitColl:FindManyVisitLogs db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// AccessSyncById 按 ObjectId 增量拉取访问数据(统计中心同步)
+func AccessSyncById(ctx context.Context, id string, size int64) (data VisitLogList, err error) {
+ oid, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ return
+ }
+ endTime := time.Now().Add(-time.Minute * 1)
+ visitFilter := bson.M{
+ FN.UserVisitLog.Id: bson.M{"$gt": oid},
+ FN.UserVisitLog.VisitAt: bson.M{"$lt": endTime},
+ }
+ visitOpt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{FN.UserVisitLog.Id: 1})
+ err = visitColl.Find(ctx, &data, visitFilter, visitOpt)
+ return data, err
+}
diff --git a/pub/models/uservisitlog/tag.go b/pub/models/uservisitlog/tag.go
new file mode 100644
index 0000000..5d7958a
--- /dev/null
+++ b/pub/models/uservisitlog/tag.go
@@ -0,0 +1,32 @@
+package uservisitlog
+
+type TagUserVisitLog struct {
+ DevID string
+ DevType string
+ IP string
+ Id string
+ SysType string
+ UserId string
+ Version string
+ VisitAt string
+}
+
+var FN struct {
+ UserVisitLog TagUserVisitLog
+}
+
+var TN struct {
+ UserVisitLog string
+}
+
+func init() {
+ FN.UserVisitLog.DevID = "devID"
+ FN.UserVisitLog.DevType = "devType"
+ FN.UserVisitLog.IP = "ip"
+ FN.UserVisitLog.Id = "_id"
+ FN.UserVisitLog.SysType = "sysType"
+ FN.UserVisitLog.UserId = "userId"
+ FN.UserVisitLog.Version = "version"
+ FN.UserVisitLog.VisitAt = "visitAt"
+ TN.UserVisitLog = "user_visit_log"
+}
diff --git a/pub/models/uservisitlog/type.go b/pub/models/uservisitlog/type.go
new file mode 100644
index 0000000..2eb3ce7
--- /dev/null
+++ b/pub/models/uservisitlog/type.go
@@ -0,0 +1,21 @@
+package uservisitlog
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// @table:user_visit_log
+type UserVisitLog struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"` // 唯一ID
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ SysType string `json:"sysType" bson:"sysType"` // 系统类型
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ DevID string `json:"devID" bson:"devID"` // 设备ID
+ Version string `json:"version" bson:"version"` // 版本
+ IP string `json:"ip" bson:"ip"` // ip
+ VisitAt time.Time `json:"visitAt" bson:"visitAt"` // 访问时间
+}
+
+type VisitLogList []*UserVisitLog
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..81a3f29
--- /dev/null
+++ b/pub/models/videocategorymod/category.go
@@ -0,0 +1,132 @@
+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.ContentType, 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 {
+ // 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..b775403
--- /dev/null
+++ b/pub/models/videocategorymod/tag.go
@@ -0,0 +1,46 @@
+package videocategorymod
+
+type TagVideoCategory struct {
+ ContentType string
+ CreatedAt string
+ Desc string
+ Id string
+ IndexNow string
+ Name string
+ Rank string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ SortType string
+ Status string
+ Title string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoCategory TagVideoCategory
+}
+
+var TN struct {
+ VideoCategory string
+}
+
+func init() {
+ FN.VideoCategory.ContentType = "contentType"
+ FN.VideoCategory.CreatedAt = "createdAt"
+ FN.VideoCategory.Desc = "desc"
+ FN.VideoCategory.Id = "_id"
+ FN.VideoCategory.IndexNow = "indexNow"
+ FN.VideoCategory.Name = "name"
+ FN.VideoCategory.Rank = "rank"
+ FN.VideoCategory.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoCategory.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoCategory.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoCategory.SortType = "sortType"
+ FN.VideoCategory.Status = "status"
+ FN.VideoCategory.Title = "title"
+ FN.VideoCategory.UpdatedAt = "updatedAt"
+ TN.VideoCategory = "video_category"
+}
diff --git a/pub/models/videocategorymod/type.go b/pub/models/videocategorymod/type.go
new file mode 100644
index 0000000..18ffa7f
--- /dev/null
+++ b/pub/models/videocategorymod/type.go
@@ -0,0 +1,30 @@
+package videocategorymod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_category
+type VideoCategory 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"` // 描述
+ Status bool `json:"status" bson:"status"` // 上架状态
+ ContentType int64 `json:"contentType" bson:"contentType"` // 内容:0: 默认 1:首页 2:今日 3:合集
+ SortType int64 `json:"sortType" bson:"sortType"` // 排序方式 0:上线时间 1:观看次数 2排序
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoCategoryList []*VideoCategory
+type VideoSortType int32
+
+const (
+ VideoSortTypeAddTime VideoSortType = 0 // 上线时间
+ VideoSortTypeWatchs VideoSortType = 1 // 观看次数
+ VideoSortTypeRank VideoSortType = 2 // 视频排序
+)
diff --git a/pub/models/videocreatermod/creater.go b/pub/models/videocreatermod/creater.go
new file mode 100644
index 0000000..c827b6f
--- /dev/null
+++ b/pub/models/videocreatermod/creater.go
@@ -0,0 +1,127 @@
+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..9440706
--- /dev/null
+++ b/pub/models/videocreatermod/tag.go
@@ -0,0 +1,38 @@
+package videocreatermod
+
+type TagVideoCreaterInfo struct {
+ CreatedAt string
+ Id string
+ IndexNow string
+ Name string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ UpdatedAt 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.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"
+ TN.VideoCreaterInfo = "video_creater"
+}
diff --git a/pub/models/videocreatermod/type.go b/pub/models/videocreatermod/type.go
new file mode 100644
index 0000000..6bcd6aa
--- /dev/null
+++ b/pub/models/videocreatermod/type.go
@@ -0,0 +1,19 @@
+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"` // 帖子标题
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ 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/videoinformationmod/information.go b/pub/models/videoinformationmod/information.go
new file mode 100644
index 0000000..e4762b0
--- /dev/null
+++ b/pub/models/videoinformationmod/information.go
@@ -0,0 +1,315 @@
+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.CategoryIds, 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.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.CategoryIds, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Video.FileId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {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.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ // /api/app/video/list 按 status 过滤,并按 addedTime/_id 倒序游标分页。
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, 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.MediaType, 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.CategoryIds, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: "nodes.nodeId", Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Tags, 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.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.IsTop, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.IsTop, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.HevcStatus, Value: 1},
+ {Key: FN.VideoInformation.HevcFailCount, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.HevcStatus, 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) (
+ *VideoInformation, *srvdef.SrvErr) {
+ var VideoInformation VideoInformation
+ if err := db.FindOneEx(ctx, &VideoInformation, filter); 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
+}
+
+// FindOneVideoInformationWithOptions 按条件查询单条视频信息,支持projection等查询选项。
+func FindOneVideoInformationWithOptions(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 {
+ log.Warn("VideoInformationmod:FindOneVideoInformationWithOptions db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ 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, opts ...*options.CountOptions) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter, opts...)
+ 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
+}
+
+// AggregateVideoInformations 聚合查询视频信息
+func AggregateVideoInformations(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("videoinformationmod:AggregateVideoInformations failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ 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
+}
+
+// BulkWriteVideoInformation 批量写入或更新视频信息
+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("视频信息批量写入或更新失败", log.E(err))
+ return nil, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result, nil
+}
diff --git a/pub/models/videoinformationmod/tag.go b/pub/models/videoinformationmod/tag.go
new file mode 100755
index 0000000..afaf9c0
--- /dev/null
+++ b/pub/models/videoinformationmod/tag.go
@@ -0,0 +1,130 @@
+package videoinformationmod
+
+type TagVideoInformation struct {
+ AddedTime string
+ CategoryIds string
+ CoverImg string
+ CreatedAt string
+ CreatorId string
+ CreatorName string
+ Desc string
+ FirstTag string
+ Id string
+ IndexNow string
+ IsHot string
+ IsTop string
+ Keyworld string
+ MediaType string
+ Nodes string
+ Real struct {
+ Comments string
+ MonthWatchs string
+ ShowTimes string
+ TodayWatchs string
+ WatchTimes string
+ WeekWatchs string
+ }
+ Fake struct {
+ WatchTimes string
+ }
+ Remark string
+ SecondTags string
+ Sort string
+ Status string
+ Tags string
+ Title string
+ UpdatedAt string
+ Video struct {
+ Cover string
+ FileId string
+ Height string
+ PreMp4FileNameFs string
+ TaskId string
+ Url string
+ H265Url string
+ VideoTime string
+ Width string
+ UnencryptedPreMp4 string
+ }
+ VideoCount string
+ UploadUserName string
+ OptionAccount string
+ PageUpdatedAt string
+ UnencryptedImage string
+ Content string
+ SecondUpdatedAt string
+ TopicIds 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.AddedTime = "addedTime"
+ FN.VideoInformation.CategoryIds = "categoryIds"
+ 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.Keyworld = "keyworld"
+ FN.VideoInformation.MediaType = "mediaType"
+ FN.VideoInformation.Nodes = "nodes"
+ FN.VideoInformation.Real.Comments = "real.comments"
+ 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.Fake.WatchTimes = "fake.watchTimes"
+ FN.VideoInformation.Remark = "remark"
+ FN.VideoInformation.SecondTags = "secondTags"
+ FN.VideoInformation.Sort = "sort"
+ FN.VideoInformation.Status = "status"
+ FN.VideoInformation.Tags = "tags"
+ FN.VideoInformation.Title = "title"
+ FN.VideoInformation.UpdatedAt = "updatedAt"
+ FN.VideoInformation.Video.Cover = "video.cover"
+ FN.VideoInformation.Video.FileId = "video.fileId"
+ FN.VideoInformation.Video.Height = "video.height"
+ FN.VideoInformation.Video.PreMp4FileNameFs = "video.preMp4FileNameFs"
+ FN.VideoInformation.Video.TaskId = "video.taskId"
+ FN.VideoInformation.Video.Url = "video.url"
+ FN.VideoInformation.Video.H265Url = "video.h265Url"
+ FN.VideoInformation.Video.VideoTime = "video.videoTime"
+ FN.VideoInformation.Video.Width = "video.width"
+ FN.VideoInformation.Video.UnencryptedPreMp4 = "video.unencryptedPreMp4"
+ FN.VideoInformation.VideoCount = "videoCount"
+ FN.VideoInformation.OptionAccount = "optionAccount"
+ FN.VideoInformation.UploadUserName = "uploadUserName"
+ FN.VideoInformation.PageUpdatedAt = "pageUpdatedAt"
+ FN.VideoInformation.UnencryptedImage = "unencryptedImage"
+ FN.VideoInformation.Content = "content"
+ FN.VideoInformation.SecondUpdatedAt = "secondUpdatedAt"
+ FN.VideoInformation.TopicIds = "topicIds"
+ FN.VideoInformation.SyncId = "syncId"
+ 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..e461279
--- /dev/null
+++ b/pub/models/videoinformationmod/type.go
@@ -0,0 +1,184 @@
+package videoinformationmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// @table:video_information
+type VideoInformation struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ MediaType 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"` // 封面
+ 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 InformationSatus `json:"status" bson:"status"` // 帖子上架状态
+ Video VideoInfo `json:"video" bson:"video"` // 视频
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类Id (帖子用)
+ TopicIds []int64 `json:"topicIds" bson:"topicIds"` // 专题ID(视频用)
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ Real Flow `json:"real" bson:"real"` // 真实流量
+ Fake FakeFlow `json:"fake" bson:"fake"` // 伪造流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ PageUpdatedAt time.Time `json:"pageUpdatedAt" bson:"pageUpdatedAt"` // 页面展示更新时间
+ IsTop bool `json:"isTop" bson:"isTop"` // 是否置顶
+ UploadUserName string `bson:"uploadUserName" json:"uploadUserName,omitempty"` // 上传用户名
+ OptionAccount string `bson:"optionAccount,omitempty" json:"optionAccount"` // 最后操作人
+ Content string `json:"content" bson:"content"` // 富文本
+ 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 视频元信息
+}
+
+// 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 VideoInformationList []*VideoInformation
+
+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"` // 视频URl
+ H265Url string `json:"h265Url" bson:"h265Url,omitempty"` // H265 视频地址
+ 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 InformationSatus int32
+
+const (
+ InformationSatusWaiting InformationSatus = 0 // 初始化
+ InformationSatusVerifyOK InformationSatus = 1 // 上架
+ InformationSatusVerifyCancel InformationSatus = 2 // 下架
+)
+
+type Flow struct {
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Comments int64 `json:"comments" bson:"comments"` // 评论条数
+ WeekWatchs int64 `json:"weekWatchs" bson:"weekWatchs"` // 本周观看次数
+ MonthWatchs int64 `json:"monthWatchs" bson:"monthWatchs"` // 本月观看次数
+ TodayWatchs int64 `json:"todayWatchs" bson:"todayWatchs"` // 当日观看次数
+}
+
+type FakeFlow struct {
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 假观看次数
+}
+
+type MediaType int64
+
+const (
+ MediaType_Video MediaType = 0 // 视频(帖子)
+ MediaType_Collection MediaType = 1 // 合集
+ MediaType_Advitise MediaType = 2 // 广告
+ MediaType_VideoV2 MediaType = 3 // 视频
+)
+
+type VideoSortItem struct {
+ Key VideoSortType `json:"key" form:"key"` // 传递值
+ Value string `json:"value" form:"value"` // 显示名字
+}
+
+// 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) 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]
+ }
+}
+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}}
+ }
+}
diff --git a/pub/models/videotagmod/tag.go b/pub/models/videotagmod/tag.go
new file mode 100755
index 0000000..f45bf0f
--- /dev/null
+++ b/pub/models/videotagmod/tag.go
@@ -0,0 +1,40 @@
+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
+}
+
+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"
+ TN.VideoTagInfo = "video_taginfo"
+}
diff --git a/pub/models/videotagmod/taginfo.go b/pub/models/videotagmod/taginfo.go
new file mode 100644
index 0000000..9ade0be
--- /dev/null
+++ b/pub/models/videotagmod/taginfo.go
@@ -0,0 +1,138 @@
+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.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
+}
+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..318faaa
--- /dev/null
+++ b/pub/models/videotagmod/type.go
@@ -0,0 +1,20 @@
+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相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 预计算:该标签下所有视频的累计播放量
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoTagInfoList []*VideoTagInfo
diff --git a/pub/models/videotopicmod/tag.go b/pub/models/videotopicmod/tag.go
new file mode 100755
index 0000000..1cc202d
--- /dev/null
+++ b/pub/models/videotopicmod/tag.go
@@ -0,0 +1,50 @@
+package videotopicmod
+
+type TagVideoTopicModel struct {
+ Id string
+ Name string
+ Title string
+ Desc string
+ Status string
+ Rank string
+ Image string
+ VideoCategoryId string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Works string
+ IndexNow string
+ UpdatedAt string
+ CreatedAt string
+ VideoTopicType string
+}
+
+var FN struct {
+ VideoTopicModel TagVideoTopicModel
+}
+
+var TN struct {
+ VideoTopicModel string
+}
+
+func init() {
+ FN.VideoTopicModel.CreatedAt = "createdAt"
+ FN.VideoTopicModel.Desc = "desc"
+ FN.VideoTopicModel.Id = "_id"
+ FN.VideoTopicModel.IndexNow = "indexNow"
+ FN.VideoTopicModel.Name = "name"
+ FN.VideoTopicModel.Rank = "rank"
+ FN.VideoTopicModel.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoTopicModel.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoTopicModel.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoTopicModel.Status = "status"
+ FN.VideoTopicModel.Title = "title"
+ FN.VideoTopicModel.UpdatedAt = "updatedAt"
+ FN.VideoTopicModel.Works = "works"
+ FN.VideoTopicModel.Image = "image"
+ FN.VideoTopicModel.VideoCategoryId = "videoCategoryId"
+ FN.VideoTopicModel.VideoTopicType = "videoTopicType"
+ TN.VideoTopicModel = "video_topic"
+}
diff --git a/pub/models/videotopicmod/type.go b/pub/models/videotopicmod/type.go
new file mode 100644
index 0000000..1ebf89d
--- /dev/null
+++ b/pub/models/videotopicmod/type.go
@@ -0,0 +1,26 @@
+package videotopicmod
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_topic
+type VideoTopicModel 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"` // 描述
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ Image string `json:"image" bson:"image"` // 背景
+ VideoTopicType videoinformationmod.MediaType `json:"videoTopicType" bson:"videoTopicType"` // 视频类型
+ VideoCategoryId int64 `json:"videoCategoryId" bson:"videoCategoryId"` // 视频分类id
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ Works int64 `json:"works" bson:"works"` // 作品数
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoTopicModelList []*VideoTopicModel
diff --git a/pub/models/videotopicmod/videotopic.go b/pub/models/videotopicmod/videotopic.go
new file mode 100644
index 0000000..628de12
--- /dev/null
+++ b/pub/models/videotopicmod/videotopic.go
@@ -0,0 +1,139 @@
+package videotopicmod
+
+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.VideoTopicModel)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTopicModel.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTopicModel.Status, Value: 1},
+ {Key: FN.VideoTopicModel.Rank, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTopicModel.VideoCategoryId, Value: 1},
+ {Key: FN.VideoTopicModel.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTopicModel.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoTopicModelIndex) Create VideoTopicModel Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoTopicModel 插入一个渠道统计
+func InsertOneVideoTopicModel(ctx context.Context, qa *VideoTopicModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoTopicModelmod:InsertOneVideoTopicModel db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoTopicModel 删除一个渠道统计
+func DelOneVideoTopicModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoTopicModelmod:DelOneVideoTopicModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoTopicModel 查找一个渠道统计
+func FindOneVideoTopicModel(ctx context.Context, filter bson.M) (
+ *VideoTopicModel, *srvdef.SrvErr) {
+ var VideoTopicModel VideoTopicModel
+ if err := db.FindOneEx(ctx, &VideoTopicModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoTopicModelmod:FindOneVideoTopicModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoTopicModel, nil
+}
+
+func FindManyVideoTopicModels(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoTopicModelList, *srvdef.SrvErr) {
+ data := make(VideoTopicModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoTopicModels", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoTopicModels 统计渠道统计数量
+func CountVideoTopicModels(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoTopicModels db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoTopicModel 更新一个渠道统计
+func UpdateOneVideoTopicModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoTopicModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoTopicModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVideoTopicModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoTopicModel 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/vipordermod/order.go b/pub/models/vipordermod/order.go
new file mode 100644
index 0000000..4f8d1ce
--- /dev/null
+++ b/pub/models/vipordermod/order.go
@@ -0,0 +1,171 @@
+package vipordermod
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var coll *mongos.Collection
+
+func Init(ctx context.Context) {
+ coll = mongos.Shop.Coll(TN.VipOrder)
+ initIndex(ctx)
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {Keys: bson.D{{Key: FN.VipOrder.UserId, Value: -1}}},
+ {Keys: bson.D{{Key: FN.VipOrder.TradeNo, Value: 1}}, Options: options.Index().SetUnique(true)},
+ {Keys: bson.D{{Key: FN.VipOrder.Status, Value: 1}}},
+ {Keys: bson.D{{Key: FN.VipOrder.DistrictCode, Value: 1}}},
+ {Keys: bson.D{{Key: FN.VipOrder.CreatedAt, Value: -1}}},
+ }
+ if _, err := coll.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(vipordermod:initIndex) Create Index failed! err:%+v:", err))
+ }
+}
+
+func InsertOneVipOrder(ctx context.Context, doc *VipOrder) *srvdef.SrvErr {
+ if _, err := coll.InsertOne(ctx, doc); err != nil {
+ log.Error("vipordermod:InsertOneVipOrder failed", log.Any("doc", doc), log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+func FindOneVipOrder(ctx context.Context, filter bson.M) (*VipOrder, *srvdef.SrvErr) {
+ var doc VipOrder
+ if err := coll.FindOneEx(ctx, &doc, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("vipordermod:FindOneVipOrder failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &doc, nil
+}
+
+func FindManyVipOrder(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VipOrderList, *srvdef.SrvErr) {
+ var data VipOrderList
+ if err := coll.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("vipordermod:FindManyVipOrder failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+func CountVipOrder(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := coll.Count(ctx, filter)
+ if err != nil {
+ log.Warn("vipordermod:CountVipOrder failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// StatSyncSuccessOrderList 增量拉取支付成功订单(统计中心同步)
+func StatSyncSuccessOrderList(ctx context.Context, successTime time.Time, size int64) (VipOrderList, error) {
+ opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{FN.VipOrder.FinishedAt: 1})
+ // 预留足够时间让注册数据先到达统计中心,避免充值记录找不到对应注册用户
+ endTime := time.Now().Add(-time.Minute * 4)
+ filter := bson.M{
+ FN.VipOrder.FinishedAt: bson.M{"$gt": successTime, "$lt": endTime},
+ FN.VipOrder.Status: pubdef.VipOrderSuccess,
+ }
+ list := VipOrderList{}
+ if err := coll.Find(ctx, &list, filter, opt); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// StatSyncAllOrderList 增量拉取非失败订单(统计中心同步)
+func StatSyncAllOrderList(ctx context.Context, createTime time.Time, size int64) (VipOrderList, error) {
+ opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{FN.VipOrder.CreatedAt: 1})
+ endTime := time.Now().Add(-time.Minute * 1)
+ filter := bson.M{
+ FN.VipOrder.CreatedAt: bson.M{"$gt": createTime, "$lt": endTime},
+ FN.VipOrder.Status: bson.M{"$ne": pubdef.VipOrderFailed},
+ }
+ list := VipOrderList{}
+ if err := coll.Find(ctx, &list, filter, opt); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+// StatSyncVipOrderList 增量拉取会员购买成功订单(统计中心同步)
+func StatSyncVipOrderList(ctx context.Context, successTime time.Time, size int64) (VipOrderList, error) {
+ opt := (&options.FindOptions{}).SetLimit(size).SetSort(bson.M{FN.VipOrder.FinishedAt: 1})
+ endTime := time.Now().Add(-time.Minute * 1)
+ filter := bson.M{
+ FN.VipOrder.FinishedAt: bson.M{"$gt": successTime, "$lt": endTime},
+ FN.VipOrder.Status: pubdef.VipOrderSuccess,
+ FN.VipOrder.OrderType: OrderTypeVip,
+ }
+ list := VipOrderList{}
+ if err := coll.Find(ctx, &list, filter, opt); err != nil {
+ return nil, err
+ }
+ return list, nil
+}
+
+func EmptyDistrictFilter() bson.M {
+ return bson.M{
+ "$or": []bson.M{
+ {FN.VipOrder.DistrictCode: bson.M{"$exists": false}},
+ {FN.VipOrder.DistrictCode: ""},
+ {FN.VipOrder.DistrictCode: nil},
+ },
+ }
+}
+
+// AggregateVipOrder 聚合查询充值订单
+func AggregateVipOrder(ctx context.Context, bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := coll.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("vipordermod:AggregateVipOrder failed", log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVipOrder(ctx context.Context, filter bson.M, update bson.M) (int64, *srvdef.SrvErr) {
+ result, err := coll.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("vipordermod:UpdateManyVipOrder failed", log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return result.ModifiedCount, nil
+}
+
+// UpdateOrderSuccess 将订单标记为支付成功
+func UpdateOrderSuccess(ctx context.Context, tradeNo string, oid string, realAmount int64, paymentAt time.Time) *srvdef.SrvErr {
+ now := time.Now()
+ result, err := coll.UpdateOne(ctx,
+ bson.M{FN.VipOrder.TradeNo: tradeNo},
+ bson.M{"$set": bson.M{
+ FN.VipOrder.Status: pubdef.VipOrderSuccess,
+ FN.VipOrder.Oid: oid,
+ FN.VipOrder.Amount: realAmount,
+ FN.VipOrder.FinishedAt: paymentAt,
+ FN.VipOrder.UpdatedAt: now,
+ }},
+ )
+ if err != nil {
+ log.Warn("vipordermod:UpdateOrderSuccess failed", log.E(err), log.Any("tradeNo", tradeNo))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount == 0 {
+ return srvdef.New(srvdef.ErrNoData, "订单不存在", "")
+ }
+ return nil
+}
diff --git a/pub/models/vipordermod/tag.go b/pub/models/vipordermod/tag.go
new file mode 100644
index 0000000..ed86e3f
--- /dev/null
+++ b/pub/models/vipordermod/tag.go
@@ -0,0 +1,60 @@
+package vipordermod
+
+type TagVipOrder struct {
+ Id string
+ OrderType string
+ UserId string
+ CardId string
+ CardName string
+ CoinCfgId string
+ Coin string
+ Amount string
+ PayMode string
+ TradeNo string
+ Oid string
+ Status string
+ PayUrl string
+ PayUrlMode string
+ PayChannel string
+ PayRate string
+ IPAddr string
+ SysType string
+ DistrictCode string
+ CreatedAt string
+ UpdatedAt string
+ FinishedAt string
+}
+
+var FN struct {
+ VipOrder TagVipOrder
+}
+var TN struct {
+ VipOrder string
+}
+
+func init() {
+ FN.VipOrder.Id = "_id"
+ FN.VipOrder.OrderType = "orderType"
+ FN.VipOrder.UserId = "userId"
+ FN.VipOrder.CardId = "cardId"
+ FN.VipOrder.CardName = "cardName"
+ FN.VipOrder.CoinCfgId = "coinCfgId"
+ FN.VipOrder.Coin = "coin"
+ FN.VipOrder.Amount = "amount"
+ FN.VipOrder.PayMode = "payMode"
+ FN.VipOrder.TradeNo = "tradeNo"
+ FN.VipOrder.Oid = "oid"
+ FN.VipOrder.Status = "status"
+ FN.VipOrder.PayUrl = "payUrl"
+ FN.VipOrder.PayUrlMode = "payUrlMode"
+ FN.VipOrder.PayChannel = "payChannel"
+ FN.VipOrder.PayRate = "payRate"
+ FN.VipOrder.IPAddr = "ipAddr"
+ FN.VipOrder.SysType = "sysType"
+ FN.VipOrder.DistrictCode = "districtCode"
+ FN.VipOrder.CreatedAt = "createdAt"
+ FN.VipOrder.UpdatedAt = "updatedAt"
+ FN.VipOrder.FinishedAt = "finishedAt"
+
+ TN.VipOrder = "vip_order"
+}
diff --git a/pub/models/vipordermod/type.go b/pub/models/vipordermod/type.go
new file mode 100644
index 0000000..31e6db5
--- /dev/null
+++ b/pub/models/vipordermod/type.go
@@ -0,0 +1,44 @@
+package vipordermod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// OrderType 订单类型
+type OrderType int8
+
+const (
+ OrderTypeVip OrderType = 1 // 会员购买
+ OrderTypeCoin OrderType = 2 // 金币购买
+)
+
+// @table: vip_order
+type VipOrder struct {
+ Id int64 `json:"id" bson:"_id"` // 唯一ID
+ OrderType OrderType `json:"orderType" bson:"orderType"` // 订单类型:1会员 2金币
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ // ── VIP 专用 ──
+ CardId int64 `json:"cardId" bson:"cardId"` // 购买套餐ID(会员畅享)
+ CardName string `json:"cardName" bson:"cardName"` // 套餐名快照
+ // ── 金币专用 ──
+ CoinCfgId int64 `json:"coinCfgId" bson:"coinCfgId"` // 购买套餐ID(金币充值)
+ Coin int64 `json:"coin" bson:"coin"` // 到账金币数(快照)
+ Amount int64 `json:"amount" bson:"amount"` // 支付金额(分)
+ PayMode string `json:"payMode" bson:"payMode"` // 支付方式:overage/alipay/wechat
+ TradeNo string `json:"tradeNo" bson:"tradeNo"` // 本地订单号
+ Oid string `json:"oid" bson:"oid"` // 第三方订单号
+ Status pubdef.VipOrderStatus `json:"status" bson:"status"` // 订单状态
+ PayUrl string `json:"payUrl" bson:"payUrl"` // 支付链接
+ PayUrlMode string `json:"payUrlMode" bson:"payUrlMode"` // 链接打开方式
+ PayChannel string `json:"payChannel" bson:"payChannel"` // 支付渠道
+ PayRate string `json:"payRate" bson:"payRate"` // 费率
+ IPAddr string `json:"ipAddr" bson:"ipAddr"` // 客户端 IP
+ SysType string `json:"sysType" bson:"sysType"` // ios/android
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 下单时用户渠道码
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ FinishedAt time.Time `json:"finishedAt" bson:"finishedAt"` // 完成时间
+}
+
+type VipOrderList []*VipOrder
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..d4418e5
--- /dev/null
+++ b/pub/mongos/generate_id.go
@@ -0,0 +1,134 @@
+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
+ VideoTopic *lid
+ VideoInfo *lid
+ TransCategory *lid
+ Translation *lid
+ VideoCreator *lid
+ Comment *lid
+ HtmlText *lid
+ Actress *lid
+ Post *lid
+ PostCategory *lid
+ Card *lid
+ CoinRecharge *lid
+ RechargePackage *lid
+ ContactUs *lid
+ Feedback *lid
+ VipOrder *lid
+ CoinTrans *lid
+ Playlet *lid
+ PlayletChapter *lid
+ PlayletChapterLike *lid
+ PlayletChapterCollect *lid
+ PlayletChapterUnlock *lid
+ PlayletCategory *lid
+ PlayletTheme *lid
+ PlayletTag *lid
+ PlayletLike *lid
+ PlayletCollect *lid
+ PlayletWatchProgress *lid
+ PlayletWatchHistory *lid
+ CommentLike *lid
+ Announcement *lid
+ HotSearch *lid
+ HomeBanner *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.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.Actress = createLid(coll, "Actress", 10)
+ Lid.Post = createLid(coll, "Post", 10)
+ Lid.VideoTopic = createLid(coll, "VideoTopic", 10)
+ Lid.PostCategory = createLid(coll, "PostCategory", 10)
+ Lid.Card = createLid(coll, "card", 10)
+ Lid.CoinRecharge = createLid(coll, "coinRecharge", 10)
+ Lid.RechargePackage = createLid(coll, "rechargePackage", 10)
+ Lid.ContactUs = createLid(coll, "contactus", 10)
+ Lid.VipOrder = createLid(coll, "vipOrder", 10)
+ Lid.CoinTrans = createLid(coll, "coinTrans", 10)
+ Lid.Feedback = createLid(coll, "feedback", 10)
+ Lid.Playlet = createLid(coll, "playlet", 10)
+ Lid.PlayletChapter = createLid(coll, "playletChapter", 10)
+ Lid.PlayletChapterLike = createLid(coll, "playletChapterLike", 10)
+ Lid.PlayletChapterCollect = createLid(coll, "playletChapterCollect", 10)
+ Lid.PlayletChapterUnlock = createLid(coll, "playletChapterUnlock", 10)
+ Lid.PlayletCategory = createLid(coll, "playletCategory", 10)
+ Lid.PlayletTheme = createLid(coll, "playletTheme", 10)
+ Lid.PlayletTag = createLid(coll, "playletTag", 10)
+ Lid.PlayletLike = createLid(coll, "playletLike", 10)
+ Lid.PlayletCollect = createLid(coll, "playletCollect", 10)
+ Lid.PlayletWatchProgress = createLid(coll, "playletWatchProgress", 10)
+ Lid.PlayletWatchHistory = createLid(coll, "playletWatchHistory", 10)
+ Lid.CommentLike = createLid(coll, "commentLike", 10)
+ Lid.Announcement = createLid(coll, "announcement", 10)
+ Lid.HotSearch = createLid(coll, "hotSearch", 10)
+ Lid.HomeBanner = createLid(coll, "homeBanner", 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/payment/trans.go b/pub/payment/trans.go
new file mode 100644
index 0000000..48b9840
--- /dev/null
+++ b/pub/payment/trans.go
@@ -0,0 +1,96 @@
+package payment
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// WriteCoinTransOpt 写流水所需参数
+type WriteCoinTransOpt struct {
+ UserId int64
+ UserName string // 可为空,自动查询
+ FlowDir cointransmod.FlowDir // FlowIn / FlowOut
+ TranType cointransmod.TranType // 交易类型
+ Amount int64 // 本次交易金币数(正整数)
+ OrderNo string // 关联订单号(可为空)
+ RelatedId int64 // 关联内容ID(可为空)
+ Remark string // 备注
+}
+
+// WriteCoinTrans 写一条金币流水记录
+// balanceBefore 和 balanceAfter 从用户当前余额自动推算:
+// - 若传入 balanceAfter(>= 0),直接使用;
+// - 否则查库获取用户当前余额作为 balanceAfter,再反推 balanceBefore。
+func WriteCoinTrans(ctx context.Context, opt WriteCoinTransOpt, balanceAfter int64) *srvdef.SrvErr {
+ userName := opt.UserName
+ if userName == "" {
+ if u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: opt.UserId}); u != nil {
+ userName = u.NickName
+ }
+ }
+
+ var balanceBefore int64
+ if opt.FlowDir == cointransmod.FlowIn {
+ balanceBefore = balanceAfter - opt.Amount
+ } else {
+ balanceBefore = balanceAfter + opt.Amount
+ }
+ if balanceBefore < 0 {
+ balanceBefore = 0
+ }
+
+ remark := opt.Remark
+ if remark == "" {
+ remark = opt.TranType.Desc()
+ }
+
+ doc := &cointransmod.CoinTrans{
+ Id: mongos.Lid.CoinTrans.Get(),
+ UserId: opt.UserId,
+ UserName: userName,
+ FlowDir: opt.FlowDir,
+ TranType: opt.TranType,
+ Amount: opt.Amount,
+ BalanceBefore: balanceBefore,
+ BalanceAfter: balanceAfter,
+ OrderNo: opt.OrderNo,
+ RelatedId: opt.RelatedId,
+ Remark: remark,
+ CreatedAt: utils.Now(),
+ }
+ if serr := cointransmod.InsertOneCoinTrans(ctx, doc); serr != nil {
+ log.Error("WriteCoinTrans InsertOneCoinTrans failed",
+ log.Any("userId", opt.UserId), log.Any("tranType", opt.TranType), log.E(serr))
+ return serr
+ }
+ return nil
+}
+
+// WriteCoinTransByUser 查询用户当前余额后自动写流水(适合已更新余额后调用)
+func WriteCoinTransByUser(ctx context.Context, opt WriteCoinTransOpt) *srvdef.SrvErr {
+ u, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: opt.UserId})
+ if serr != nil || u == nil {
+ return srvdef.New(srvdef.ErrNoData, "用户不存在", "")
+ }
+ if opt.UserName == "" {
+ opt.UserName = u.NickName
+ }
+ return WriteCoinTrans(ctx, opt, u.Recharge)
+}
+
+// CoinTransRemark 生成标准备注
+func CoinTransRemark(tranType cointransmod.TranType, extra string) string {
+ name := tranType.Desc()
+ if extra != "" {
+ return fmt.Sprintf("%s:%s", name, extra)
+ }
+ return name
+}
diff --git a/pub/payment/vip.go b/pub/payment/vip.go
new file mode 100644
index 0000000..8fab675
--- /dev/null
+++ b/pub/payment/vip.go
@@ -0,0 +1,73 @@
+package payment
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// ActivateVip 支付成功后为用户开通/续期会员。
+// 若用户已有未过期会员,则从当前到期时间向后顺延;否则从当前时刻起算。
+// 同时将赠送金币(pkg.GiftCoin)计入用户余额。
+func ActivateVip(ctx context.Context, userId int64, pkg *rechargepackagemod.RechargePackage) (time.Time, *srvdef.SrvErr) {
+ if pkg == nil {
+ return time.Time{}, srvdef.New(srvdef.ErrParamError, "会员套餐不能为空", "")
+ }
+ user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
+ if serr != nil {
+ log.Error("payment:ActivateVip FindOneUser failed", log.Any("userId", userId), log.E(serr))
+ return time.Time{}, serr
+ }
+
+ now := time.Now()
+ duration := time.Duration(pkg.DurationDays) * 24 * time.Hour
+ vipType := vipTypeFromDurationDays(pkg.DurationDays)
+
+ baseTime := now
+ if user.CurrentVipExpire.After(now) {
+ baseTime = user.CurrentVipExpire
+ }
+ newExpire := baseTime.Add(duration)
+
+ setDoc := bson.M{
+ usermod.FN.User.VipType: vipType,
+ usermod.FN.User.CurrentVipExpire: newExpire,
+ usermod.FN.User.ShowCardId: pkg.Id,
+ usermod.FN.User.ShowCardName: pkg.Name,
+ usermod.FN.User.UpdatedAt: now,
+ }
+ updateDoc := bson.M{"$set": setDoc}
+ if pkg.GiftCoin > 0 {
+ updateDoc["$inc"] = bson.M{usermod.FN.User.Recharge: pkg.GiftCoin}
+ }
+
+ if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: userId}, updateDoc); serr != nil {
+ log.Error("payment:ActivateVip UpdateOneUser failed",
+ log.Any("userId", userId), log.Any("packageId", pkg.Id), log.E(serr))
+ return time.Time{}, serr
+ }
+
+ log.Info("payment:ActivateVip success",
+ log.Any("userId", userId),
+ log.Any("packageId", pkg.Id),
+ log.Any("vipType", vipType),
+ log.Any("newExpire", newExpire),
+ )
+ return newExpire, nil
+}
+
+func vipTypeFromDurationDays(days int64) pubdef.VipType {
+ if days >= 365 {
+ return pubdef.VipTypeYear
+ }
+ if days > 0 {
+ return pubdef.VipTypeMonth
+ }
+ return pubdef.VipTypeNone
+}
diff --git a/pub/pubdef/card.go b/pub/pubdef/card.go
new file mode 100644
index 0000000..91c1aa0
--- /dev/null
+++ b/pub/pubdef/card.go
@@ -0,0 +1,26 @@
+package pubdef
+
+// 卡类型
+type CardTypeInt int32
+
+// vip类型
+type VipType int32
+
+const (
+ VipTypeNone VipType = 0 + iota // 没卡
+ VipTypeMonth VipType = 1 // 月卡
+ VipTypeYear VipType = 2 // 年卡
+)
+
+var VipTypeNamedMap = map[VipType]string{
+ VipTypeNone: "无",
+ VipTypeMonth: "月卡",
+ VipTypeYear: "年卡",
+}
+
+type BannerInfo struct {
+ Img string `json:"img" bson:"img"` // banner图片
+ Text string `json:"text" bson:"text"` // banner文字类
+ BackgroundColor string `json:"backgroundColor" bson:"backgroundColor"` // banner背景图
+ SkipUrl string `json:"skipUrl" bson:"skipUrl"` // banner跳转地址
+}
diff --git a/pub/pubdef/media.go b/pub/pubdef/media.go
new file mode 100644
index 0000000..0e55aff
--- /dev/null
+++ b/pub/pubdef/media.go
@@ -0,0 +1,125 @@
+package pubdef
+
+import (
+ "slices"
+)
+
+type IMedia interface {
+ GetID() int64
+ GetTitle() string
+ GetPrice() int64
+ GetCover() string
+ GetPublishID() int64
+ GetLocationType() LocationType
+ GetIsVIP() bool
+ GetIsDark() bool
+ GetCollectionIds() []int64
+ GetEpisodeIds() []int64
+}
+
+type DateCount struct {
+ Date string `json:"date" bson:"date"` // 日期: yyyy-MM-dd
+ Count int64 `json:"count" bson:"count"` // 数量
+}
+
+type DateCountList []DateCount
+
+type MediaType = LocationType
+
+type ContentType int32
+
+const (
+ ContentTypeVideoLong ContentType = 1 // 长视频
+ ContentTypeVideoDsp ContentType = 2 // 短视频
+)
+
+type LocationType int32
+
+const (
+ LocationTypeNone LocationType = 0 // 占位符,无意义
+ LocationTypeVideo LocationType = 1 // 视频
+ LocationTypeVideoLLD LocationType = 2 // 萝莉岛
+ LocationTypeCartoon LocationType = 3 // 动漫
+ LocationTypePost LocationType = 4 // 帖子
+ LocationTypeComment LocationType = 5 // 评论
+ LocationTypeCollection LocationType = 6 // 合集
+ LocationTypeComics LocationType = 7 // 漫画
+ LocationTypeNovel LocationType = 8 // 小说
+ LocationTypeVoiceNovel LocationType = 9 // 有声小说
+ LocationTypePhoto LocationType = 10 // 图集
+ LocationTypeGame LocationType = 11 // 黄游
+ LocationTypeShort LocationType = 12 // 短视频
+)
+
+func (s LocationType) String() string {
+ switch s {
+ case LocationTypeVideo:
+ return "长视频"
+ case LocationTypeVideoLLD:
+ return "萝莉岛"
+ case LocationTypeCartoon:
+ return "动漫"
+ case LocationTypePost:
+ return "动态"
+ case LocationTypeComment:
+ return "评论"
+ case LocationTypeCollection:
+ return "合集"
+ case LocationTypeComics:
+ return "漫画"
+ case LocationTypeNovel:
+ return "小说"
+ case LocationTypeVoiceNovel:
+ return "有声小说"
+ case LocationTypePhoto:
+ return "图集"
+ case LocationTypeGame:
+ return "黄游"
+ case LocationTypeShort:
+ return "短视频"
+ default:
+ return "未知"
+ }
+}
+func (s LocationType) HasIntersection(other ...LocationType) bool {
+ for _, v := range other {
+ if s == v {
+ return true
+ }
+ }
+ return false
+}
+
+// 哪些类型可以分配创作者收益, 目前是所有类型
+func (s LocationType) CanDistributionWorkerIncome() bool {
+ return slices.Contains([]LocationType{
+ LocationTypeVideo,
+ LocationTypeVideoLLD,
+ LocationTypeCartoon,
+ LocationTypePost,
+ LocationTypeCollection,
+ LocationTypeComics,
+ LocationTypeNovel,
+ LocationTypePhoto,
+ LocationTypeGame,
+ }, s)
+}
+
+type MediaTagInfo struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+}
+
+type MediaTagInfoList []MediaTagInfo
+
+func (s MediaTagInfoList) ToUniqueList() MediaTagInfoList {
+ var ret MediaTagInfoList
+ dict := make(map[int64]struct{})
+ for _, v := range s {
+ if _, ok := dict[v.Id]; !ok {
+ dict[v.Id] = struct{}{}
+ ret = append(ret, v)
+ }
+ }
+ return ret
+}
diff --git a/pub/pubdef/paydef.go b/pub/pubdef/paydef.go
new file mode 100644
index 0000000..20dc914
--- /dev/null
+++ b/pub/pubdef/paydef.go
@@ -0,0 +1,52 @@
+package pubdef
+
+import "videoweb/pub/common/maths"
+
+// VipOrderStatus 订单状态
+type VipOrderStatus int32
+
+const (
+ VipOrderPending VipOrderStatus = 1 // 待支付
+ VipOrderSuccess VipOrderStatus = 2 // 支付成功
+ VipOrderFailed VipOrderStatus = 3 // 支付失败
+)
+
+func (s VipOrderStatus) Desc() string {
+ switch s {
+ case VipOrderPending:
+ return "待支付"
+ case VipOrderSuccess:
+ return "支付成功"
+ case VipOrderFailed:
+ return "支付失败"
+ }
+ return "未知"
+}
+
+// PayMode 支付方式
+const (
+ PayModeAlipay = "alipay" // 支付宝
+ PayModeWechat = "wechat" // 微信
+)
+
+// SubmitVipOrderInfo 第三方下单成功后的返回信息
+type SubmitVipOrderInfo struct {
+ TradeNo string
+ Oid string // 第三方订单号
+ PayUrl string // 支付链接
+ PayUrlMode string // 支付链接模式(webview/scheme)
+ PayChannel string // 支付渠道
+ PayRate string // 费率
+}
+
+// VipOrderCallbackParam 支付回调参数(标准化后)
+type VipOrderCallbackParam struct {
+ TradeNo string // 本地订单号
+ Oid string // 第三方订单号
+ RealAmount int64 // 实际支付金额(分)
+ PaymentAt interface{} // 支付时间(time.Time)
+}
+
+func GetTrandNoWithUUID() string {
+ return AppNameZhifuUUID + maths.GenUUID()
+}
diff --git a/pub/pubdef/pubdef.go b/pub/pubdef/pubdef.go
new file mode 100644
index 0000000..1434f56
--- /dev/null
+++ b/pub/pubdef/pubdef.go
@@ -0,0 +1,48 @@
+package pubdef
+
+import "fmt"
+
+type Page struct {
+ PageNum int64 `form:"pageNum" json:"pageNum,omitempty"`
+ PageSize int64 `form:"pageSize" json:"pageSize,omitempty"`
+}
+
+const (
+ OutputDir = "../h5/templates/page/"
+ DateISO = "2006-01-02"
+ TimeISO = "2006-01-02 15:04:05"
+ AppId = 1043
+ AppName = "黄果短剧"
+ AppNameZhifu = "黄果短剧2"
+ AppNameZhifuUUID = "CZ-HGDJ2-"
+ DefaultChannelCode = "HGDL"
+)
+
+// TrackerAppId 数据上报 SDK / 服务端埋点共用的应用 ID
+func TrackerAppId() string {
+ return fmt.Sprintf("JHA-%d", AppId)
+}
+
+const PageSize int64 = 36
+
+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描述
+}
+
+type TranslationType int64
+
+const (
+ TranslationTypeUrl TranslationType = 1 // URL模式
+ TranslationTypeSub TranslationType = 2 // 二级导航
+ TranslationTypeBottom TranslationType = 3 // 底部导航
+)
diff --git a/pub/pubdef/systype.go b/pub/pubdef/systype.go
new file mode 100644
index 0000000..786578d
--- /dev/null
+++ b/pub/pubdef/systype.go
@@ -0,0 +1,132 @@
+package pubdef
+
+import "strings"
+
+type OSType string
+
+const (
+ OSTypeNone OSType = ""
+ OSTypeIOS OSType = "ios"
+ OSTypeAndroid OSType = "android"
+)
+
+func (o OSType) String() string {
+ switch o {
+ case OSTypeIOS:
+ return string(OSTypeIOS)
+ case OSTypeAndroid:
+ return string(OSTypeAndroid)
+ default:
+ return string(OSTypeNone)
+ }
+}
+
+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"
+)
+
+func (s SysType) String() string {
+ return string(s)
+}
+
+func SysTypeFromString(s string) SysType {
+ s = strings.ToLower(strings.TrimSpace(s))
+ switch s {
+ case string(ST_IOS):
+ return ST_IOS
+ case string(ST_Android):
+ return ST_Android
+ case string(ST_H5):
+ return ST_H5
+ case string(ST_PC):
+ return ST_PC
+ case string(ST_H5IOS):
+ return ST_H5IOS
+ case string(ST_H5Android):
+ return ST_H5Android
+ case string(ST_H5PC):
+ return ST_H5PC
+ case string(ST_Light):
+ return ST_Light
+ case string(ST_TG):
+ return ST_TG
+ default:
+ return ST_None
+ }
+}
+
+func ParseOSType(sysType SysType, device string) OSType {
+ device = strings.ToLower(device)
+ switch sysType {
+ case ST_IOS, ST_H5IOS, ST_Light:
+ return OSTypeIOS
+ case ST_Android, ST_H5Android, ST_H5PC, ST_PC:
+ return OSTypeAndroid
+ case ST_H5, ST_TG:
+ if strings.Contains(device, "ios") || strings.Contains(device, "iphone") || strings.Contains(device, "apple") {
+ return OSTypeIOS
+ }
+ return OSTypeAndroid
+ default:
+ return OSTypeNone
+ }
+}
+
+// GetCenterType 获取统计中心系统类型和设备类型。
+// sysType: android / ios;devType: h5 / ios / tg 或原始 device。
+func GetCenterType(sysType SysType, device string) (sType string, dType string) {
+ sType = "ios"
+ dType = "h5"
+ //osType := ParseOSType(sysType, device)
+ //sType = osType.String()
+ //switch sysType {
+ //case ST_IOS:
+ // dType = "ios"
+ // if sType == "" {
+ // sType = "ios"
+ // }
+ //case ST_Android:
+ // dType = device
+ // if sType == "" {
+ // sType = "android"
+ // }
+ //case ST_H5IOS, ST_Light:
+ // dType = "h5"
+ // if sType == "" {
+ // sType = "ios"
+ // }
+ //case ST_H5Android, ST_H5PC, ST_H5, ST_PC:
+ // dType = "h5"
+ // if sType == "" {
+ // sType = "android"
+ // }
+ //case ST_TG:
+ // dType = "tg"
+ // if sType == "" {
+ // if strings.Contains(strings.ToLower(device), "ios") ||
+ // strings.Contains(strings.ToLower(device), "iphone") ||
+ // strings.Contains(strings.ToLower(device), "apple") {
+ // sType = "ios"
+ // } else {
+ // sType = "android"
+ // }
+ // }
+ //default:
+ // dType = device
+ // if sType == "" {
+ // sType = "android"
+ // }
+ //}
+ return
+}
diff --git a/pub/pubdef/user.go b/pub/pubdef/user.go
new file mode 100644
index 0000000..60d78c1
--- /dev/null
+++ b/pub/pubdef/user.go
@@ -0,0 +1,99 @@
+package pubdef
+
+import (
+ "time"
+)
+
+// 机器人
+const (
+ RobotId_Min = 9000
+ RobotId_Max = 10000
+)
+
+// CrawlerUserId 爬虫观看会话的虚拟用户 ID。
+// 不对应任何真实用户;业务侧按 userId<=0 当作未登录,免费内容仍可看。
+const CrawlerUserId int64 = -1
+
+func IsCrawlerUser(userId int64) bool {
+ return userId == CrawlerUserId
+}
+
+// 性别
+type Gender int32
+
+const (
+ Gender_male Gender = 0 // 男
+ Gender_female Gender = 1 // 女
+)
+
+// 用户类型(身份)
+type UserType int32
+
+const (
+ UserTypeGuest UserType = 0 // 游客用户
+ UserTypeNormal UserType = 1 // 普通用户(已注册)
+)
+
+// UserTypeNone 兼容旧命名,等同游客
+const UserTypeNone = UserTypeGuest
+
+var UserTypeMap = map[UserType]string{
+ UserTypeGuest: "游客用户",
+ UserTypeNormal: "普通用户",
+}
+
+func (s UserType) String() string {
+ return UserTypeMap[s]
+}
+
+// 用户状态
+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 // 设备登陆
+ LoginTypeAccount // 账号登陆
+ LoginTypeEmail // 邮箱登陆
+)
+
+type VipInfo struct {
+ CardId int64 `json:"cardId" bson:"cardId,omitempty"` // 会员卡ID
+ VipType VipType `json:"vipType" bson:"vipType,omitempty"` // VIP类型 0:没卡、1:月卡、2:年卡
+ VipExpire time.Time `json:"vipExpire" bson:"vipExpire,omitempty"` // 会员过期时间
+ CardName string `json:"cardName" bson:"cardName,omitempty"` // 会员卡名字
+ VipExpireInt int64 `json:"vipExpireInt" bson:"-"` // 会员过期时间
+ VipExpireStr string `json:"vipExpireStr" bson:"-"` // 会员过期时间
+ IsUpgrade bool `json:"isUpgrade" bson:"-"` // 是否可升级
+}
+
+func (vi *VipInfo) IsValid() bool {
+ return vi.VipType > VipTypeNone && vi.VipExpire.After(time.Now())
+}
+
+func (vi *VipInfo) ReSet() {
+ vi.CardId = 0
+ vi.VipType = VipTypeNone
+ vi.VipExpire = time.Now().AddDate(0, 0, -1)
+ vi.CardName = ""
+}
+func (vi *VipInfo) ToJson() VipInfo {
+ if !vi.IsValid() {
+ vi.ReSet()
+ }
+ vi.VipExpireInt = vi.VipExpire.Unix()
+ vi.VipExpireStr = vi.VipExpire.Format(TimeISO)
+ return *vi
+}
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..21197fe
--- /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..742a735
--- /dev/null
+++ b/pub/redis/redis.go
@@ -0,0 +1,279 @@
+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 SAddCount(key string, members ...interface{}) (int64, error) {
+ return redisCli.SAdd(key, members...).Result()
+}
+
+// 无序集合操作 随机删除集合中某一元素
+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/storage/proxy/autopublish_image.go b/pub/storage/proxy/autopublish_image.go
new file mode 100644
index 0000000..284127e
--- /dev/null
+++ b/pub/storage/proxy/autopublish_image.go
@@ -0,0 +1,131 @@
+package proxy
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/pubdef"
+)
+
+const mediaAutopublishImagePath = "/media/web/autopublish/image"
+
+type mediaImageUploadResp struct {
+ Code int `json:"code"`
+ Data struct {
+ Id string `json:"id"`
+ SourceNameFs string `json:"sourceNameFs"`
+ } `json:"data"`
+ Msg string `json:"msg"`
+}
+
+// UploadAutopublishImage 按 App /api/app/upload/image 同样方式上传到 media
+// POST {media}/media/web/autopublish/image,表单字段 file + appId,不加盖水印。
+func UploadAutopublishImage(fileName string, data []byte) (string, error) {
+ if len(data) == 0 {
+ return "", fmt.Errorf("图片内容为空")
+ }
+ fileName = sanitizeAutopublishFileName(fileName)
+
+ var buf bytes.Buffer
+ mw := multipart.NewWriter(&buf)
+ if err := mw.WriteField("appId", strconv.Itoa(pubdef.AppId)); err != nil {
+ return "", err
+ }
+ if err := writeAutopublishImageFile(mw, fileName, data); err != nil {
+ return "", err
+ }
+ if err := mw.Close(); err != nil {
+ return "", err
+ }
+
+ base := strings.TrimRight(DomainAddressRepair("", BucketVideoAddressV2), "/")
+ if base == "" || (!strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://")) {
+ return "", fmt.Errorf("media地址未配置")
+ }
+ url := base + mediaAutopublishImagePath
+ req, err := http.NewRequest(http.MethodPost, url, &buf)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Content-Type", mw.FormDataContentType())
+
+ cli := &http.Client{Timeout: 60 * time.Second}
+ resp, err := cli.Do(req)
+ if err != nil {
+ log.Error("上传media图片失败", log.Any("url", url), log.E(err))
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("media上传图片失败,状态码 %d,响应 %s", resp.StatusCode, trimRespBody(body))
+ }
+
+ var result mediaImageUploadResp
+ if err = json.Unmarshal(body, &result); err != nil {
+ return "", fmt.Errorf("解析media上传图片响应失败: %w,响应 %s", err, trimRespBody(body))
+ }
+ if result.Code != Success {
+ msg := strings.TrimSpace(result.Msg)
+ if msg == "" {
+ msg = "unknown"
+ }
+ return "", fmt.Errorf("media上传图片失败: %s,响应 %s", msg, trimRespBody(body))
+ }
+ pathStr := strings.TrimSpace(result.Data.SourceNameFs)
+ if pathStr == "" {
+ return "", fmt.Errorf("media上传图片返回空路径")
+ }
+ return pathStr, nil
+}
+
+func writeAutopublishImageFile(mw *multipart.Writer, fileName string, data []byte) error {
+ h := make(textproto.MIMEHeader)
+ contentType := mime.TypeByExtension(strings.ToLower(filepath.Ext(fileName)))
+ if contentType == "" {
+ contentType = "image/jpeg"
+ }
+ h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, fileName))
+ h.Set("Content-Type", contentType)
+ part, err := mw.CreatePart(h)
+ if err != nil {
+ return err
+ }
+ _, err = part.Write(data)
+ return err
+}
+
+func sanitizeAutopublishFileName(fileName string) string {
+ fileName = filepath.Base(strings.TrimSpace(fileName))
+ fileName = strings.ReplaceAll(fileName, `"`, "")
+ if fileName == "" || fileName == "." || !strings.Contains(fileName, ".") {
+ return "playlet_cover.jpg"
+ }
+ for _, r := range fileName {
+ if r > 127 || r < 32 || r == '/' || r == '\\' {
+ return "playlet_cover.jpg"
+ }
+ }
+ return fileName
+}
+
+func trimRespBody(body []byte) string {
+ s := strings.TrimSpace(string(body))
+ if len(s) > 300 {
+ return s[:300]
+ }
+ return s
+}
diff --git a/pub/storage/proxy/object.go b/pub/storage/proxy/object.go
new file mode 100644
index 0000000..40b87ef
--- /dev/null
+++ b/pub/storage/proxy/object.go
@@ -0,0 +1,509 @@
+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 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 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
+}
+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
+}
+
+// SyncMediaFile 将相对路径 m3u8 登记到 media 切片服(status=MediaFileSyncing)
+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..ae9fdd4
--- /dev/null
+++ b/pub/storage/proxy/types.go
@@ -0,0 +1,509 @@
+package proxy
+
+import (
+ "strings"
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+
+ "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"` // 文件名称
+ 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
+ UploadUserName string `bson:"uploadUserName" json:"uploadUserName,omitempty"` // 上传用户名
+ OptionAccount string `bson:"optionAccount,omitempty" json:"optionAccount"` // 最后操作人
+}
+
+func (info *MediaResp) ToVideoInfo() *videoinformationmod.VideoInformation {
+ 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_VideoV2,
+ Status: videoinformationmod.InformationSatusWaiting,
+ Tags: make([]int64, 0),
+ Real: videoinformationmod.Flow{},
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ OptionAccount: info.OptionAccount,
+ UploadUserName: info.UploadUserName,
+ UnencryptedImage: info.UnencryptedImage,
+ SyncId: info.Id.Hex(),
+ }
+ 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: info.Id.Hex(),
+ 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"` // 封面
+ 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 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名称)
+ FriendList []AdvitiseInfo `json:"friendList" bson:"friendList"` // 友链
+ NineGridList []AdvitiseInfo `json:"nineGridList" bson:"nineGridList"` // 9宫格
+ VideoList []AdvitiseInfo `json:"videoList" bson:"videoList"` // 视频列表
+ StopList []AdvitiseInfo `json:"stopList" bson:"stopList"` // 暂停广告
+ ForcedJumpList []AdvitiseInfo `json:"forcedJumpList" bson:"forcedJumpList"` // 强制跳转广告
+ AppInfoAList []AdvitiseInfo `json:"appInfoAList" bson:"appInfoAList"` // 应用管理广告A
+ AppInfoBList []AdvitiseInfo `json:"appInfoBList" bson:"appInfoBList"` // 应用管理广告B
+}
+type AdvitiseQuerysResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data AdvitiseData `json:"data,omitempty"`
+}
+
+// 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"`
+ VideoList []MediaFileSyncItem `json:"videoList"`
+}
+
+// MediaTaskAddReq 提交 media 加水印切片任务
+type MediaTaskAddReq struct {
+ MediaSourceId string `json:"mediaSourceId"`
+ AppId int64 `json:"appId"`
+ 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"`
+ 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..519e179
--- /dev/null
+++ b/pub/storage/proxy/utils.go
@@ -0,0 +1,139 @@
+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" //正式
+)
+
+func InitBucketVideoAddress(addressV2 string) {
+ 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..5b7a39a
--- /dev/null
+++ b/pub/storage/upload.go
@@ -0,0 +1,34 @@
+package storage
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "mime/multipart"
+ "videoweb/pub/storage/proxy"
+)
+
+// 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 QueryMedias(ctx context.Context, in *proxy.MediaQueryesReq, domain string) (*proxy.VideoQuerysResult, error) {
+ return proxy.QueryMedias(in, domain)
+}
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..1a12260
--- /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_91av_web
+ env GOOS=linux GOARCH=amd64 go build $(WEB_BUILD_FLAG) -o web_91av_web
+ tar czf web_91av_web.tar.gz web_91av_web
+ rm -f web_91av_web
+test:
+ go clean
+ rm -f web_91av_web
+ GOOS=linux GOARCH=amd64 go build $(WEB_BUILD_FLAG) -o web_91av_web
+ tar czf web_91av_web.tar.gz web_91av_web docs
+ rm -f web_91av_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/contactus.go b/web/api/proto/contactus.go
new file mode 100644
index 0000000..947ae21
--- /dev/null
+++ b/web/api/proto/contactus.go
@@ -0,0 +1,76 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+// ContactUsListReq web端联系我们分页列表请求
+type ContactUsListReq struct {
+ // 页码
+ pubdef.Page
+ // 用户ID
+ UserId *int64 `form:"userId" json:"userId"`
+ // 邮箱
+ Email string `form:"email" json:"email"`
+ // 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ Type *int32 `form:"type" json:"type"`
+ // 处理状态,0-待处理、1-已处理
+ Status *int32 `form:"status" json:"status"`
+ // 创建开始时间,格式:2026-06-08 00:00:00
+ CreateStartTime string `form:"createStartTime" json:"createStartTime"`
+ // 创建结束时间,格式:2026-06-09 00:00:00
+ CreateEndTime string `form:"createEndTime" json:"createEndTime"`
+}
+
+// ContactUsListResp web端联系我们分页列表响应
+type ContactUsListResp struct {
+ // ID
+ Id int64 `json:"id"`
+ // 用户ID
+ UserId int64 `json:"userId"`
+ // 用户名称
+ UserName string `json:"userName"`
+ // 邮箱
+ Email string `json:"email"`
+ // 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ Type int32 `json:"type"`
+ // 标题
+ Title string `json:"title"`
+ // 内容
+ Content string `json:"content"`
+ // 附件地址
+ Attachments []string `json:"attachments"`
+ // 处理状态,0-待处理、1-已处理
+ Status int32 `json:"status"`
+ // 处理备注
+ Remark string `json:"remark"`
+ // 创建时间
+ CreatedAt time.Time `json:"createdAt"`
+ // 更新时间
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// ContactUsListPageResp web端联系我们分页列表响应
+type ContactUsListPageResp struct {
+ response.BasePageResp
+}
+
+// ContactUsHandleReq web端批量处理联系我们请求
+type ContactUsHandleReq struct {
+ // 联系我们ID集合
+ Ids []int64 `json:"ids" binding:"required"`
+ // 处理状态,0-待处理、1-已处理
+ Status int32 `json:"status" binding:"required"`
+ // 处理备注
+ Remark string `json:"remark"`
+}
+
+// ContactUsHandleResp web端批量处理联系我们响应
+type ContactUsHandleResp struct {
+ // 命中数量
+ MatchedCount int64 `json:"matchedCount"`
+ // 修改数量
+ ModifiedCount int64 `json:"modifiedCount"`
+}
diff --git a/web/api/proto/feedback.go b/web/api/proto/feedback.go
new file mode 100644
index 0000000..e175ed3
--- /dev/null
+++ b/web/api/proto/feedback.go
@@ -0,0 +1,72 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/pubdef"
+)
+
+// FeedbackListReq web端意见反馈分页列表请求
+type FeedbackListReq struct {
+ // 页码
+ pubdef.Page
+ // 用户ID
+ UserId *int64 `form:"userId" json:"userId"`
+ // 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
+ Type *int32 `form:"type" json:"type"`
+ // 处理状态,0-待处理、1-已处理
+ Status *int32 `form:"status" json:"status"`
+ // 创建开始时间,格式:2026-06-08 00:00:00
+ CreateStartTime string `form:"createStartTime" json:"createStartTime"`
+ // 创建结束时间,格式:2026-06-09 00:00:00
+ CreateEndTime string `form:"createEndTime" json:"createEndTime"`
+}
+
+// FeedbackListResp web端意见反馈分页列表响应
+type FeedbackListResp struct {
+ // ID
+ Id int64 `json:"id"`
+ // 用户ID
+ UserId int64 `json:"userId"`
+ // 用户名称
+ UserName string `json:"userName"`
+ // 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
+ Type int32 `json:"type"`
+ // 描述内容
+ Content string `json:"content"`
+ // 问题截图
+ Screenshots []string `json:"screenshots"`
+ // 联系方式
+ Contact string `json:"contact"`
+ // 处理状态,0-待处理、1-已处理
+ Status int32 `json:"status"`
+ // 处理备注
+ Remark string `json:"remark"`
+ // 创建时间
+ CreatedAt time.Time `json:"createdAt"`
+ // 更新时间
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// FeedbackListPageResp web端意见反馈分页列表响应
+type FeedbackListPageResp struct {
+ response.BasePageResp
+}
+
+// FeedbackHandleReq web端批量处理意见反馈请求
+type FeedbackHandleReq struct {
+ // 意见反馈ID集合
+ Ids []int64 `json:"ids" binding:"required"`
+ // 处理状态,0-待处理、1-已处理
+ Status int32 `json:"status" binding:"required"`
+ // 处理备注
+ Remark string `json:"remark"`
+}
+
+// FeedbackHandleResp web端批量处理意见反馈响应
+type FeedbackHandleResp struct {
+ // 命中数量
+ MatchedCount int64 `json:"matchedCount"`
+ // 修改数量
+ ModifiedCount int64 `json:"modifiedCount"`
+}
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..970ee7d
--- /dev/null
+++ b/web/api/proto/playlet.go
@@ -0,0 +1,700 @@
+package proto
+
+import (
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+)
+
+// 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-增量同步
+ IsAdd *bool `form:"isAdd" json:"isAdd,omitempty"` // 是否已加入后台数据:true只查已加入,false只查未加入,不传查全部
+ PageNumber int `form:"pageNumber" json:"pageNumber"` // 页码
+ PageSize int `form:"pageSize" json:"pageSize"` // 个数
+}
+
+// PlayletResourceJHListReq web端嘉华短剧媒资列表请求
+type PlayletResourceJHListReq struct {
+ Title *string `form:"title"` // 标题
+ Tag *string `form:"tag"` // 标签
+ SyncType *int `form:"sync_type"` // 同步类型 1-全量同步 2-增量同步
+ IsAdd *bool `form:"isAdd" json:"isAdd,omitempty"` // 是否已入库:true只查已入库,false只查未入库,不传查全部
+ PageNumber int `form:"pageNumber"` // 页码
+ PageSize int `form:"pageSize"` // 个数
+}
+
+// PlayletResourceMediaActor web端短剧媒资演员
+type PlayletResourceMediaActor struct {
+ Name string `json:"name"` // 名称
+ Avatar string `json:"avatar"` // 头像
+ Gender int `json:"gender"` // 性别
+}
+
+// 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"` // 是否已加入后台数据
+}
+
+// PlayletResourceJHListItem web端嘉华短剧媒资列表项
+type PlayletResourceJHListItem 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"` // 竖版封面
+ HorizontalCoverAlt string `json:"horizontalCover"` // 兼容驼峰
+ VerticalCoverAlt string `json:"verticalCover"` // 兼容驼峰
+ Summary string `json:"summary"` // 简介
+ TotalEpisode uint `json:"total_episode"` // 总集数
+ UpdateStatus int `json:"update_status"` // 更新状态
+ MediaType int `json:"media_type"` // 媒体类型
+ Anchor string `json:"anchor"` // 作者
+ Actors []PlayletResourceMediaActor `json:"actors"` // 演员集合
+ 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"`
+}
+
+// PlayletResourceJHListResp web端嘉华短剧媒资列表响应
+type PlayletResourceJHListResp struct {
+ List []*PlayletResourceJHListItem `json:"list"`
+ Total uint `json:"total"`
+}
+
+// PlayletResourceUseReq web端短剧媒资入库请求
+type PlayletResourceUseReq struct {
+ Ids []uint `json:"ids"` // 媒资中心资源ID集合-多个用逗号隔开,请求示例[28830,28829,28828,28827,28826,28825]
+}
+
+// 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"`
+}
+
+// PlayletListSortType web端短剧列表排序类型,均为降序
+type PlayletListSortType int
+
+const (
+ PlayletListSortCreatedAt PlayletListSortType = 0 // 创建时间(默认)
+ PlayletListSortRecommendRank PlayletListSortType = 1 // 推荐排序
+ PlayletListSortUpdatedAt PlayletListSortType = 2 // 修改时间
+ PlayletListSortCountLike PlayletListSortType = 3 // 点赞数
+ PlayletListSortCountBrowse PlayletListSortType = 4 // 浏览数
+ PlayletListSortCountCollect PlayletListSortType = 5 // 收藏数
+ PlayletListSortOnlineAt PlayletListSortType = 6 // 上架时间
+)
+
+// PlayletListReq web端短剧分页列表请求
+type PlayletListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"`
+ CategoryId *int64 `form:"categoryId" json:"categoryId"` // 分类ID
+ ThemeId *int64 `form:"themeId" json:"themeId"` // 题材id
+ TagId *int64 `form:"tagId" json:"tagId"` // 标签ID
+ ContentType *playletmod.ContentType `form:"contentType" json:"contentType"` // 0短剧 1普通短剧/影集 2漫画短剧 3动漫 4漫画
+ OperateType *playletmod.OperateType `form:"operateType" json:"operateType"` // 运营类型 1-短剧 2-漫剧
+ 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只查非原创,不传查全部
+ IsRecommend *bool `form:"isRecommend" json:"isRecommend"` // 是否原创(推荐)
+ ShowOnBanner *bool `form:"showOnBanner" json:"showOnBanner"`
+ Title string `form:"title" json:"title"`
+ Slug string `form:"slug" json:"slug"` // slug,精确匹配
+ OperatorName string `form:"operatorName" json:"operatorName"` // 最后操作人名称
+ SortType PlayletListSortType `form:"sortType" json:"sortType"` // 排序:0创建时间(默认) 1推荐排序(按分类查询时为该分类排序) 2修改时间 3点赞数 4浏览数 5收藏数 6上架时间,均为降序
+}
+
+// PlayletListResp web端短剧列表项
+type PlayletListResp struct {
+ Id int64 `json:"id"`
+ Status playletmod.Status `json:"status"`
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧(true-是,false-否)
+ IsOriginal bool `json:"isOriginal"` // 是否原创
+ IsRecommend bool `json:"isRecommend"`
+ ShowOnBanner bool `json:"showOnBanner"`
+ Banner string `json:"banner"`
+ ReviewAt time.Time `json:"reviewAt"`
+ OnlineAt time.Time `json:"onlineAt"`
+ CategoryIds []int64 `json:"categoryIds"`
+ CategoryNames []string `json:"categoryNames"`
+ CategoryBinds []PlayletCategoryBind `json:"categoryBinds"` // 分类及该分类下的排序
+ CategoryRank int64 `json:"categoryRank"` // 当前筛选分类下的排序,未按分类查询时为0
+ ThemeIds []int64 `json:"themeIds"`
+ ThemeNames []string `json:"themeNames"`
+ Title string `json:"title"`
+ PlayName string `json:"playName"` // 剧名
+ Description string `json:"description"` // SEO Description
+ 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"`
+ TagNames []string `json:"tagNames"`
+ ContentType playletmod.ContentType `json:"contentType"`
+ OperateType playletmod.OperateType `json:"operateType"` // 运营类型 1-短剧 2-漫剧
+ PayType playletmod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ TotalEpisode int64 `json:"totalEpisode"`
+ 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"`
+ HeatValue int64 `json:"heatValue"`
+ 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"`
+ CompressedCover string `json:"compressedCover"`
+}
+
+// PlayletListPageResp web端短剧分页列表响应
+type PlayletListPageResp struct {
+ response.BasePageResp
+}
+
+// PlayletCategoryBind 短剧绑定的分类及该分类下的排序
+type PlayletCategoryBind struct {
+ Id int64 `json:"id"` // 分类ID
+ Rank int64 `json:"rank"` // 该分类下的排序,越大越靠前
+}
+
+// PlayletAddReq web端新增短剧请求
+type PlayletAddReq struct {
+ CategoryIds []int64 `json:"categoryIds"` // 短剧分类id集合,未传 categoryBinds 时使用,排序为0
+ CategoryBinds []PlayletCategoryBind `json:"categoryBinds"` // 分类及该分类下的排序,优先于 categoryIds
+ ThemeIds []int64 `json:"themeIds"`
+ Title string `json:"title" binding:"required"`
+ PlayName string `json:"playName"` // 剧名
+ 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"`
+ ContentType playletmod.ContentType `json:"contentType"`
+ OperateType playletmod.OperateType `json:"operateType"` // 运营类型 1-短剧 2-漫剧,不传默认短剧
+ PayType playletmod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ TotalEpisode int64 `json:"totalEpisode"`
+ TopRank int64 `json:"topRank"`
+ RecommendRank int64 `json:"recommendRank"`
+ Status playletmod.Status `json:"status"`
+ IsFeatured bool `json:"isFeatured"` // 是否爆剧(true-是,false-否)
+ IsRecommend bool `json:"isRecommend"`
+ ShowOnBanner bool `json:"showOnBanner"`
+ Banner string `json:"banner"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ OnlineAt *time.Time `json:"onlineAt"` // 上架时间,上架时不传则按当前时间
+}
+
+// PlayletAddResp web端新增短剧响应
+type PlayletAddResp struct {
+ Id int64 `json:"id"`
+}
+
+// PlayletUpdateReq web端更新短剧请求
+type PlayletUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Status *playletmod.Status `json:"status"`
+ IsFeatured *bool `json:"isFeatured"` // 是否爆剧(true-是,false-否)
+ ShowOnBanner *bool `json:"showOnBanner"`
+ Banner *string `json:"banner"`
+ CategoryIds *[]int64 `json:"categoryIds"` // 短剧分类id集合,未传 categoryBinds 时使用,保留已有分类排序
+ CategoryBinds *[]PlayletCategoryBind `json:"categoryBinds"` // 分类及该分类下的排序,优先于 categoryIds
+ ThemeIds *[]int64 `json:"themeIds"`
+ Title *string `json:"title"`
+ PlayName *string `json:"playName"` // 剧名
+ 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"`
+ ContentType *playletmod.ContentType `json:"contentType"`
+ OperateType *playletmod.OperateType `json:"operateType"` // 运营类型 1-短剧 2-漫剧
+ PayType *playletmod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
+ SingleCoins *int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
+ TotalEpisode *int64 `json:"totalEpisode"`
+ TopRank *int64 `json:"topRank"`
+ RecommendRank *int64 `json:"recommendRank"`
+ IsRecommend *bool `json:"isRecommend"`
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+ OnlineAt *time.Time `json:"onlineAt"` // 上架时间,新上架时不传则按当前时间
+}
+
+// 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个表示单条,传多个表示批量;会重置其下全部剧集水印
+}
+
+// PlayletStatusSetReq web端单/批量设置短剧上下架请求
+type PlayletStatusSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ Status playletmod.Status `json:"status"` // 上架状态 0-下架 1-上架
+ OnlineAt *time.Time `json:"onlineAt"` // 上架时间,上架时不传则按当前时间
+}
+
+// PlayletOnlineAtFillResp 回填已上架但上架时间为空的短剧
+type PlayletOnlineAtFillResp struct {
+ UpdatedCount int64 `json:"updatedCount"` // 本次回填数量
+}
+
+// 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-否)
+}
+
+// PlayletOperateTypeSetReq web端单/批量设置短剧运营类型请求
+type PlayletOperateTypeSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ OperateType playletmod.OperateType `json:"operateType"` // 运营类型 1-短剧 2-漫剧
+}
+
+// PlayletBannerSetReq web端单/批量设置短剧是否显示在首页banner请求
+type PlayletBannerSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ ShowOnBanner bool `json:"showOnBanner"` // 是否显示在首页banner(true-显示,false-不显示)
+}
+
+// PlayletThemeSetReq web端单/批量设置短剧题材请求
+type PlayletThemeSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ ThemeIds []int64 `json:"themeIds"` // 题材ID集合,可设置多个题材
+}
+
+// PlayletCategorySetReq web端单/批量设置短剧分类请求
+type PlayletCategorySetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ CategoryIds *[]int64 `json:"categoryIds"` // 分类ID集合,未传 categoryBinds 时使用,排序为0;传空数组表示清空分类
+ CategoryBinds *[]PlayletCategoryBind `json:"categoryBinds"` // 分类及该分类下的排序,优先于 categoryIds;传空数组表示清空分类
+}
+
+// PlayletTagSetReq web端单/批量设置短剧标签请求
+type PlayletTagSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ TagIds *[]int64 `json:"tagIds"` // 标签ID集合,可设置多个标签,传空数组表示清空标签,不传表示参数错误
+}
+
+// PlayletVirtualLikeSetReq web端单/批量设置短剧虚拟点赞数请求
+type PlayletVirtualLikeSetReq struct {
+ Ids []int64 `json:"ids"` // 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ MinVirtualCountLike int64 `json:"minVirtualCountLike"` // 虚拟点赞数最小值,必须大于等于0
+ MaxVirtualCountLike int64 `json:"maxVirtualCountLike"` // 虚拟点赞数最大值,必须大于等于最小值
+}
+
+// PlayletCategoryListReq web端分类分页列表请求
+type PlayletCategoryListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"` // 分类ID
+ Name string `form:"name" json:"name"` // 分类名称,支持模糊查询
+ Slug string `form:"slug" json:"slug"` // slug,精确匹配
+ 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
+ Desc string `json:"desc"` // 描述
+ 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 关键词或名称自动生成
+ Desc string `json:"desc"` // 描述
+ 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,历史数据为空时更新会自动生成
+ Desc *string `json:"desc"` // 描述
+ 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
+}
+
+// PlayletThemeListReq web端题材分页列表请求
+type PlayletThemeListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"`
+ Name string `form:"name" json:"name"`
+ Slug string `form:"slug" json:"slug"` // slug,精确匹配
+ Status *bool `form:"status" json:"status"`
+ IsRecommend *bool `form:"isRecommend" json:"isRecommend"` // 是否推荐
+}
+
+// PlayletThemeListResp web端题材列表项
+type PlayletThemeListResp struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"` // slug
+ Icon string `json:"icon"`
+ Status bool `json:"status"`
+ IsRecommend bool `json:"isRecommend"` // 是否推荐
+ 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"`
+ IsRecommend bool `json:"isRecommend"` // 是否推荐
+ Rank int64 `json:"rank"`
+ SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletThemeAddResp web端新增题材响应
+type PlayletThemeAddResp struct {
+ Id int64 `json:"id"`
+}
+
+// PlayletThemeUpdateReq web端更新题材请求
+type PlayletThemeUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Name *string `json:"name"`
+ Slug *string `json:"slug"` // slug,历史数据为空时更新会自动生成
+ Icon *string `json:"icon"`
+ Status *bool `json:"status"`
+ IsRecommend *bool `json:"isRecommend"` // 是否推荐
+ Rank *int64 `json:"rank"`
+ SeoInfo *pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
+}
+
+// PlayletThemeDelReq web端删除题材请求
+type PlayletThemeDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+
+// PlayletThemeRecommendSetReq web端单/批量设置题材是否推荐请求
+type PlayletThemeRecommendSetReq struct {
+ Ids []int64 `json:"ids"` // 题材ID集合,传1个表示单条设置,传多个表示批量设置
+ IsRecommend bool `json:"isRecommend"` // 是否推荐(true-推荐,false-取消推荐)
+}
+
+// PlayletTagListReq web端标签分页列表请求
+type PlayletTagListReq struct {
+ pubdef.Page
+ Id *int64 `form:"id" json:"id"`
+ Name string `form:"name" json:"name"`
+ Slug string `form:"slug" json:"slug"` // slug,精确匹配
+ Status *bool `form:"status" json:"status"`
+}
+
+// PlayletTagListResp web端标签列表项
+type PlayletTagListResp struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"` // slug
+ 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"`
+ Slug string `json:"slug"` // slug,不传则按 SEO 关键词或名称自动生成
+ 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"`
+ Slug *string `json:"slug"` // slug,历史数据为空时更新会自动生成
+ 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"`
+}
+
+// PlayletChapterListReq web端章节分页列表请求
+type PlayletChapterListReq struct {
+ pubdef.Page
+ PlayletId *int64 `form:"playletId" json:"playletId"`
+ Id *int64 `form:"id" json:"id"`
+ WatermarkStatus *playletchaptermod.WatermarkStatus `form:"watermarkStatus" json:"watermarkStatus"`
+}
+
+// PlayletChapterListResp web端章节列表项
+type PlayletChapterListResp struct {
+ Id int64 `json:"id"`
+ PlayletId int64 `json:"playletId"`
+ MediaContentId uint `json:"mediaContentId"`
+ CurrentEpisode int64 `json:"currentEpisode"`
+ Title string `json:"title"`
+ Md5 string `json:"md5"`
+ Hash string `json:"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"`
+ SingleCoins int64 `json:"singleCoins"`
+ WatermarkStatus playletchaptermod.WatermarkStatus `json:"watermarkStatus"`
+ MediaSourceId string `json:"mediaSourceId"`
+ MediaTaskId string `json:"mediaTaskId"`
+ 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"`
+ CurrentEpisode int64 `json:"currentEpisode" binding:"required"`
+ Title string `json:"title"`
+ Md5 string `json:"md5"`
+ Hash string `json:"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"`
+ SingleCoins int64 `json:"singleCoins"`
+ MediaSourceId string `json:"mediaSourceId"`
+}
+
+// PlayletChapterAddResp web端新增章节响应
+type PlayletChapterAddResp struct {
+ Id int64 `json:"id"`
+}
+
+// PlayletChapterUpdateReq web端更新章节请求
+type PlayletChapterUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ CurrentEpisode *int64 `json:"currentEpisode"`
+ Title *string `json:"title"`
+ Md5 *string `json:"md5"`
+ Hash *string `json:"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"`
+ SingleCoins *int64 `json:"singleCoins"`
+}
+
+// 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)
+}
+
+// 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"` // 同步结果集合
+}
+
+// PlayletSlugSyncReq web端同步历史空 slug 请求
+type PlayletSlugSyncReq struct {
+ MaxSize int64 `json:"maxSize"` // 本次最多处理的空 slug 记录数,默认500,最大2000
+}
+
+// PlayletSlugSyncResp web端同步历史空 slug 响应
+type PlayletSlugSyncResp struct {
+ ThemeUpdated int64 `json:"themeUpdated"` // 本次补齐的题材数
+ TagUpdated int64 `json:"tagUpdated"` // 本次补齐的标签数
+ CategoryUpdated int64 `json:"categoryUpdated"` // 本次补齐的分类数
+ PlayletUpdated int64 `json:"playletUpdated"` // 本次补齐的短剧数
+ 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_analytics/analytics.go b/web/api/web_analytics/analytics.go
new file mode 100644
index 0000000..85cb2b1
--- /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} webanalytics.AnalyticsStat "成功"
+// @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} webanalytics.AnalyticsDailyStat "成功"
+// @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)
+}
diff --git a/web/api/web_analytics/proto.go b/web/api/web_analytics/proto.go
new file mode 100644
index 0000000..b2d441f
--- /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"` //当月数据
+}
diff --git a/web/api/web_announcement/announcement.go b/web/api/web_announcement/announcement.go
new file mode 100644
index 0000000..e6076e1
--- /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 公告-列表
+// @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 公告-增加
+// @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 公告-删除
+// @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 公告-更新
+// @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_card/viporder.go b/web/api/web_card/viporder.go
new file mode 100644
index 0000000..8d4b548
--- /dev/null
+++ b/web/api/web_card/viporder.go
@@ -0,0 +1,67 @@
+package web_card
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_viporder"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 会员卡管理
+// @Summary 查询充值订单列表
+// @Description 分页查询充值订单,支持按 OrderType 筛选:1会员 2金币;不传 orderType 则返回全部类型
+// @Accept json
+// @Produce json
+// @Param param body srv_viporder.VipOrderReq true "查询参数"
+// @Success 200 {object} srv_viporder.VipOrderResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Security ApiKeyAuth
+// @Router /api/web/card/order/list [POST]
+func GetOrders(ctx *gin.Context) {
+ var req srv_viporder.VipOrderReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if req.PageNum < 1 {
+ req.PageNum = 1
+ }
+ if req.PageSize < 1 || req.PageSize > 100 {
+ req.PageSize = 20
+ }
+
+ resp, serr := srv_viporder.GetOrders(ctx, &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SyncDistrictCode web端同步历史订单渠道码
+// @Tags 会员卡管理
+// @Summary 同步历史订单渠道码
+// @Description 将历史订单中空的渠道码,按用户当前 districtCode 回填。可重复调用,hasMore 为 true 时继续请求。
+// @Accept json
+// @Produce json
+// @Param param body srv_viporder.SyncDistrictCodeReq false "同步参数"
+// @Success 200 {object} srv_viporder.SyncDistrictCodeResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Security ApiKeyAuth
+// @Router /api/web/card/order/district/sync [POST]
+func SyncDistrictCode(ctx *gin.Context) {
+ var req srv_viporder.SyncDistrictCodeReq
+ if ctx.Request.ContentLength > 0 {
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ }
+ resp, serr := srv_viporder.SyncDistrictCode(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_cfg/cfg.go b/web/api/web_cfg/cfg.go
new file mode 100644
index 0000000..4b17013
--- /dev/null
+++ b/web/api/web_cfg/cfg.go
@@ -0,0 +1,52 @@
+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{})
+}
diff --git a/web/api/web_cfg/protodef.go b/web/api/web_cfg/protodef.go
new file mode 100644
index 0000000..98a2422
--- /dev/null
+++ b/web/api/web_cfg/protodef.go
@@ -0,0 +1,18 @@
+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{}
diff --git a/web/api/web_coin/cointrans.go b/web/api/web_coin/cointrans.go
new file mode 100644
index 0000000..2c282b4
--- /dev/null
+++ b/web/api/web_coin/cointrans.go
@@ -0,0 +1,40 @@
+package web_coin
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_cointrans"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 金币管理
+// @Summary 查询金币交易记录
+// @Description 分页查询金币收支流水,支持按用户、收支方向、交易类型、时间范围筛选
+// @Accept json
+// @Produce json
+// @Param param body srv_cointrans.CoinTransReq true "查询参数"
+// @Success 200 {object} srv_cointrans.CoinTransResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Security ApiKeyAuth
+// @Router /api/web/coin/trans [POST]
+func GetCoinTrans(ctx *gin.Context) {
+ var req srv_cointrans.CoinTransReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if req.PageNum < 1 {
+ req.PageNum = 1
+ }
+ if req.PageSize < 1 || req.PageSize > 100 {
+ req.PageSize = 20
+ }
+
+ resp, serr := srv_cointrans.GetCoinTrans(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_commend/commend.go b/web/api/web_commend/commend.go
new file mode 100644
index 0000000..c9f7b52
--- /dev/null
+++ b/web/api/web_commend/commend.go
@@ -0,0 +1,168 @@
+package web_commend
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_commend"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @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"`
+}
+
+// @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)
+}
+
+// @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)
+}
+
+// @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"`
+}
+
+// @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)
+}
+
+// @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..6b81076
--- /dev/null
+++ b/web/api/web_contactus/contactus.go
@@ -0,0 +1,60 @@
+package web_contactus
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_contactus"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List web端联系我们列表
+// @Tags web联系我们相关接口
+// @Summary web端联系我们列表 -- by 旺鸣
+// @Description 分页查询联系我们列表
+// @Accept json
+// @Produce json
+// @Param params query proto.ContactUsListReq true "web端联系我们列表请求"
+// @Success 200 {object} proto.ContactUsListPageResp{list=[]proto.ContactUsListResp} "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/contactus/list [get]
+func List(ctx *gin.Context) {
+ var req proto.ContactUsListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_contactus.ListContactUs(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Handle web端处理联系我们
+// @Tags web联系我们相关接口
+// @Summary web端处理联系我们 -- by 旺鸣
+// @Description 批量更新联系我们处理状态
+// @Accept json
+// @Produce json
+// @Param params body proto.ContactUsHandleReq true "web端联系我们处理请求"
+// @Success 200 {object} proto.ContactUsHandleResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/contactus/handle [post]
+func Handle(ctx *gin.Context) {
+ var req proto.ContactUsHandleReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_contactus.HandleContactUs(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_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_feedback/feedback.go b/web/api/web_feedback/feedback.go
new file mode 100644
index 0000000..abe8aba
--- /dev/null
+++ b/web/api/web_feedback/feedback.go
@@ -0,0 +1,60 @@
+package web_feedback
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_feedback"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List web端意见反馈列表
+// @Tags web意见反馈相关接口
+// @Summary web端意见反馈列表 -- by 旺鸣
+// @Description 分页查询用户意见反馈列表
+// @Accept json
+// @Produce json
+// @Param params query proto.FeedbackListReq true "web端意见反馈列表请求"
+// @Success 200 {object} proto.FeedbackListPageResp{list=[]proto.FeedbackListResp} "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/feedback/list [get]
+func List(ctx *gin.Context) {
+ var req proto.FeedbackListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_feedback.ListFeedback(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Handle web端处理意见反馈
+// @Tags web意见反馈相关接口
+// @Summary web端处理意见反馈 -- by 旺鸣
+// @Description 批量更新意见反馈处理状态
+// @Accept json
+// @Produce json
+// @Param params body proto.FeedbackHandleReq true "web端意见反馈处理请求"
+// @Success 200 {object} proto.FeedbackHandleResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/feedback/handle [post]
+func Handle(ctx *gin.Context) {
+ var req proto.FeedbackHandleReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_feedback.HandleFeedback(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_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..02a9150
--- /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.Code, err.Tips)
+ 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.Code, err.Tips)
+ 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.Code, err.Tips)
+ 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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_ipblack/ipblack.go b/web/api/web_ipblack/ipblack.go
new file mode 100644
index 0000000..a888a76
--- /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, "")
+}
diff --git a/web/api/web_mediaresource/resource.go b/web/api/web_mediaresource/resource.go
new file mode 100644
index 0000000..5b9e374
--- /dev/null
+++ b/web/api/web_mediaresource/resource.go
@@ -0,0 +1,163 @@
+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)
+}
+
+// ResourceJHList web端嘉华短剧媒资列表
+// @Tags web媒资库相关接口
+// @Summary web端嘉华短剧媒资列表 -- by 王鸣
+// @Description 嘉华短剧媒资列表,固定查询 media_type=5,回填是否已入库,支持按 isAdd 筛选
+// @Accept json
+// @Produce json
+// @Param params query proto.PlayletResourceJHListReq true "嘉华短剧媒资列表请求"
+// @Success 200 {object} proto.PlayletResourceJHListResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/resource/jh/list [get]
+func ResourceJHList(ctx *gin.Context) {
+ var req proto.PlayletResourceJHListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_mediaresource.ListPlayletJHResource(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ResourceJHImport 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/jh/import [post]
+func ResourceJHImport(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.ImportPlayletJHResource(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err, "")
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ResourceJHChapters web端嘉华短剧媒资剧集列表
+// @Tags web媒资库相关接口
+// @Summary web端嘉华短剧媒资剧集列表 -- by 王鸣
+// @Description 按媒资ID查询嘉华短剧剧集信息
+// @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/jh/chapters [get]
+func ResourceJHChapters(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.ListPlayletJHResourceChapters(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, "")
+ 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/playlet.go b/web/api/web_playlet/playlet.go
new file mode 100644
index 0000000..4f6b2d3
--- /dev/null
+++ b/web/api/web_playlet/playlet.go
@@ -0,0 +1,537 @@
+package web_playlet
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/service/srv_playlet"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List web端短剧列表
+// @Tags web短剧相关接口
+// @Summary web端短剧列表
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Add web端新增短剧
+// @Tags web短剧相关接口
+// @Summary web端新增短剧
+// @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)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Update web端更新短剧
+// @Tags web短剧相关接口
+// @Summary web端更新短剧
+// @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); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ 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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// FillOnlineAt web端临时回填已上架但上架时间为空的短剧
+// @Tags web短剧相关接口
+// @Summary 临时回填空上架时间
+// @Description 将已上架且上架时间为空的短剧,上架时间设置为该条修改时间
+// @Accept json
+// @Produce json
+// @Success 200 {object} proto.PlayletOnlineAtFillResp "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/online-at/fill [post]
+func FillOnlineAt(ctx *gin.Context) {
+ resp, err := srv_playlet.FillEmptyPlayletOnlineAt(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// 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.Code, err.Tips)
+ 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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetOperateType web端单/批量设置短剧运营类型
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧运营类型
+// @Description 根据短剧ID集合批量更新 playlet 集合 operateType 字段,1-短剧 2-漫剧
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletOperateTypeSetReq true "单/批量设置短剧运营类型请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/operate-type/set [post]
+func SetOperateType(ctx *gin.Context) {
+ var req proto.PlayletOperateTypeSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletOperateType(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ 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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetTheme web端单/批量设置短剧题材
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧题材 --by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置;themeIds 可设置多个题材
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetCategory web端单/批量设置短剧分类
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧分类 --by 王鸣
+// @Description 根据短剧ID集合批量更新分类;优先使用 categoryBinds(含各分类排序),未传时使用 categoryIds(排序为0);传空数组表示清空分类
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetTag web端单/批量设置短剧标签
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧标签 --by 王鸣
+// @Description 根据短剧ID集合批量更新 playlet 集合 tagIds 字段,传1个ID表示单条设置,传多个ID表示批量设置;tagIds 可设置多个标签,传空数组表示清空标签
+// @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.Code, err.Tips)
+ 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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterList web端章节列表
+// @Tags web短剧相关接口
+// @Summary web端章节列表
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterAdd web端新增章节
+// @Tags web短剧相关接口
+// @Summary web端新增章节
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ChapterUpdate web端更新章节
+// @Tags web短剧相关接口
+// @Summary web端更新章节
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterDel web端单/批量删除章节
+// @Tags web短剧相关接口
+// @Summary web端单/批量删除章节
+// @Description 根据章节ID集合删除 playlet_chapter 记录,传1个ID表示单条删除,传多个ID表示批量删除;同时清理该剧集的点赞、收藏、解锁记录
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterSetPayType web端单/批量设置章节支付方式
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置章节支付方式 --by 王鸣
+// @Description 根据章节ID集合批量更新 playlet_chapter 集合 payType 字段,传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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ChapterSync web端手动同步短剧剧集
+// @Tags web短剧相关接口
+// @Summary web端手动单/批量同步短剧剧集 --by 王鸣
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// SetPayType web端单/批量设置短剧默认支付方式
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置短剧默认支付方式
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ResetWatermark web端按短剧ID批量重置剧集水印
+// @Tags web短剧相关接口
+// @Summary web端按短剧ID批量重置剧集水印
+// @Description 根据短剧ID集合,将其下全部剧集水印状态重置为未处理,并清空水印地址与 media 任务关联;原片地址不变,等待定时任务重新加水印
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SyncSlug web端同步历史空 slug
+// @Tags web短剧相关接口
+// @Summary 同步历史空 slug
+// @Description 为题材、标签、分类、短剧中空的 slug 按 SEO 关键词或名称自动生成。可重复调用,hasMore 为 true 时继续请求。
+// @Accept json
+// @Produce json
+// @Param param body proto.PlayletSlugSyncReq false "同步参数"
+// @Success 200 {object} proto.PlayletSlugSyncResp "成功"
+// @Failure 400 {string} string "参数错误"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/slug/sync [POST]
+func SyncSlug(ctx *gin.Context) {
+ var req proto.PlayletSlugSyncReq
+ if ctx.Request.ContentLength > 0 {
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ }
+ resp, serr := srv_playlet.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_playlet/tag.go b/web/api/web_playlet/tag.go
new file mode 100644
index 0000000..a84bab2
--- /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端标签列表
+// @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端新增标签
+// @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端更新标签
+// @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端删除标签
+// @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..a78e28a
--- /dev/null
+++ b/web/api/web_playlet/theme.go
@@ -0,0 +1,129 @@
+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端题材列表
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ThemeAdd web端新增题材
+// @Tags web短剧相关接口
+// @Summary web端新增题材
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// ThemeUpdate web端更新题材
+// @Tags web短剧相关接口
+// @Summary web端更新题材
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetThemeRecommend web端单/批量设置题材是否推荐
+// @Tags web短剧相关接口
+// @Summary web端单/批量设置题材是否推荐
+// @Description 根据题材ID集合批量更新 playlet_theme 集合 isRecommend 字段
+// @Accept json
+// @Produce json
+// @Param params body proto.PlayletThemeRecommendSetReq true "单/批量设置题材是否推荐请求"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "失败"
+// @Security ApiKeyAuth
+// @Router /api/web/playlet/theme/recommend/set [post]
+func SetThemeRecommend(ctx *gin.Context) {
+ var req proto.PlayletThemeRecommendSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playlet.SetPlayletThemeRecommend(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// ThemeDel web端删除题材
+// @Tags web短剧相关接口
+// @Summary web端删除题材
+// @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.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_playletcategory/category.go b/web/api/web_playletcategory/category.go
new file mode 100644
index 0000000..f677854
--- /dev/null
+++ b/web/api/web_playletcategory/category.go
@@ -0,0 +1,105 @@
+package web_playletcategory
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_playletcategory"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List web端短剧分类列表
+// @Tags web短剧分类相关接口
+// @Summary web端短剧分类列表 --by 王鸣
+// @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 List(ctx *gin.Context) {
+ var req proto.PlayletCategoryListReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playletcategory.ListPlayletCategory(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Add 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 Add(ctx *gin.Context) {
+ var req proto.PlayletCategoryAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_playletcategory.AddPlayletCategory(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Update 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 Update(ctx *gin.Context) {
+ var req proto.PlayletCategoryUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playletcategory.UpdatePlayletCategory(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// Del web端删除短剧分类
+// @Tags web短剧分类相关接口
+// @Summary web端删除短剧分类 --by 王鸣
+// @Description 分类已被短剧引用时禁止删除
+// @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 Del(ctx *gin.Context) {
+ var req proto.PlayletCategoryDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_playletcategory.DelPlayletCategory(ctx, req.Id); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
diff --git a/web/api/web_recharge/recharge.go b/web/api/web_recharge/recharge.go
new file mode 100644
index 0000000..b01ac21
--- /dev/null
+++ b/web/api/web_recharge/recharge.go
@@ -0,0 +1,32 @@
+package web_recharge
+
+import (
+ "net/http"
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ srv_recharge "videoweb/web/service/srv_recharge"
+
+ "github.com/gin-gonic/gin"
+)
+
+// PayCenterBillCallback 嘉华支付中心支付回调
+// @Tags 支付回调
+// @Summary 嘉华支付回调通知(内部接口,无需鉴权)
+// @Accept json
+// @Produce json
+// @Param param body srv_recharge.BillCallbackParam true "回调参数"
+// @Success 200 {string} string "success"
+// @Router /api/web/third/paycenter/bill/callback [POST]
+func PayCenterBillCallback(ctx *gin.Context) {
+ var req srv_recharge.BillCallbackParam
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+
+ if serr := srv_recharge.OnPayCenterCallback(ctx, &req); serr != nil {
+ ctx.String(http.StatusBadRequest, "fail")
+ return
+ }
+ ctx.String(http.StatusOK, "success")
+}
diff --git a/web/api/web_recharge/summary.go b/web/api/web_recharge/summary.go
new file mode 100644
index 0000000..7b27081
--- /dev/null
+++ b/web/api/web_recharge/summary.go
@@ -0,0 +1,34 @@
+package web_recharge
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_viporder"
+
+ "github.com/gin-gonic/gin"
+)
+
+// RechargeSummary 统计指定时间范围内的充值汇总
+// @Tags wbapi统计
+// @Summary 统计充值汇总
+// @Description 按北京时间的开始/结束时间汇总支付成功订单金额与笔数
+// @Accept json
+// @Produce json
+// @Param startTime query string true "开始时间,北京时间,支持 2006-01-02、2006-01-02 15:04:05、RFC3339"
+// @Param endTime query string true "结束时间,北京时间,支持 2006-01-02、2006-01-02 15:04:05、RFC3339"
+// @Success 200 {object} srv_viporder.RechargeSummaryResp "成功"
+// @Failure 400 {string} string "失败"
+// @Router /wbapi/hgdj/recharge/summary [GET]
+func RechargeSummary(ctx *gin.Context) {
+ var req srv_viporder.RechargeSummaryReq
+ if err := ctx.ShouldBindQuery(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
+ return
+ }
+ resp, serr := srv_viporder.GetRechargeSummary(ctx, &req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr, nil)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/web/api/web_rechargepackage/package.go b/web/api/web_rechargepackage/package.go
new file mode 100644
index 0000000..1aa2654
--- /dev/null
+++ b/web/api/web_rechargepackage/package.go
@@ -0,0 +1,137 @@
+package web_rechargepackage
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/web/service/srv_rechargepackage"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 查询购买套餐列表
+// @Tags 套餐管理
+// @Summary 查询购买套餐列表 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body srv_rechargepackage.RechargePackageListReq true "查询参数"
+// @Success 200 {object} srv_rechargepackage.RechargePackageListResp "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/recharge/package/list [POST]
+func List(ctx *gin.Context) {
+ var req srv_rechargepackage.RechargePackageListReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, err := srv_rechargepackage.ListRechargePackage(ctx, &req)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// VipList 查询会员畅享套餐选项
+// @Tags 套餐管理
+// @Summary 查询会员畅享套餐选项 -- by 王鸣
+// @Description 只返回 PackageType=1 的启用套餐,字段仅包含 id 和 name,不分页
+// @Produce json
+// @Success 200 {array} srv_rechargepackage.VipPackageOption "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/recharge/package/vip/list [POST]
+func VipList(ctx *gin.Context) {
+ resp, err := srv_rechargepackage.ListVipPackageOptions(ctx)
+ if err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
+
+// Add 新增购买套餐
+// @Tags 套餐管理
+// @Summary 新增购买套餐 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body srv_rechargepackage.RechargePackageAddReq true "新增参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/recharge/package/add [POST]
+func Add(ctx *gin.Context) {
+ var req srv_rechargepackage.RechargePackageAddReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_rechargepackage.AddRechargePackage(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// Update 更新购买套餐
+// @Tags 套餐管理
+// @Summary 更新购买套餐 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body srv_rechargepackage.RechargePackageUpdateReq true "更新参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/recharge/package/update [POST]
+func Update(ctx *gin.Context) {
+ var req srv_rechargepackage.RechargePackageUpdateReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_rechargepackage.UpdateRechargePackage(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// Delete 删除购买套餐
+// @Tags 套餐管理
+// @Summary 删除购买套餐 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body srv_rechargepackage.RechargePackageDelReq true "删除参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/recharge/package/del [POST]
+func Delete(ctx *gin.Context) {
+ var req srv_rechargepackage.RechargePackageDelReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_rechargepackage.DeleteRechargePackage(ctx, req.Id); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetStatus 单/批量设置购买套餐状态
+// @Tags 套餐管理
+// @Summary 单/批量设置购买套餐状态 -- by 王鸣
+// @Accept json
+// @Produce json
+// @Param param body srv_rechargepackage.RechargePackageStatusSetReq true "设置状态参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/recharge/package/status/set [POST]
+func SetStatus(ctx *gin.Context) {
+ var req srv_rechargepackage.RechargePackageStatusSetReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if err := srv_rechargepackage.SetRechargePackageStatus(ctx, &req); err != nil {
+ common.ServerJSON(ctx, err.Code, err.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
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_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..54d2edc
--- /dev/null
+++ b/web/api/web_user/user.go
@@ -0,0 +1,96 @@
+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, "")
+}
+
+// @Tags 用户管理
+// @Summary 赠送/扣除用户金币
+// @Description coin 为正数表示赠送,为负数表示扣除;操作会自动记录金币流水
+// @Accept json
+// @Produce json
+// @Param param body srv_user.GiftCoinReq true "赠送参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/user/giftcoin [POST]
+func GiftCoin(ctx *gin.Context) {
+ var req srv_user.GiftCoinReq
+ if err := ctx.ShouldBind(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if serr := srv_user.GiftCoin(ctx, &req); serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// @Tags 用户管理
+// @Summary 修改用户会员
+// @Description 根据会员畅享套餐ID修改用户会员信息,不处理赠送金币
+// @Accept json
+// @Produce json
+// @Param param body srv_user.UpdateVipReq true "修改会员参数"
+// @Success 200 {string} string "成功"
+// @Security ApiKeyAuth
+// @Router /api/web/user/vip/update [POST]
+func UpdateVip(ctx *gin.Context) {
+ var req srv_user.UpdateVipReq
+ if err := ctx.ShouldBindJSON(&req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ if serr := srv_user.UpdateVip(ctx, &req); 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..da9f0f6
--- /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"
+)
+
+// @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, serr := srv_videocategory.GetVideoCategorys(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 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
+ }
+ serr := srv_videocategory.AddVideoCategory(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_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
+ }
+ serr := srv_videocategory.DelVideoCategory(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_videocategory.VideoCategoryUpdateReq 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
+ }
+
+ serr := srv_videocategory.UpdateVideoCategory(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/creater.go b/web/api/web_video/creater.go
new file mode 100644
index 0000000..0e3e041
--- /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.VideoCreaterUpdateReq 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/hottag.go b/web/api/web_video/hottag.go
new file mode 100644
index 0000000..ca8992b
--- /dev/null
+++ b/web/api/web_video/hottag.go
@@ -0,0 +1,45 @@
+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/cgdj/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 标签管理
+// @Summary 查询今日和昨日 IP 去重统计
+// @Description 返回 app 端同步到 MongoDB 的每日访问 IP 去重数
+// @Accept json
+// @Produce json
+// @Success 200 {object} srv_videotag.IpStatResp "成功"
+// @Router /wbapi/cgdj/ipstat [GET]
+func GetIpStatData(ctx *gin.Context) {
+ resp := srv_videotag.FetchIpStat(ctx)
+ common.ServerJSON(ctx, srvdef.Success, resp)
+}
diff --git a/web/api/web_video/m3u8.go b/web/api/web_video/m3u8.go
new file mode 100644
index 0000000..a66fa01
--- /dev/null
+++ b/web/api/web_video/m3u8.go
@@ -0,0 +1,125 @@
+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
+ useNewKey := ctx.Query("useNewKey") == "1" || ctx.Query("newKey") == "1"
+ var buff *bytes.Buffer
+ if useNewKey {
+ // 短剧原片:不追加媒资版本前缀,并使用 conf/newm3u8.key
+ _, buff, err = m3u8.TransformExOriginalPath(req.Name, cdnDomain, origDomain, 3, webg.Conf.TsAuthKey, webg.Conf.LsjMediaConf.M3u8Yuan, "", "/api/web/newenkey")
+ } else {
+ _, 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)
+}
+
+// NewEnkey 返回短剧原片使用的新 M3U8 解密密钥(conf/newm3u8.key)。
+func NewEnkey(ctx *gin.Context) {
+ fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
+ buffer, err := m3u8.NewEnkey(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..05c65fe
--- /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.VideoTagInfoUpdateReq 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/topic.go b/web/api/web_video/topic.go
new file mode 100644
index 0000000..9b36814
--- /dev/null
+++ b/web/api/web_video/topic.go
@@ -0,0 +1,111 @@
+package web_video
+
+import (
+ "videoweb/pub/common"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videotopicmod"
+ "videoweb/web/service/srv_videotopic"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Tags 视频专题
+// @Summary 分组分页获取视频专题
+// @Description 分组分页获取视频专题
+// @Accept json
+// @Produce json
+// @Param param body srv_videotopic.VideoTopicReq true "参数列表"
+// @Success 200 {object} srv_videotopic.VideoTopicResp "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotopic/list [POST]
+func GetVideoTopic(ctx *gin.Context) {
+ var req = &srv_videotopic.VideoTopicReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+ resp, serr := srv_videotopic.GetVideoTopics(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 videotopicmod.VideoTopicModel true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotopic/add [POST]
+func AddOneVideoTopic(ctx *gin.Context) {
+ var req = &videotopicmod.VideoTopicModel{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videotopic.AddVideoTopic(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_videotopic.VideoTopicDelReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotopic/del [POST]
+func DelOneVideoTopic(ctx *gin.Context) {
+ var req = &srv_videotopic.VideoTopicDelReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, err)
+ return
+ }
+ serr := srv_videotopic.DelVideoTopic(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_videotopic.VideoTopicUpdateReq true "参数列表"
+// @Success 200 {string} string "成功"
+// @Failure 400 {string} string "获取失败的返回结果"
+// @Security ApiKeyAuth
+// @Router /api/web/videotopic/update [POST]
+func UpdateOneVideoTopic(ctx *gin.Context) {
+ var req = &srv_videotopic.VideoTopicUpdateReq{}
+ if err := ctx.ShouldBind(req); err != nil {
+ common.ServerJSON(ctx, srvdef.ErrParamError, nil)
+ return
+ }
+
+ serr := srv_videotopic.UpdateVideoTopic(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..c5c1b47
--- /dev/null
+++ b/web/api/web_video/video.go
@@ -0,0 +1,183 @@
+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)
+}
+
+// @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 删除一个视频管理
+// @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.ErrParamError, nil)
+ return
+ }
+ userId := ctx.GetInt64(auth.AccId)
+ ua, _ := common.GetUA(ctx)
+ serr := srv_videoinfo.UpdateVideoInfo(ctx, req.Id, userId, req.Param, ua)
+ 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.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
+ }
+
+ serr := srv_videoinfo.AddVideoCollection(ctx, req)
+ if serr != nil {
+ common.ServerJSON(ctx, serr.Code, serr.Tips)
+ return
+ }
+
+ common.ServerJSON(ctx, srvdef.Success, "")
+}
+
+// SetVideoFakeWatchTimes 单/批量设置视频假观看次数
+// @Tags 视频管理
+// @Summary 单/批量设置视频假观看次数 --by 旺鸣
+// @Description 根据ids批量随机设置视频假观看次数
+// @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)
+}
+
+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/conf/lsjm3u8.key b/web/conf/lsjm3u8.key
new file mode 100644
index 0000000..d93e501
Binary files /dev/null and b/web/conf/lsjm3u8.key differ
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..1d94123
--- /dev/null
+++ b/web/conf/web.yaml
@@ -0,0 +1,41 @@
+redis:
+ addr: 172.104.181.99:16789
+ db: 2
+ passwd: "B6vJHlYiTseLj77z"
+mongodb:
+ addr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,103.3.61.140:57020,103.3.61.140:57021/web_91av?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
+ logaddr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,103.3.61.140:57020,103.3.61.140:57021/web_91av_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: "25120"
+prefetchSubmitUrl: "http://172.104.171.80:9067/master/submit"
+aiCenterAppId: 25311
+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"
+lsjMediaConf:
+ appId: "430696"
+ apiKey: "ed48e96ce490bc94bd5eb5133dd542d0"
+ apiUrl: "https://zhlsaj.zrr4ij4.site"
+ imageYuan: "https://d1kh52pvweq1c0.cloudfront.net"
+ keyHex: "525202f9149e061d"
+ m3u8Yuan: "https://zhlsaj.zrr4ij4.site"
+ tsYuan: "https://tssdaa.hlvwcn.cn"
+
+# 媒资配置
+mediaResourceDBCfg:
+ baseUrl: "https://mediav2-api.htycpc.com"
+ merchat: "91porn"
+ secret: "08f1c3ab5e11e9b6c18aa24c06d1eb77"
diff --git a/web/deploy_prod.sh b/web/deploy_prod.sh
new file mode 100755
index 0000000..9794f2f
--- /dev/null
+++ b/web/deploy_prod.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+FILE_NAME="web_mdcom_web"
+REMOTE_FILE_PATH="/home/server2/web_mdcom/web/"
+TAG_NAME="web_mdcom_web.tar.gz"
+
+set -e
+set -o
+function send_msg() {
+ msg="@jscdsndj @xiuyuan_Lee @steven0yu %0A服务:SERVER2 站点-91spz.com管理服务(WEB)%0A更新内容:$content%0A创建时间: $(date +%F_%X)%0A操作人:$(git config user.name)"
+ curl -X POST "https://api.telegram.org/bot6357085965:AAEmXfjZnZwnDTicL7Wjbd7cXvYv_G98w8A/sendMessage" -d "chat_id=$1&text=$msg" >/dev/null 2>&1
+}
+
+read -p "输入yes, 开始上传: " word
+if [[ ! "yes" =~ $word ]]; then
+ echo "终止"
+else
+ read -e -p "本次更新内容(输入q退出编辑): " -r content
+ echo "开始"
+ rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
+ GOOS=linux GOARCH=amd64 go build -o $FILE_NAME
+ md5sum $FILE_NAME | cut -d ' ' -f1 >yinsemd5.txt
+ tar zvcf $TAG_NAME ./$FILE_NAME yinsemd5.txt
+ md5sum $TAG_NAME | cut -d ' ' -f1 >yinsemd5.txt
+ scp -i ~/.ssh/id_rsa -P 22889 ./yinsemd5.txt server2@116.212.120.2:$REMOTE_FILE_PATH
+ scp -i ~/.ssh/id_rsa -P 22889 ./$TAG_NAME server2@116.212.120.2:$REMOTE_FILE_PATH
+ send_msg -4791758991 #服务器小组
+fi
+rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
\ No newline at end of file
diff --git a/web/deploy_test.sh b/web/deploy_test.sh
new file mode 100755
index 0000000..1342b70
--- /dev/null
+++ b/web/deploy_test.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+sudo make test
+
+scp -P 22889 web_91av_web.tar.gz root@172.104.181.99:/server/web_91av/web/
+ssh root@172.104.181.99 -p 22889 "cd /server/web_91av/web/ && /bin/tar -xf /server/web_91av/web/web_91av_web.tar.gz && /usr/bin/supervisorctl restart web_91av_web"
+if [ $? -eq 0 ];then
+ echo -e "\033[32m 91spz.com web更新成功 \033[0m"
+else
+ echo -e "\033[31m 91spz.com web更新失败 \033[0m"
+fi
+rm -f web_91av_web.tar.gz
diff --git a/web/docs/docs.go b/web/docs/docs.go
new file mode 100644
index 0000000..51e7fab
--- /dev/null
+++ b/web/docs/docs.go
@@ -0,0 +1,11352 @@
+// Package docs Code generated by swaggo/swag. DO NOT EDIT
+package 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/card/order/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询充值订单,支持按 OrderType 筛选:1会员 2金币;不传 orderType 则返回全部类型",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "会员卡管理"
+ ],
+ "summary": "查询充值订单列表",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_viporder.VipOrderReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_viporder.VipOrderResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/coin/trans": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询金币收支流水,支持按用户、收支方向、交易类型、时间范围筛选",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币管理"
+ ],
+ "summary": "查询金币交易记录",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_cointrans.CoinTransReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_cointrans.CoinTransResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/handle": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "批量更新联系我们处理状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web联系我们相关接口"
+ ],
+ "summary": "web端处理联系我们 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "web端联系我们处理请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsHandleReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsHandleResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询联系我们列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web联系我们相关接口"
+ ],
+ "summary": "web端联系我们列表 -- by 旺鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "创建结束时间,格式:2026-06-09 00:00:00",
+ "name": "createEndTime",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "创建开始时间,格式:2026-06-08 00:00:00",
+ "name": "createStartTime",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "邮箱",
+ "name": "email",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "处理状态,0-待处理、1-已处理",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "用户ID",
+ "name": "userId",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ContactUsListPageResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContactUsListResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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/feedback/handle": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "批量更新意见反馈处理状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web意见反馈相关接口"
+ ],
+ "summary": "web端处理意见反馈 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "web端意见反馈处理请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackHandleReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackHandleResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/feedback/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询用户意见反馈列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web意见反馈相关接口"
+ ],
+ "summary": "web端意见反馈列表 -- by 旺鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "创建结束时间,格式:2026-06-09 00:00:00",
+ "name": "createEndTime",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "创建开始时间,格式:2026-06-08 00:00:00",
+ "name": "createStartTime",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "处理状态,0-待处理、1-已处理",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "用户ID",
+ "name": "userId",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.FeedbackListPageResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.FeedbackListResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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/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": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧",
+ "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/chapter/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增章节",
+ "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/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端章节列表",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone"
+ ],
+ "name": "watermarkStatus",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据章节ID集合删除 playlet_chapter 记录,传1个ID表示单条删除,传多个ID表示批量删除;同时清理该剧集的点赞、收藏、解锁记录",
+ "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/pay-type/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据章节ID集合批量更新 playlet_chapter 集合 payType 字段,传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/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新章节",
+ "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/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧列表",
+ "parameters": [
+ {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集",
+ "ContentTypeAnime": "动漫",
+ "ContentTypeComic": "漫画短剧",
+ "ContentTypeManga": "漫画",
+ "ContentTypeTheater": "短剧"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ],
+ "name": "contentType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否爆剧(true-是,false-否)",
+ "name": "isFeatured",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "name": "isRecommend",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "name": "showOnBanner",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ],
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "themeId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "name": "title",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletListPageResp"
+ }
+ },
+ "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端新增标签",
+ "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端删除标签",
+ "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端标签列表",
+ "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表示批量设置;tagIds 可设置多个标签,传空数组表示清空标签",
+ "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端更新标签",
+ "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端新增题材",
+ "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端删除题材",
+ "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端题材列表",
+ "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.PlayletThemeListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置;themeIds 可设置多个题材",
+ "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端更新题材",
+ "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": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新短剧",
+ "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/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/recharge/package/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "新增购买套餐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "删除购买套餐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "查询购买套餐列表 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageListResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/status/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "单/批量设置购买套餐状态 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "设置状态参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageStatusSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "更新购买套餐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/vip/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "只返回 PackageType=1 的启用套餐,字段仅包含 id 和 name,不分页",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "查询会员畅享套餐选项 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_rechargepackage.VipPackageOption"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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": "媒体类型 4-短剧 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/stat/analytics": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "统计数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "获取统计数据"
+ ],
+ "summary": "统计数据",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/webanalytics.AnalyticsStat"
+ }
+ },
+ "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/webanalytics.AnalyticsDailyStat"
+ }
+ },
+ "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/third/paycenter/bill/callback": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "支付回调"
+ ],
+ "summary": "嘉华支付回调通知(内部接口,无需鉴权)",
+ "parameters": [
+ {
+ "description": "回调参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_recharge.BillCallbackParam"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "success",
+ "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/giftcoin": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "coin 为正数表示赠送,为负数表示扣除;操作会自动记录金币流水",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "赠送/扣除用户金币",
+ "parameters": [
+ {
+ "description": "赠送参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.GiftCoinReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "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/user/vip/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据会员畅享套餐ID修改用户会员信息,不处理赠送金币",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "修改用户会员",
+ "parameters": [
+ {
+ "description": "修改会员参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.UpdateVipReq"
+ }
+ }
+ ],
+ "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.VideoCategoryUpdateReq"
+ }
+ }
+ ],
+ "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.VideoCreaterUpdateReq"
+ }
+ }
+ ],
+ "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/setFakeWatchTimes": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据ids批量随机设置视频假观看次数",
+ "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/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.VideoTagInfoUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个视频专题",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频专题"
+ ],
+ "summary": "添加一个视频专题",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videotopicmod.VideoTopicModel"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/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_videotopic.VideoTopicDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/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_videotopic.VideoTopicReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotopic.VideoTopicResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/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_videotopic.VideoTopicUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/wbapi/69cgw/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"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "cointransmod.CoinTrans": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "本次交易金币数(正整数)",
+ "type": "integer"
+ },
+ "balanceAfter": {
+ "description": "交易后余额",
+ "type": "integer"
+ },
+ "balanceBefore": {
+ "description": "交易前余额",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "交易时间",
+ "type": "string"
+ },
+ "flowDir": {
+ "description": "收支方向:1收入 -1支出",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.FlowDir"
+ }
+ ]
+ },
+ "id": {
+ "description": "唯一ID",
+ "type": "integer"
+ },
+ "orderNo": {
+ "description": "关联订单号(可为空)",
+ "type": "string"
+ },
+ "relatedId": {
+ "description": "关联内容ID(视频等,可为空)",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注描述",
+ "type": "string"
+ },
+ "tranType": {
+ "description": "交易类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.TranType"
+ }
+ ]
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户昵称(快照)",
+ "type": "string"
+ }
+ }
+ },
+ "cointransmod.FlowDir": {
+ "type": "integer",
+ "enum": [
+ 1,
+ -1
+ ],
+ "x-enum-comments": {
+ "FlowIn": "收入(充值、赠送)",
+ "FlowOut": "支出(消费、解锁)"
+ },
+ "x-enum-varnames": [
+ "FlowIn",
+ "FlowOut"
+ ]
+ },
+ "cointransmod.TranType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "x-enum-comments": {
+ "TranTypeAdminAdjust": "管理员手动调整",
+ "TranTypeCoinRecharge": "金币充值(购买金币)",
+ "TranTypeDarkAlley": "历史遗留交易类型(已下线)",
+ "TranTypeVideoUnlock": "视频解锁",
+ "TranTypeVipGiftCoin": "购买会员赠送金币"
+ },
+ "x-enum-varnames": [
+ "TranTypeCoinRecharge",
+ "TranTypeVipGiftCoin",
+ "TranTypeDarkAlley",
+ "TranTypeVideoUnlock",
+ "TranTypeAdminAdjust"
+ ]
+ },
+ "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"
+ }
+ }
+ },
+ "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
+ ],
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone"
+ ]
+ },
+ "playletmod.ContentType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "x-enum-comments": {
+ "ContentTypeAlbum": "普通短剧/影集",
+ "ContentTypeAnime": "动漫",
+ "ContentTypeComic": "漫画短剧",
+ "ContentTypeManga": "漫画",
+ "ContentTypeTheater": "短剧"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum",
+ "ContentTypeComic",
+ "ContentTypeAnime",
+ "ContentTypeManga"
+ ]
+ },
+ "playletmod.Status": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ]
+ },
+ "proto.ContactUsHandleReq": {
+ "type": "object",
+ "required": [
+ "ids",
+ "status"
+ ],
+ "properties": {
+ "ids": {
+ "description": "联系我们ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsHandleResp": {
+ "type": "object",
+ "properties": {
+ "matchedCount": {
+ "description": "命中数量",
+ "type": "integer"
+ },
+ "modifiedCount": {
+ "description": "修改数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsListResp": {
+ "type": "object",
+ "properties": {
+ "attachments": {
+ "description": "附件地址",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FeedbackHandleReq": {
+ "type": "object",
+ "required": [
+ "ids",
+ "status"
+ ],
+ "properties": {
+ "ids": {
+ "description": "意见反馈ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackHandleResp": {
+ "type": "object",
+ "properties": {
+ "matchedCount": {
+ "description": "命中数量",
+ "type": "integer"
+ },
+ "modifiedCount": {
+ "description": "修改数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackListResp": {
+ "type": "object",
+ "properties": {
+ "contact": {
+ "description": "联系方式",
+ "type": "string"
+ },
+ "content": {
+ "description": "描述内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "screenshots": {
+ "description": "问题截图",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ },
+ "type": {
+ "description": "问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletAddReq": {
+ "type": "object",
+ "required": [
+ "title"
+ ],
+ "properties": {
+ "anchor": {
+ "type": "string"
+ },
+ "banner": {
+ "type": "string"
+ },
+ "contentType": {
+ "$ref": "#/definitions/playletmod.ContentType"
+ },
+ "cover": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "type": "boolean"
+ },
+ "preview": {
+ "type": "string"
+ },
+ "recommendRank": {
+ "type": "integer"
+ },
+ "showOnBanner": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/playletmod.Status"
+ },
+ "tagIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "type": "string"
+ },
+ "topRank": {
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletAddResp": {
+ "type": "object",
+ "properties": {
+ "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.PlayletChapterAddReq": {
+ "type": "object",
+ "required": [
+ "currentEpisode",
+ "playletId"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "type": "integer"
+ },
+ "hash": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "md5": {
+ "type": "string"
+ },
+ "mediaSize": {
+ "type": "integer"
+ },
+ "mediaTime": {
+ "type": "integer"
+ },
+ "payType": {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ },
+ "playletId": {
+ "type": "integer"
+ },
+ "ratio": {
+ "type": "number"
+ },
+ "singleCoins": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "videoUrl": {
+ "type": "string"
+ },
+ "width": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterDelReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "章节ID集合,传1个表示单条删除,传多个表示批量删除",
+ "type": "array",
+ "items": {
+ "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-金币时必填且需\u003e0)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "type": "integer"
+ },
+ "hash": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "md5": {
+ "type": "string"
+ },
+ "mediaSize": {
+ "type": "integer"
+ },
+ "mediaTime": {
+ "type": "integer"
+ },
+ "payType": {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ },
+ "ratio": {
+ "type": "number"
+ },
+ "singleCoins": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "videoUrl": {
+ "type": "string"
+ },
+ "width": {
+ "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.PlayletRecommendSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isRecommend": {
+ "description": "是否推荐(true-推荐,false-不推荐)",
+ "type": "boolean"
+ }
+ }
+ },
+ "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集合-多个用逗号隔开,请求示例[28830,28829,28828,28827,28826,28825]",
+ "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"
+ },
+ "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"
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "icon": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "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": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "anchor": {
+ "type": "string"
+ },
+ "banner": {
+ "type": "string"
+ },
+ "contentType": {
+ "$ref": "#/definitions/playletmod.ContentType"
+ },
+ "cover": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "type": "boolean"
+ },
+ "preview": {
+ "type": "string"
+ },
+ "recommendRank": {
+ "type": "integer"
+ },
+ "showOnBanner": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/playletmod.Status"
+ },
+ "tagIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "type": "string"
+ },
+ "topRank": {
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeEmail": "邮箱登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeAccount",
+ "LoginTypeEmail"
+ ]
+ },
+ "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"
+ ]
+ },
+ "pubdef.UserType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 0
+ ],
+ "x-enum-comments": {
+ "UserTypeGuest": "游客用户",
+ "UserTypeNormal": "普通用户(已注册)"
+ },
+ "x-enum-varnames": [
+ "UserTypeGuest",
+ "UserTypeNormal",
+ "UserTypeNone"
+ ]
+ },
+ "pubdef.VipOrderStatus": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "VipOrderFailed": "支付失败",
+ "VipOrderPending": "待支付",
+ "VipOrderSuccess": "支付成功"
+ },
+ "x-enum-varnames": [
+ "VipOrderPending",
+ "VipOrderSuccess",
+ "VipOrderFailed"
+ ]
+ },
+ "pubdef.VipType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "VipTypeMonth": "月卡",
+ "VipTypeNone": "没卡",
+ "VipTypeYear": "年卡"
+ },
+ "x-enum-varnames": [
+ "VipTypeNone",
+ "VipTypeMonth",
+ "VipTypeYear"
+ ]
+ },
+ "rechargepackagemod.PackageType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PackageTypeCoin": "金币充值",
+ "PackageTypeVip": "会员畅享"
+ },
+ "x-enum-varnames": [
+ "PackageTypeVip",
+ "PackageTypeCoin"
+ ]
+ },
+ "rechargepackagemod.Status": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "StatusDisabled": "停用",
+ "StatusEnabled": "启用"
+ },
+ "x-enum-varnames": [
+ "StatusEnabled",
+ "StatusDisabled"
+ ]
+ },
+ "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_cointrans.CoinTransReq": {
+ "type": "object",
+ "properties": {
+ "endTime": {
+ "description": "结束时间 2006-01-02 15:04:05",
+ "type": "string"
+ },
+ "flowDir": {
+ "description": "收支方向:1收入 -1支出",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.FlowDir"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "开始时间 2006-01-02 15:04:05",
+ "type": "string"
+ },
+ "tranType": {
+ "description": "交易类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.TranType"
+ }
+ ]
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_cointrans.CoinTransResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/cointransmod.CoinTrans"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "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": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "reviewStatus": {
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userId": {
+ "description": "评论用户ID",
+ "type": "integer"
+ },
+ "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": {
+ "countLike": {
+ "description": "评论点赞数",
+ "type": "integer"
+ },
+ "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": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "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"
+ },
+ "userAvatar": {
+ "description": "评论用户头像",
+ "type": "string"
+ },
+ "userId": {
+ "description": "评论用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "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_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_recharge.BillCallbackParam": {
+ "type": "object",
+ "properties": {
+ "oid": {
+ "type": "string"
+ },
+ "payMoney": {
+ "type": "integer"
+ },
+ "paymentAt": {
+ "type": "string"
+ },
+ "successAt": {
+ "type": "string"
+ },
+ "transNo": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageAddReq": {
+ "type": "object",
+ "required": [
+ "name",
+ "packageType",
+ "price"
+ ],
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数,会员套餐传0",
+ "type": "integer"
+ },
+ "durationDays": {
+ "description": "会员有效天数,金币套餐传0",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "后台备注",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序,越小越靠前",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用,默认1",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageItem": {
+ "type": "object",
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "durationDays": {
+ "description": "会员有效天数",
+ "type": "integer"
+ },
+ "durationHours": {
+ "description": "会员有效小时数,由durationDays计算",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "后台备注",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序,越小越靠前",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ },
+ "subTitle": {
+ "description": "展示副标题",
+ "type": "string"
+ },
+ "totalCoin": {
+ "description": "到账总金币数,由baseCoin+giftCoin计算",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageListReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题,模糊搜索",
+ "type": "string"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageItem"
+ }
+ },
+ "total": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageStatusSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "套餐ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数",
+ "type": "integer"
+ },
+ "durationDays": {
+ "description": "会员有效天数",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "后台备注",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.VipPackageOption": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "套餐名称",
+ "type": "string"
+ }
+ }
+ },
+ "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": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "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": {
+ "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.GiftCoinReq": {
+ "type": "object",
+ "required": [
+ "coin",
+ "userId"
+ ],
+ "properties": {
+ "coin": {
+ "description": "赠送金币数(正数=增加,负数=扣除)",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注(记录在流水表)",
+ "type": "string"
+ },
+ "userId": {
+ "description": "目标用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_user.UpdateVipReq": {
+ "type": "object",
+ "required": [
+ "packageId",
+ "userId"
+ ],
+ "properties": {
+ "packageId": {
+ "description": "会员畅享套餐ID",
+ "type": "integer"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "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"
+ }
+ ]
+ },
+ "type": {
+ "description": "用户类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ }
+ }
+ },
+ "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": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videocategorymod.VideoCategory"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "contentType": {
+ "description": "内容:0: 默认 1:首页 2:今日",
+ "type": "integer"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sortType": {
+ "description": "排序方式 0:上线时间 1:观看次数 2排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "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"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "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.SetFakeWatchTimesReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,支持单个或批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "watchTimesMax": {
+ "description": "假观看次数随机最大值",
+ "type": "integer"
+ },
+ "watchTimesMin": {
+ "description": "假观看次数随机最小值",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoReq": {
+ "type": "object",
+ "properties": {
+ "categoryId": {
+ "description": "分类Id",
+ "type": "integer"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "创建时间-截止",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "isH265": {
+ "description": "是否支持 H265",
+ "type": "boolean"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "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=播放次数",
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "创建时间-开始",
+ "type": "string"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.InformationSatus"
+ }
+ ]
+ },
+ "tag": {
+ "description": "标签",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "topicId": {
+ "description": "专题Id",
+ "type": "integer"
+ },
+ "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": {
+ "addedTime": {
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类Id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "content": {
+ "type": "string"
+ },
+ "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"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "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"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.InformationSatus"
+ }
+ ]
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "topicIds": {
+ "description": "专题ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "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_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.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"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "videoCategoryId": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videotopicmod.VideoTopicModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicUpdate": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "image": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "videoCategoryId": {
+ "description": "视频分类id",
+ "type": "integer"
+ },
+ "videoTopicType": {
+ "description": "视频类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videotopic.VideoTopicUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_viporder.VipOrderReq": {
+ "type": "object",
+ "properties": {
+ "endTime": {
+ "description": "创建时间-结束",
+ "type": "string"
+ },
+ "orderType": {
+ "description": "订单类型:1会员 2金币",
+ "allOf": [
+ {
+ "$ref": "#/definitions/vipordermod.OrderType"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "创建时间-开始",
+ "type": "string"
+ },
+ "status": {
+ "description": "订单状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipOrderStatus"
+ }
+ ]
+ },
+ "tradeNo": {
+ "description": "订单号",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_viporder.VipOrderResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/vipordermod.VipOrder"
+ }
+ },
+ "total": {
+ "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"
+ },
+ "deviceBrand": {
+ "description": "设备品牌",
+ "type": "string"
+ },
+ "deviceModel": {
+ "description": "设备型号",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "商区码",
+ "type": "string"
+ },
+ "email": {
+ "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"
+ },
+ "merchantExpiredAt": {
+ "description": "历史遗留字段",
+ "type": "string"
+ },
+ "modifyUserNameAt": {
+ "description": "上一次修改用户时间",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oriVer": {
+ "description": "注册时的app版本",
+ "type": "string"
+ },
+ "password": {
+ "description": "登陆密码",
+ "type": "string"
+ },
+ "privacyMode": {
+ "description": "隐私模式:true-开启,false-关闭(默认)",
+ "type": "boolean"
+ },
+ "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"
+ },
+ "sessionId": {
+ "description": "会话ID",
+ "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"
+ },
+ "systemName": {
+ "description": "系统名称",
+ "type": "string"
+ },
+ "systemVersion": {
+ "description": "系统版本",
+ "type": "string"
+ },
+ "type": {
+ "description": "类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "vipType": {
+ "description": "Vip类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipType"
+ }
+ ]
+ }
+ }
+ },
+ "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"
+ },
+ "deviceBrand": {
+ "description": "设备品牌",
+ "type": "string"
+ },
+ "deviceModel": {
+ "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"
+ },
+ "sessionId": {
+ "description": "会话ID",
+ "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"
+ },
+ "systemName": {
+ "description": "系统名称",
+ "type": "string"
+ },
+ "systemVersion": {
+ "description": "系统版本",
+ "type": "string"
+ },
+ "type": {
+ "description": "类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "vipType": {
+ "description": "Vip类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipType"
+ }
+ ]
+ }
+ }
+ },
+ "videocategorymod.VideoCategory": {
+ "type": "object",
+ "properties": {
+ "contentType": {
+ "description": "内容:0: 默认 1:首页 2:今日 3:合集",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "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"
+ }
+ ]
+ },
+ "sortType": {
+ "description": "排序方式 0:上线时间 1:观看次数 2排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videocreatermod.VideoCreaterInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "name": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videoinformationmod.FakeFlow": {
+ "type": "object",
+ "properties": {
+ "watchTimes": {
+ "description": "假观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.Flow": {
+ "type": "object",
+ "properties": {
+ "comments": {
+ "description": "评论条数",
+ "type": "integer"
+ },
+ "monthWatchs": {
+ "description": "本月观看次数",
+ "type": "integer"
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "todayWatchs": {
+ "description": "当日观看次数",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "观看次数",
+ "type": "integer"
+ },
+ "weekWatchs": {
+ "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.InformationSatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "InformationSatusVerifyCancel": "下架",
+ "InformationSatusVerifyOK": "上架",
+ "InformationSatusWaiting": "初始化"
+ },
+ "x-enum-varnames": [
+ "InformationSatusWaiting",
+ "InformationSatusVerifyOK",
+ "InformationSatusVerifyCancel"
+ ]
+ },
+ "videoinformationmod.MediaType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "MediaType_Advitise": "广告",
+ "MediaType_Collection": "合集",
+ "MediaType_Video": "视频(帖子)",
+ "MediaType_VideoV2": "视频"
+ },
+ "x-enum-varnames": [
+ "MediaType_Video",
+ "MediaType_Collection",
+ "MediaType_Advitise",
+ "MediaType_VideoV2"
+ ]
+ },
+ "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": "H265 视频地址",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "视频URl",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.VideoInformation": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "帖子上架时间",
+ "type": "string"
+ },
+ "audioFile": {
+ "description": "音频文件路径",
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类Id (帖子用)",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "content": {
+ "description": "富文本",
+ "type": "string"
+ },
+ "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/videoinformationmod.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"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "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/videoinformationmod.Flow"
+ }
+ ]
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secondUpdatedAt": {
+ "description": "二次修改时间",
+ "type": "string"
+ },
+ "smartCover": {
+ "description": "智能封面",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.InformationSatus"
+ }
+ ]
+ },
+ "syncId": {
+ "description": "同步ID",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "topicIds": {
+ "description": "专题ID(视频用)",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "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"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "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"
+ }
+ }
+ },
+ "videotopicmod.VideoTopicModel": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "image": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "name": {
+ "description": "专题名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "videoCategoryId": {
+ "description": "视频分类id",
+ "type": "integer"
+ },
+ "videoTopicType": {
+ "description": "视频类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "vipordermod.OrderType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "OrderTypeCoin": "金币购买",
+ "OrderTypeVip": "会员购买"
+ },
+ "x-enum-varnames": [
+ "OrderTypeVip",
+ "OrderTypeCoin"
+ ]
+ },
+ "vipordermod.VipOrder": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "支付金额(分)",
+ "type": "integer"
+ },
+ "cardId": {
+ "description": "── VIP 专用 ──",
+ "type": "integer"
+ },
+ "cardName": {
+ "description": "套餐名快照",
+ "type": "string"
+ },
+ "coin": {
+ "description": "到账金币数(快照)",
+ "type": "integer"
+ },
+ "coinCfgId": {
+ "description": "── 金币专用 ──",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "finishedAt": {
+ "description": "完成时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "唯一ID",
+ "type": "integer"
+ },
+ "ipAddr": {
+ "description": "客户端 IP",
+ "type": "string"
+ },
+ "oid": {
+ "description": "第三方订单号",
+ "type": "string"
+ },
+ "orderType": {
+ "description": "订单类型:1会员 2金币",
+ "allOf": [
+ {
+ "$ref": "#/definitions/vipordermod.OrderType"
+ }
+ ]
+ },
+ "payChannel": {
+ "description": "支付渠道",
+ "type": "string"
+ },
+ "payMode": {
+ "description": "支付方式:overage/alipay/wechat",
+ "type": "string"
+ },
+ "payRate": {
+ "description": "费率",
+ "type": "string"
+ },
+ "payUrl": {
+ "description": "支付链接",
+ "type": "string"
+ },
+ "payUrlMode": {
+ "description": "链接打开方式",
+ "type": "string"
+ },
+ "status": {
+ "description": "订单状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipOrderStatus"
+ }
+ ]
+ },
+ "sysType": {
+ "description": "ios/android",
+ "type": "string"
+ },
+ "tradeNo": {
+ "description": "本地订单号",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "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": {
+ "actressBackGround": {
+ "description": "女优通用背景图",
+ "type": "string"
+ },
+ "advitiesTopicNum": {
+ "description": "广告间隔",
+ "type": "integer"
+ },
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "customerServiceHost": {
+ "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"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "imgAppCdn": {
+ "description": "图片CDN(app使用)",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgWebCdn": {
+ "description": "图片CDN(web使用)",
+ "type": "string"
+ },
+ "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"
+ }
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "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": {
+ "actressBackGround": {
+ "description": "女优通用背景图",
+ "type": "string"
+ },
+ "advitiesTopicNum": {
+ "description": "广告间隔",
+ "type": "integer"
+ },
+ "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"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "freeVPN": {
+ "description": "免费vpn链接",
+ "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"
+ },
+ "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"
+ }
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "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"
+ }
+ }
+ }
+ },
+ "webanalytics.AnalyticsDailyStat": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当月数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ }
+ }
+ },
+ "webanalytics.AnalyticsStat": {
+ "type": "object",
+ "properties": {
+ "todayList": {
+ "description": "当日数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ },
+ "weekList": {
+ "description": "环比数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ },
+ "yestodayList": {
+ "description": "同比数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ }
+ }
+ },
+ "webanalytics.LastWebStatInfo": {
+ "type": "object",
+ "properties": {
+ "activeUsers": {
+ "description": "最近半小时活跃数",
+ "type": "integer"
+ },
+ "appId": {
+ "description": "appId",
+ "type": "integer"
+ },
+ "appName": {
+ "description": "APP名称",
+ "type": "string"
+ },
+ "leadAmount": {
+ "description": "日导量充值",
+ "type": "number"
+ },
+ "leadNewCount": {
+ "description": "日导量新增",
+ "type": "number"
+ },
+ "sumDate": {
+ "description": "时间半小时",
+ "type": "string"
+ },
+ "yestodayActiveUsers": {
+ "description": "活跃用户数",
+ "type": "integer"
+ },
+ "yestodayNewUsers": {
+ "description": "新增数量",
+ "type": "integer"
+ },
+ "yestodayPV": {
+ "description": "页面浏览量 (PV)",
+ "type": "integer"
+ },
+ "yestodaySessions": {
+ "description": "会话数",
+ "type": "integer"
+ },
+ "yestodayUV": {
+ "description": "独立用户数 (UV)",
+ "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: "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..5938715
--- /dev/null
+++ b/web/docs/swagger.json
@@ -0,0 +1,11317 @@
+{
+ "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/card/order/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询充值订单,支持按 OrderType 筛选:1会员 2金币;不传 orderType 则返回全部类型",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "会员卡管理"
+ ],
+ "summary": "查询充值订单列表",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_viporder.VipOrderReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_viporder.VipOrderResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/coin/trans": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询金币收支流水,支持按用户、收支方向、交易类型、时间范围筛选",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "金币管理"
+ ],
+ "summary": "查询金币交易记录",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_cointrans.CoinTransReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_cointrans.CoinTransResp"
+ }
+ },
+ "400": {
+ "description": "参数错误",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/handle": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "批量更新联系我们处理状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web联系我们相关接口"
+ ],
+ "summary": "web端处理联系我们 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "web端联系我们处理请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsHandleReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.ContactUsHandleResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/contactus/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询联系我们列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web联系我们相关接口"
+ ],
+ "summary": "web端联系我们列表 -- by 旺鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "创建结束时间,格式:2026-06-09 00:00:00",
+ "name": "createEndTime",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "创建开始时间,格式:2026-06-08 00:00:00",
+ "name": "createStartTime",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "邮箱",
+ "name": "email",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "处理状态,0-待处理、1-已处理",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "用户ID",
+ "name": "userId",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.ContactUsListPageResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.ContactUsListResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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/feedback/handle": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "批量更新意见反馈处理状态",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web意见反馈相关接口"
+ ],
+ "summary": "web端处理意见反馈 -- by 旺鸣",
+ "parameters": [
+ {
+ "description": "web端意见反馈处理请求",
+ "name": "params",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackHandleReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.FeedbackHandleResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/feedback/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询用户意见反馈列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web意见反馈相关接口"
+ ],
+ "summary": "web端意见反馈列表 -- by 旺鸣",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "创建结束时间,格式:2026-06-09 00:00:00",
+ "name": "createEndTime",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "创建开始时间,格式:2026-06-08 00:00:00",
+ "name": "createStartTime",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "处理状态,0-待处理、1-已处理",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "用户ID",
+ "name": "userId",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/proto.FeedbackListPageResp"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/proto.FeedbackListResp"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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/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": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增短剧",
+ "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/chapter/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端新增章节",
+ "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/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端章节列表",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "playletId",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone"
+ ],
+ "name": "watermarkStatus",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletChapterListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/chapter/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据章节ID集合删除 playlet_chapter 记录,传1个ID表示单条删除,传多个ID表示批量删除;同时清理该剧集的点赞、收藏、解锁记录",
+ "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/pay-type/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据章节ID集合批量更新 playlet_chapter 集合 payType 字段,传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/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新章节",
+ "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/list": {
+ "get": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "分页查询短剧列表",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端短剧列表",
+ "parameters": [
+ {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "ContentTypeAlbum": "影集",
+ "ContentTypeTheater": "短剧"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum"
+ ],
+ "name": "contentType",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "description": "是否爆剧(true-是,false-否)",
+ "name": "isFeatured",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "name": "isRecommend",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageNum",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "pageSize",
+ "in": "query"
+ },
+ {
+ "type": "boolean",
+ "name": "showOnBanner",
+ "in": "query"
+ },
+ {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ],
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "name": "themeId",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "name": "title",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/proto.PlayletListPageResp"
+ }
+ },
+ "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端新增标签",
+ "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端删除标签",
+ "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端标签列表",
+ "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表示批量设置;tagIds 可设置多个标签,传空数组表示清空标签",
+ "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端更新标签",
+ "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端新增题材",
+ "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端删除题材",
+ "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端题材列表",
+ "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.PlayletThemeListPageResp"
+ }
+ },
+ "400": {
+ "description": "失败",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/playlet/theme/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置;themeIds 可设置多个题材",
+ "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端更新题材",
+ "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": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "web短剧相关接口"
+ ],
+ "summary": "web端更新短剧",
+ "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/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/recharge/package/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "新增购买套餐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "新增参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageAddReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/del": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "删除购买套餐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "删除参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "查询购买套餐列表 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "查询参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageListReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageListResp"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/status/set": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "单/批量设置购买套餐状态 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "设置状态参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageStatusSetReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "更新购买套餐 -- by 王鸣",
+ "parameters": [
+ {
+ "description": "更新参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/recharge/package/vip/list": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "只返回 PackageType=1 的启用套餐,字段仅包含 id 和 name,不分页",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "套餐管理"
+ ],
+ "summary": "查询会员畅享套餐选项 -- by 王鸣",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_rechargepackage.VipPackageOption"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/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": "媒体类型 4-短剧 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/stat/analytics": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "统计数据",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "获取统计数据"
+ ],
+ "summary": "统计数据",
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/webanalytics.AnalyticsStat"
+ }
+ },
+ "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/webanalytics.AnalyticsDailyStat"
+ }
+ },
+ "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/third/paycenter/bill/callback": {
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "支付回调"
+ ],
+ "summary": "嘉华支付回调通知(内部接口,无需鉴权)",
+ "parameters": [
+ {
+ "description": "回调参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_recharge.BillCallbackParam"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "success",
+ "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/giftcoin": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "coin 为正数表示赠送,为负数表示扣除;操作会自动记录金币流水",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "赠送/扣除用户金币",
+ "parameters": [
+ {
+ "description": "赠送参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.GiftCoinReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "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/user/vip/update": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据会员畅享套餐ID修改用户会员信息,不处理赠送金币",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "用户管理"
+ ],
+ "summary": "修改用户会员",
+ "parameters": [
+ {
+ "description": "修改会员参数",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/srv_user.UpdateVipReq"
+ }
+ }
+ ],
+ "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.VideoCategoryUpdateReq"
+ }
+ }
+ ],
+ "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.VideoCreaterUpdateReq"
+ }
+ }
+ ],
+ "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/setFakeWatchTimes": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "根据ids批量随机设置视频假观看次数",
+ "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/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.VideoTagInfoUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/add": {
+ "post": {
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "description": "添加一个视频专题",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "视频专题"
+ ],
+ "summary": "添加一个视频专题",
+ "parameters": [
+ {
+ "description": "参数列表",
+ "name": "param",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/videotopicmod.VideoTopicModel"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/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_videotopic.VideoTopicDelReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/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_videotopic.VideoTopicReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "$ref": "#/definitions/srv_videotopic.VideoTopicResp"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/api/web/videotopic/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_videotopic.VideoTopicUpdateReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "获取失败的返回结果",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/wbapi/69cgw/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"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "cointransmod.CoinTrans": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "本次交易金币数(正整数)",
+ "type": "integer"
+ },
+ "balanceAfter": {
+ "description": "交易后余额",
+ "type": "integer"
+ },
+ "balanceBefore": {
+ "description": "交易前余额",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "交易时间",
+ "type": "string"
+ },
+ "flowDir": {
+ "description": "收支方向:1收入 -1支出",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.FlowDir"
+ }
+ ]
+ },
+ "id": {
+ "description": "唯一ID",
+ "type": "integer"
+ },
+ "orderNo": {
+ "description": "关联订单号(可为空)",
+ "type": "string"
+ },
+ "relatedId": {
+ "description": "关联内容ID(视频等,可为空)",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注描述",
+ "type": "string"
+ },
+ "tranType": {
+ "description": "交易类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.TranType"
+ }
+ ]
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户昵称(快照)",
+ "type": "string"
+ }
+ }
+ },
+ "cointransmod.FlowDir": {
+ "type": "integer",
+ "enum": [
+ 1,
+ -1
+ ],
+ "x-enum-comments": {
+ "FlowIn": "收入(充值、赠送)",
+ "FlowOut": "支出(消费、解锁)"
+ },
+ "x-enum-varnames": [
+ "FlowIn",
+ "FlowOut"
+ ]
+ },
+ "cointransmod.TranType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "x-enum-comments": {
+ "TranTypeAdminAdjust": "管理员手动调整",
+ "TranTypeCoinRecharge": "金币充值(购买金币)",
+ "TranTypeDarkAlley": "历史遗留交易类型(已下线)",
+ "TranTypeVideoUnlock": "视频解锁",
+ "TranTypeVipGiftCoin": "购买会员赠送金币"
+ },
+ "x-enum-varnames": [
+ "TranTypeCoinRecharge",
+ "TranTypeVipGiftCoin",
+ "TranTypeDarkAlley",
+ "TranTypeVideoUnlock",
+ "TranTypeAdminAdjust"
+ ]
+ },
+ "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"
+ }
+ }
+ },
+ "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
+ ],
+ "x-enum-comments": {
+ "WatermarkStatusDone": "已完成",
+ "WatermarkStatusPending": "未处理(待 mediafile/sync)",
+ "WatermarkStatusProcessing": "处理中(已提交加水印任务,待回写)",
+ "WatermarkStatusRegistered": "已登记(原片已登记,待 task/add)"
+ },
+ "x-enum-varnames": [
+ "WatermarkStatusPending",
+ "WatermarkStatusRegistered",
+ "WatermarkStatusProcessing",
+ "WatermarkStatusDone"
+ ]
+ },
+ "playletmod.ContentType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "ContentTypeAlbum": "影集",
+ "ContentTypeTheater": "短剧"
+ },
+ "x-enum-varnames": [
+ "ContentTypeTheater",
+ "ContentTypeAlbum"
+ ]
+ },
+ "playletmod.Status": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "x-enum-comments": {
+ "StatusDown": "下架",
+ "StatusUp": "上架"
+ },
+ "x-enum-varnames": [
+ "StatusDown",
+ "StatusUp"
+ ]
+ },
+ "proto.ContactUsHandleReq": {
+ "type": "object",
+ "required": [
+ "ids",
+ "status"
+ ],
+ "properties": {
+ "ids": {
+ "description": "联系我们ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsHandleResp": {
+ "type": "object",
+ "properties": {
+ "matchedCount": {
+ "description": "命中数量",
+ "type": "integer"
+ },
+ "modifiedCount": {
+ "description": "修改数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.ContactUsListResp": {
+ "type": "object",
+ "properties": {
+ "attachments": {
+ "description": "附件地址",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "content": {
+ "description": "内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "email": {
+ "description": "邮箱",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "type": {
+ "description": "问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.FeedbackHandleReq": {
+ "type": "object",
+ "required": [
+ "ids",
+ "status"
+ ],
+ "properties": {
+ "ids": {
+ "description": "意见反馈ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackHandleResp": {
+ "type": "object",
+ "properties": {
+ "matchedCount": {
+ "description": "命中数量",
+ "type": "integer"
+ },
+ "modifiedCount": {
+ "description": "修改数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.FeedbackListResp": {
+ "type": "object",
+ "properties": {
+ "contact": {
+ "description": "联系方式",
+ "type": "string"
+ },
+ "content": {
+ "description": "描述内容",
+ "type": "string"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "处理备注",
+ "type": "string"
+ },
+ "screenshots": {
+ "description": "问题截图",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "status": {
+ "description": "处理状态,0-待处理、1-已处理",
+ "type": "integer"
+ },
+ "type": {
+ "description": "问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "proto.PlayletAddReq": {
+ "type": "object",
+ "required": [
+ "title"
+ ],
+ "properties": {
+ "anchor": {
+ "type": "string"
+ },
+ "banner": {
+ "type": "string"
+ },
+ "contentType": {
+ "$ref": "#/definitions/playletmod.ContentType"
+ },
+ "cover": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "type": "boolean"
+ },
+ "preview": {
+ "type": "string"
+ },
+ "recommendRank": {
+ "type": "integer"
+ },
+ "showOnBanner": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/playletmod.Status"
+ },
+ "tagIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "type": "string"
+ },
+ "topRank": {
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletAddResp": {
+ "type": "object",
+ "properties": {
+ "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.PlayletChapterAddReq": {
+ "type": "object",
+ "required": [
+ "currentEpisode",
+ "playletId"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "type": "integer"
+ },
+ "hash": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "md5": {
+ "type": "string"
+ },
+ "mediaSize": {
+ "type": "integer"
+ },
+ "mediaTime": {
+ "type": "integer"
+ },
+ "payType": {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ },
+ "playletId": {
+ "type": "integer"
+ },
+ "ratio": {
+ "type": "number"
+ },
+ "singleCoins": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "videoUrl": {
+ "type": "string"
+ },
+ "width": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterListPageResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "集合列表"
+ },
+ "pageNum": {
+ "description": "页数",
+ "type": "integer"
+ },
+ "pageSize": {
+ "description": "每页数",
+ "type": "integer"
+ },
+ "totalCount": {
+ "description": "总共数量",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterDelReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "章节ID集合,传1个表示单条删除,传多个表示批量删除",
+ "type": "array",
+ "items": {
+ "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-金币时必填且需\u003e0)",
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletChapterUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "currentEpisode": {
+ "type": "integer"
+ },
+ "hash": {
+ "type": "string"
+ },
+ "height": {
+ "type": "integer"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "md5": {
+ "type": "string"
+ },
+ "mediaSize": {
+ "type": "integer"
+ },
+ "mediaTime": {
+ "type": "integer"
+ },
+ "payType": {
+ "$ref": "#/definitions/playletchaptermod.PayType"
+ },
+ "ratio": {
+ "type": "number"
+ },
+ "singleCoins": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "videoUrl": {
+ "type": "string"
+ },
+ "width": {
+ "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.PlayletRecommendSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "短剧ID集合,传1个表示单条设置,传多个表示批量设置",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "isRecommend": {
+ "description": "是否推荐(true-推荐,false-不推荐)",
+ "type": "boolean"
+ }
+ }
+ },
+ "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集合-多个用逗号隔开,请求示例[28830,28829,28828,28827,28826,28825]",
+ "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"
+ },
+ "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"
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddReq": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "icon": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletThemeAddResp": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "proto.PlayletThemeDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "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": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "status": {
+ "type": "boolean"
+ }
+ }
+ },
+ "proto.PlayletUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "anchor": {
+ "type": "string"
+ },
+ "banner": {
+ "type": "string"
+ },
+ "contentType": {
+ "$ref": "#/definitions/playletmod.ContentType"
+ },
+ "cover": {
+ "type": "string"
+ },
+ "desc": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "isFeatured": {
+ "description": "是否爆剧(true-是,false-否)",
+ "type": "boolean"
+ },
+ "isRecommend": {
+ "type": "boolean"
+ },
+ "preview": {
+ "type": "string"
+ },
+ "recommendRank": {
+ "type": "integer"
+ },
+ "showOnBanner": {
+ "type": "boolean"
+ },
+ "status": {
+ "$ref": "#/definitions/playletmod.Status"
+ },
+ "tagIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "themeIds": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "type": "string"
+ },
+ "topRank": {
+ "type": "integer"
+ },
+ "totalEpisode": {
+ "type": "integer"
+ }
+ }
+ },
+ "pubdef.LoginType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "LoginTypeAccount": "账号登陆",
+ "LoginTypeDevice": "设备登陆",
+ "LoginTypeEmail": "邮箱登陆"
+ },
+ "x-enum-varnames": [
+ "LoginTypeDevice",
+ "LoginTypeAccount",
+ "LoginTypeEmail"
+ ]
+ },
+ "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"
+ ]
+ },
+ "pubdef.UserType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 0
+ ],
+ "x-enum-comments": {
+ "UserTypeGuest": "游客用户",
+ "UserTypeNormal": "普通用户(已注册)"
+ },
+ "x-enum-varnames": [
+ "UserTypeGuest",
+ "UserTypeNormal",
+ "UserTypeNone"
+ ]
+ },
+ "pubdef.VipOrderStatus": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "VipOrderFailed": "支付失败",
+ "VipOrderPending": "待支付",
+ "VipOrderSuccess": "支付成功"
+ },
+ "x-enum-varnames": [
+ "VipOrderPending",
+ "VipOrderSuccess",
+ "VipOrderFailed"
+ ]
+ },
+ "pubdef.VipType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "VipTypeMonth": "月卡",
+ "VipTypeNone": "没卡",
+ "VipTypeYear": "年卡"
+ },
+ "x-enum-varnames": [
+ "VipTypeNone",
+ "VipTypeMonth",
+ "VipTypeYear"
+ ]
+ },
+ "rechargepackagemod.PackageType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "PackageTypeCoin": "金币充值",
+ "PackageTypeVip": "会员畅享"
+ },
+ "x-enum-varnames": [
+ "PackageTypeVip",
+ "PackageTypeCoin"
+ ]
+ },
+ "rechargepackagemod.Status": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "StatusDisabled": "停用",
+ "StatusEnabled": "启用"
+ },
+ "x-enum-varnames": [
+ "StatusEnabled",
+ "StatusDisabled"
+ ]
+ },
+ "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_cointrans.CoinTransReq": {
+ "type": "object",
+ "properties": {
+ "endTime": {
+ "description": "结束时间 2006-01-02 15:04:05",
+ "type": "string"
+ },
+ "flowDir": {
+ "description": "收支方向:1收入 -1支出",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.FlowDir"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "开始时间 2006-01-02 15:04:05",
+ "type": "string"
+ },
+ "tranType": {
+ "description": "交易类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/cointransmod.TranType"
+ }
+ ]
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_cointrans.CoinTransResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/cointransmod.CoinTrans"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "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": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "reviewStatus": {
+ "type": "integer"
+ },
+ "text": {
+ "description": "评论内容",
+ "type": "string"
+ },
+ "userId": {
+ "description": "评论用户ID",
+ "type": "integer"
+ },
+ "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": {
+ "countLike": {
+ "description": "评论点赞数",
+ "type": "integer"
+ },
+ "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": "对象类型 10-短剧 11-短剧剧集",
+ "type": "integer"
+ },
+ "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"
+ },
+ "userAvatar": {
+ "description": "评论用户头像",
+ "type": "string"
+ },
+ "userId": {
+ "description": "评论用户ID",
+ "type": "integer"
+ },
+ "userName": {
+ "description": "用户名称",
+ "type": "string"
+ }
+ }
+ },
+ "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_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_recharge.BillCallbackParam": {
+ "type": "object",
+ "properties": {
+ "oid": {
+ "type": "string"
+ },
+ "payMoney": {
+ "type": "integer"
+ },
+ "paymentAt": {
+ "type": "string"
+ },
+ "successAt": {
+ "type": "string"
+ },
+ "transNo": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageAddReq": {
+ "type": "object",
+ "required": [
+ "name",
+ "packageType",
+ "price"
+ ],
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数,会员套餐传0",
+ "type": "integer"
+ },
+ "durationDays": {
+ "description": "会员有效天数,金币套餐传0",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "后台备注",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序,越小越靠前",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用,默认1",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageItem": {
+ "type": "object",
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "durationDays": {
+ "description": "会员有效天数",
+ "type": "integer"
+ },
+ "durationHours": {
+ "description": "会员有效小时数,由durationDays计算",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "后台备注",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序,越小越靠前",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ },
+ "subTitle": {
+ "description": "展示副标题",
+ "type": "string"
+ },
+ "totalCoin": {
+ "description": "到账总金币数,由baseCoin+giftCoin计算",
+ "type": "integer"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageListReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题,模糊搜索",
+ "type": "string"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageListResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当前页列表",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/srv_rechargepackage.RechargePackageItem"
+ }
+ },
+ "total": {
+ "description": "总数量",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageStatusSetReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "套餐ID集合",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.RechargePackageUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "baseCoin": {
+ "description": "基础金币数",
+ "type": "integer"
+ },
+ "durationDays": {
+ "description": "会员有效天数",
+ "type": "integer"
+ },
+ "giftCoin": {
+ "description": "赠送金币数",
+ "type": "integer"
+ },
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "展示标题",
+ "type": "string"
+ },
+ "originalPrice": {
+ "description": "原价,单位分",
+ "type": "integer"
+ },
+ "packageType": {
+ "description": "套餐类型:1-会员畅享 2-金币充值",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.PackageType"
+ }
+ ]
+ },
+ "price": {
+ "description": "售价,单位分",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "后台备注",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "状态:1-启用 2-停用",
+ "allOf": [
+ {
+ "$ref": "#/definitions/rechargepackagemod.Status"
+ }
+ ]
+ }
+ }
+ },
+ "srv_rechargepackage.VipPackageOption": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "套餐ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "套餐名称",
+ "type": "string"
+ }
+ }
+ },
+ "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": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "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": {
+ "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.GiftCoinReq": {
+ "type": "object",
+ "required": [
+ "coin",
+ "userId"
+ ],
+ "properties": {
+ "coin": {
+ "description": "赠送金币数(正数=增加,负数=扣除)",
+ "type": "integer"
+ },
+ "remark": {
+ "description": "备注(记录在流水表)",
+ "type": "string"
+ },
+ "userId": {
+ "description": "目标用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_user.UpdateVipReq": {
+ "type": "object",
+ "required": [
+ "packageId",
+ "userId"
+ ],
+ "properties": {
+ "packageId": {
+ "description": "会员畅享套餐ID",
+ "type": "integer"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "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"
+ }
+ ]
+ },
+ "type": {
+ "description": "用户类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ }
+ }
+ },
+ "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": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videocategorymod.VideoCategory"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryUpdate": {
+ "type": "object",
+ "properties": {
+ "appId": {
+ "description": "appId,唯一id",
+ "type": "integer"
+ },
+ "contentType": {
+ "description": "内容:0: 默认 1:首页 2:今日",
+ "type": "integer"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sortType": {
+ "description": "排序方式 0:上线时间 1:观看次数 2排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videocategory.VideoCategoryUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videocategory.VideoCategoryUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterReq": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "分类ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "分类名称",
+ "type": "string"
+ },
+ "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"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videocreater.VideoCreaterUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videocreater.VideoCreaterUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "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.SetFakeWatchTimesReq": {
+ "type": "object",
+ "properties": {
+ "ids": {
+ "description": "视频ID集合,支持单个或批量",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "watchTimesMax": {
+ "description": "假观看次数随机最大值",
+ "type": "integer"
+ },
+ "watchTimesMin": {
+ "description": "假观看次数随机最小值",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videoinfo.VideoInfoReq": {
+ "type": "object",
+ "properties": {
+ "categoryId": {
+ "description": "分类Id",
+ "type": "integer"
+ },
+ "creatorId": {
+ "description": "作者Id",
+ "type": "integer"
+ },
+ "creatorName": {
+ "description": "作者名称",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "创建时间-截止",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "isH265": {
+ "description": "是否支持 H265",
+ "type": "boolean"
+ },
+ "isHot": {
+ "description": "是否热门",
+ "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=播放次数",
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "创建时间-开始",
+ "type": "string"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.InformationSatus"
+ }
+ ]
+ },
+ "tag": {
+ "description": "标签",
+ "type": "integer"
+ },
+ "title": {
+ "description": "标题",
+ "type": "string"
+ },
+ "topicId": {
+ "description": "专题Id",
+ "type": "integer"
+ },
+ "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": {
+ "addedTime": {
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类Id",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "content": {
+ "type": "string"
+ },
+ "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"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "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"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.InformationSatus"
+ }
+ ]
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "topicIds": {
+ "description": "专题ID",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "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_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.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"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ }
+ }
+ },
+ "srv_videotag.VideoTagInfoUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videotag.VideoTagInfoUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicDelReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicReq": {
+ "type": "object",
+ "properties": {
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "videoCategoryId": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/videotopicmod.VideoTopicModel"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicUpdate": {
+ "type": "object",
+ "properties": {
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "image": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "type": "integer"
+ },
+ "isReIndexNow": {
+ "description": "是否重新提交索引",
+ "type": "boolean"
+ },
+ "name": {
+ "description": "名称",
+ "type": "string"
+ },
+ "rank": {
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "videoCategoryId": {
+ "description": "视频分类id",
+ "type": "integer"
+ },
+ "videoTopicType": {
+ "description": "视频类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ }
+ }
+ },
+ "srv_videotopic.VideoTopicUpdateReq": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "param": {
+ "description": "appId,唯一id",
+ "allOf": [
+ {
+ "$ref": "#/definitions/srv_videotopic.VideoTopicUpdate"
+ }
+ ]
+ }
+ }
+ },
+ "srv_viporder.VipOrderReq": {
+ "type": "object",
+ "properties": {
+ "endTime": {
+ "description": "创建时间-结束",
+ "type": "string"
+ },
+ "orderType": {
+ "description": "订单类型:1会员 2金币",
+ "allOf": [
+ {
+ "$ref": "#/definitions/vipordermod.OrderType"
+ }
+ ]
+ },
+ "pageNum": {
+ "type": "integer"
+ },
+ "pageSize": {
+ "type": "integer"
+ },
+ "startTime": {
+ "description": "创建时间-开始",
+ "type": "string"
+ },
+ "status": {
+ "description": "订单状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipOrderStatus"
+ }
+ ]
+ },
+ "tradeNo": {
+ "description": "订单号",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "type": "integer"
+ }
+ }
+ },
+ "srv_viporder.VipOrderResp": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/vipordermod.VipOrder"
+ }
+ },
+ "total": {
+ "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"
+ },
+ "deviceBrand": {
+ "description": "设备品牌",
+ "type": "string"
+ },
+ "deviceModel": {
+ "description": "设备型号",
+ "type": "string"
+ },
+ "districtCode": {
+ "description": "商区码",
+ "type": "string"
+ },
+ "email": {
+ "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"
+ },
+ "merchantExpiredAt": {
+ "description": "历史遗留字段",
+ "type": "string"
+ },
+ "modifyUserNameAt": {
+ "description": "上一次修改用户时间",
+ "type": "string"
+ },
+ "nickName": {
+ "description": "昵称",
+ "type": "string"
+ },
+ "oriVer": {
+ "description": "注册时的app版本",
+ "type": "string"
+ },
+ "password": {
+ "description": "登陆密码",
+ "type": "string"
+ },
+ "privacyMode": {
+ "description": "隐私模式:true-开启,false-关闭(默认)",
+ "type": "boolean"
+ },
+ "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"
+ },
+ "sessionId": {
+ "description": "会话ID",
+ "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"
+ },
+ "systemName": {
+ "description": "系统名称",
+ "type": "string"
+ },
+ "systemVersion": {
+ "description": "系统版本",
+ "type": "string"
+ },
+ "type": {
+ "description": "类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "vipType": {
+ "description": "Vip类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipType"
+ }
+ ]
+ }
+ }
+ },
+ "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"
+ },
+ "deviceBrand": {
+ "description": "设备品牌",
+ "type": "string"
+ },
+ "deviceModel": {
+ "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"
+ },
+ "sessionId": {
+ "description": "会话ID",
+ "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"
+ },
+ "systemName": {
+ "description": "系统名称",
+ "type": "string"
+ },
+ "systemVersion": {
+ "description": "系统版本",
+ "type": "string"
+ },
+ "type": {
+ "description": "类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.UserType"
+ }
+ ]
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "vipType": {
+ "description": "Vip类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipType"
+ }
+ ]
+ }
+ }
+ },
+ "videocategorymod.VideoCategory": {
+ "type": "object",
+ "properties": {
+ "contentType": {
+ "description": "内容:0: 默认 1:首页 2:今日 3:合集",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "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"
+ }
+ ]
+ },
+ "sortType": {
+ "description": "排序方式 0:上线时间 1:观看次数 2排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videocreatermod.VideoCreaterInfo": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "name": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ }
+ }
+ },
+ "videoinformationmod.FakeFlow": {
+ "type": "object",
+ "properties": {
+ "watchTimes": {
+ "description": "假观看次数",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.Flow": {
+ "type": "object",
+ "properties": {
+ "comments": {
+ "description": "评论条数",
+ "type": "integer"
+ },
+ "monthWatchs": {
+ "description": "本月观看次数",
+ "type": "integer"
+ },
+ "showTimes": {
+ "description": "展示次数",
+ "type": "integer"
+ },
+ "todayWatchs": {
+ "description": "当日观看次数",
+ "type": "integer"
+ },
+ "watchTimes": {
+ "description": "观看次数",
+ "type": "integer"
+ },
+ "weekWatchs": {
+ "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.InformationSatus": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "InformationSatusVerifyCancel": "下架",
+ "InformationSatusVerifyOK": "上架",
+ "InformationSatusWaiting": "初始化"
+ },
+ "x-enum-varnames": [
+ "InformationSatusWaiting",
+ "InformationSatusVerifyOK",
+ "InformationSatusVerifyCancel"
+ ]
+ },
+ "videoinformationmod.MediaType": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "x-enum-comments": {
+ "MediaType_Advitise": "广告",
+ "MediaType_Collection": "合集",
+ "MediaType_Video": "视频(帖子)",
+ "MediaType_VideoV2": "视频"
+ },
+ "x-enum-varnames": [
+ "MediaType_Video",
+ "MediaType_Collection",
+ "MediaType_Advitise",
+ "MediaType_VideoV2"
+ ]
+ },
+ "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": "H265 视频地址",
+ "type": "string"
+ },
+ "height": {
+ "description": "分辨率高(视频才会存在)",
+ "type": "integer"
+ },
+ "preMp4FileNameFs": {
+ "description": "预览MP4文件",
+ "type": "string"
+ },
+ "taskId": {
+ "description": "视频ID(视频才会存在)",
+ "type": "string"
+ },
+ "unencryptedPreMp4": {
+ "description": "不加密的预览MP4",
+ "type": "string"
+ },
+ "url": {
+ "description": "视频URl",
+ "type": "string"
+ },
+ "videoTime": {
+ "description": "影片长度",
+ "type": "integer"
+ },
+ "width": {
+ "description": "分辨率宽(视频才会存在)",
+ "type": "integer"
+ }
+ }
+ },
+ "videoinformationmod.VideoInformation": {
+ "type": "object",
+ "properties": {
+ "addedTime": {
+ "description": "帖子上架时间",
+ "type": "string"
+ },
+ "audioFile": {
+ "description": "音频文件路径",
+ "type": "string"
+ },
+ "categoryIds": {
+ "description": "分类Id (帖子用)",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "content": {
+ "description": "富文本",
+ "type": "string"
+ },
+ "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/videoinformationmod.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"
+ },
+ "isTop": {
+ "description": "是否置顶",
+ "type": "boolean"
+ },
+ "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/videoinformationmod.Flow"
+ }
+ ]
+ },
+ "remark": {
+ "description": "备注",
+ "type": "string"
+ },
+ "secondTags": {
+ "description": "二级标签标签",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secondUpdatedAt": {
+ "description": "二次修改时间",
+ "type": "string"
+ },
+ "smartCover": {
+ "description": "智能封面",
+ "type": "string"
+ },
+ "sort": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "status": {
+ "description": "帖子上架状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.InformationSatus"
+ }
+ ]
+ },
+ "syncId": {
+ "description": "同步ID",
+ "type": "string"
+ },
+ "tags": {
+ "description": "标签",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "title": {
+ "description": "帖子标题",
+ "type": "string"
+ },
+ "topicIds": {
+ "description": "专题ID(视频用)",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "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"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "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"
+ }
+ }
+ },
+ "videotopicmod.VideoTopicModel": {
+ "type": "object",
+ "properties": {
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "desc": {
+ "description": "描述",
+ "type": "string"
+ },
+ "id": {
+ "description": "Id",
+ "type": "integer"
+ },
+ "image": {
+ "description": "背景",
+ "type": "string"
+ },
+ "indexNow": {
+ "description": "是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改",
+ "type": "integer"
+ },
+ "name": {
+ "description": "专题名称",
+ "type": "string"
+ },
+ "rank": {
+ "description": "排序",
+ "type": "integer"
+ },
+ "seoInfo": {
+ "description": "Seo相关信息",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.SeoValue"
+ }
+ ]
+ },
+ "status": {
+ "description": "上架状态",
+ "type": "boolean"
+ },
+ "title": {
+ "description": "APP显示",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "videoCategoryId": {
+ "description": "视频分类id",
+ "type": "integer"
+ },
+ "videoTopicType": {
+ "description": "视频类型",
+ "allOf": [
+ {
+ "$ref": "#/definitions/videoinformationmod.MediaType"
+ }
+ ]
+ },
+ "works": {
+ "description": "作品数",
+ "type": "integer"
+ }
+ }
+ },
+ "vipordermod.OrderType": {
+ "type": "integer",
+ "enum": [
+ 1,
+ 2
+ ],
+ "x-enum-comments": {
+ "OrderTypeCoin": "金币购买",
+ "OrderTypeVip": "会员购买"
+ },
+ "x-enum-varnames": [
+ "OrderTypeVip",
+ "OrderTypeCoin"
+ ]
+ },
+ "vipordermod.VipOrder": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "description": "支付金额(分)",
+ "type": "integer"
+ },
+ "cardId": {
+ "description": "── VIP 专用 ──",
+ "type": "integer"
+ },
+ "cardName": {
+ "description": "套餐名快照",
+ "type": "string"
+ },
+ "coin": {
+ "description": "到账金币数(快照)",
+ "type": "integer"
+ },
+ "coinCfgId": {
+ "description": "── 金币专用 ──",
+ "type": "integer"
+ },
+ "createdAt": {
+ "description": "创建时间",
+ "type": "string"
+ },
+ "finishedAt": {
+ "description": "完成时间",
+ "type": "string"
+ },
+ "id": {
+ "description": "唯一ID",
+ "type": "integer"
+ },
+ "ipAddr": {
+ "description": "客户端 IP",
+ "type": "string"
+ },
+ "oid": {
+ "description": "第三方订单号",
+ "type": "string"
+ },
+ "orderType": {
+ "description": "订单类型:1会员 2金币",
+ "allOf": [
+ {
+ "$ref": "#/definitions/vipordermod.OrderType"
+ }
+ ]
+ },
+ "payChannel": {
+ "description": "支付渠道",
+ "type": "string"
+ },
+ "payMode": {
+ "description": "支付方式:overage/alipay/wechat",
+ "type": "string"
+ },
+ "payRate": {
+ "description": "费率",
+ "type": "string"
+ },
+ "payUrl": {
+ "description": "支付链接",
+ "type": "string"
+ },
+ "payUrlMode": {
+ "description": "链接打开方式",
+ "type": "string"
+ },
+ "status": {
+ "description": "订单状态",
+ "allOf": [
+ {
+ "$ref": "#/definitions/pubdef.VipOrderStatus"
+ }
+ ]
+ },
+ "sysType": {
+ "description": "ios/android",
+ "type": "string"
+ },
+ "tradeNo": {
+ "description": "本地订单号",
+ "type": "string"
+ },
+ "updatedAt": {
+ "description": "更新时间",
+ "type": "string"
+ },
+ "userId": {
+ "description": "用户ID",
+ "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": {
+ "actressBackGround": {
+ "description": "女优通用背景图",
+ "type": "string"
+ },
+ "advitiesTopicNum": {
+ "description": "广告间隔",
+ "type": "integer"
+ },
+ "appAddr": {
+ "description": "APP下载地址(落地页)",
+ "type": "string"
+ },
+ "channelCode": {
+ "description": "渠道码",
+ "type": "string"
+ },
+ "customerServiceHost": {
+ "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"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "imgAppCdn": {
+ "description": "图片CDN(app使用)",
+ "type": "string"
+ },
+ "imgAppUsCdn": {
+ "description": "图片CDN(海外app使用)",
+ "type": "string"
+ },
+ "imgWebCdn": {
+ "description": "图片CDN(web使用)",
+ "type": "string"
+ },
+ "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"
+ }
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "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": {
+ "actressBackGround": {
+ "description": "女优通用背景图",
+ "type": "string"
+ },
+ "advitiesTopicNum": {
+ "description": "广告间隔",
+ "type": "integer"
+ },
+ "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"
+ },
+ "enableH265": {
+ "description": "是否允许客户端使用 H265 地址播放",
+ "type": "boolean"
+ },
+ "foreverUrlAddr": {
+ "description": "永久地址",
+ "type": "string"
+ },
+ "freeVPN": {
+ "description": "免费vpn链接",
+ "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"
+ },
+ "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"
+ }
+ },
+ "watermarkImage": {
+ "description": "图片水印文件",
+ "type": "string"
+ },
+ "watermarkMedia": {
+ "description": "视频水印文件(底部)",
+ "type": "string"
+ },
+ "watermarkMediaTop": {
+ "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"
+ }
+ }
+ }
+ },
+ "webanalytics.AnalyticsDailyStat": {
+ "type": "object",
+ "properties": {
+ "list": {
+ "description": "当月数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ }
+ }
+ },
+ "webanalytics.AnalyticsStat": {
+ "type": "object",
+ "properties": {
+ "todayList": {
+ "description": "当日数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ },
+ "weekList": {
+ "description": "环比数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ },
+ "yestodayList": {
+ "description": "同比数据",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/webanalytics.LastWebStatInfo"
+ }
+ }
+ }
+ },
+ "webanalytics.LastWebStatInfo": {
+ "type": "object",
+ "properties": {
+ "activeUsers": {
+ "description": "最近半小时活跃数",
+ "type": "integer"
+ },
+ "appId": {
+ "description": "appId",
+ "type": "integer"
+ },
+ "appName": {
+ "description": "APP名称",
+ "type": "string"
+ },
+ "leadAmount": {
+ "description": "日导量充值",
+ "type": "number"
+ },
+ "leadNewCount": {
+ "description": "日导量新增",
+ "type": "number"
+ },
+ "sumDate": {
+ "description": "时间半小时",
+ "type": "string"
+ },
+ "yestodayActiveUsers": {
+ "description": "活跃用户数",
+ "type": "integer"
+ },
+ "yestodayNewUsers": {
+ "description": "新增数量",
+ "type": "integer"
+ },
+ "yestodayPV": {
+ "description": "页面浏览量 (PV)",
+ "type": "integer"
+ },
+ "yestodaySessions": {
+ "description": "会话数",
+ "type": "integer"
+ },
+ "yestodayUV": {
+ "description": "独立用户数 (UV)",
+ "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/web/docs/swagger.yaml b/web/docs/swagger.yaml
new file mode 100644
index 0000000..48662e7
--- /dev/null
+++ b/web/docs/swagger.yaml
@@ -0,0 +1,7462 @@
+basePath: /
+definitions:
+ cointransmod.CoinTrans:
+ properties:
+ amount:
+ description: 本次交易金币数(正整数)
+ type: integer
+ balanceAfter:
+ description: 交易后余额
+ type: integer
+ balanceBefore:
+ description: 交易前余额
+ type: integer
+ createdAt:
+ description: 交易时间
+ type: string
+ flowDir:
+ allOf:
+ - $ref: '#/definitions/cointransmod.FlowDir'
+ description: 收支方向:1收入 -1支出
+ id:
+ description: 唯一ID
+ type: integer
+ orderNo:
+ description: 关联订单号(可为空)
+ type: string
+ relatedId:
+ description: 关联内容ID(视频等,可为空)
+ type: integer
+ remark:
+ description: 备注描述
+ type: string
+ tranType:
+ allOf:
+ - $ref: '#/definitions/cointransmod.TranType'
+ description: 交易类型
+ userId:
+ description: 用户ID
+ type: integer
+ userName:
+ description: 用户昵称(快照)
+ type: string
+ type: object
+ cointransmod.FlowDir:
+ enum:
+ - 1
+ - -1
+ type: integer
+ x-enum-comments:
+ FlowIn: 收入(充值、赠送)
+ FlowOut: 支出(消费、解锁)
+ x-enum-varnames:
+ - FlowIn
+ - FlowOut
+ cointransmod.TranType:
+ enum:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ type: integer
+ x-enum-comments:
+ TranTypeAdminAdjust: 管理员手动调整
+ TranTypeCoinRecharge: 金币充值(购买金币)
+ TranTypeDarkAlley: 历史遗留交易类型(已下线)
+ TranTypeVideoUnlock: 视频解锁
+ TranTypeVipGiftCoin: 购买会员赠送金币
+ x-enum-varnames:
+ - TranTypeCoinRecharge
+ - TranTypeVipGiftCoin
+ - TranTypeDarkAlley
+ - TranTypeVideoUnlock
+ - TranTypeAdminAdjust
+ 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
+ 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
+ type: integer
+ x-enum-comments:
+ WatermarkStatusDone: 已完成
+ WatermarkStatusPending: 未处理(待 mediafile/sync)
+ WatermarkStatusProcessing: 处理中(已提交加水印任务,待回写)
+ WatermarkStatusRegistered: 已登记(原片已登记,待 task/add)
+ x-enum-varnames:
+ - WatermarkStatusPending
+ - WatermarkStatusRegistered
+ - WatermarkStatusProcessing
+ - WatermarkStatusDone
+ playletmod.ContentType:
+ enum:
+ - 0
+ - 1
+ type: integer
+ x-enum-comments:
+ ContentTypeAlbum: 影集
+ ContentTypeTheater: 短剧
+ x-enum-varnames:
+ - ContentTypeTheater
+ - ContentTypeAlbum
+ playletmod.Status:
+ enum:
+ - 0
+ - 1
+ type: integer
+ x-enum-comments:
+ StatusDown: 下架
+ StatusUp: 上架
+ x-enum-varnames:
+ - StatusDown
+ - StatusUp
+ proto.ContactUsHandleReq:
+ properties:
+ ids:
+ description: 联系我们ID集合
+ items:
+ type: integer
+ type: array
+ remark:
+ description: 处理备注
+ type: string
+ status:
+ description: 处理状态,0-待处理、1-已处理
+ type: integer
+ required:
+ - ids
+ - status
+ type: object
+ proto.ContactUsHandleResp:
+ properties:
+ matchedCount:
+ description: 命中数量
+ type: integer
+ modifiedCount:
+ description: 修改数量
+ type: integer
+ type: object
+ proto.ContactUsListPageResp:
+ properties:
+ list:
+ description: 集合列表
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.ContactUsListResp:
+ properties:
+ attachments:
+ description: 附件地址
+ items:
+ type: string
+ type: array
+ content:
+ description: 内容
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ email:
+ description: 邮箱
+ type: string
+ id:
+ description: ID
+ type: integer
+ remark:
+ description: 处理备注
+ type: string
+ status:
+ description: 处理状态,0-待处理、1-已处理
+ type: integer
+ title:
+ description: 标题
+ type: string
+ type:
+ description: 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ userName:
+ description: 用户名称
+ type: string
+ type: object
+ proto.FeedbackHandleReq:
+ properties:
+ ids:
+ description: 意见反馈ID集合
+ items:
+ type: integer
+ type: array
+ remark:
+ description: 处理备注
+ type: string
+ status:
+ description: 处理状态,0-待处理、1-已处理
+ type: integer
+ required:
+ - ids
+ - status
+ type: object
+ proto.FeedbackHandleResp:
+ properties:
+ matchedCount:
+ description: 命中数量
+ type: integer
+ modifiedCount:
+ description: 修改数量
+ type: integer
+ type: object
+ proto.FeedbackListPageResp:
+ properties:
+ list:
+ description: 集合列表
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.FeedbackListResp:
+ properties:
+ contact:
+ description: 联系方式
+ type: string
+ content:
+ description: 描述内容
+ type: string
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ description: ID
+ type: integer
+ remark:
+ description: 处理备注
+ type: string
+ screenshots:
+ description: 问题截图
+ items:
+ type: string
+ type: array
+ status:
+ description: 处理状态,0-待处理、1-已处理
+ type: integer
+ type:
+ description: 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ userName:
+ description: 用户名称
+ type: string
+ type: object
+ proto.PlayletAddReq:
+ properties:
+ anchor:
+ type: string
+ banner:
+ type: string
+ contentType:
+ $ref: '#/definitions/playletmod.ContentType'
+ cover:
+ type: string
+ desc:
+ type: string
+ isFeatured:
+ description: 是否爆剧(true-是,false-否)
+ type: boolean
+ isRecommend:
+ type: boolean
+ preview:
+ type: string
+ recommendRank:
+ type: integer
+ showOnBanner:
+ type: boolean
+ status:
+ $ref: '#/definitions/playletmod.Status'
+ tagIds:
+ items:
+ type: integer
+ type: array
+ themeIds:
+ items:
+ type: integer
+ type: array
+ title:
+ type: string
+ topRank:
+ type: integer
+ totalEpisode:
+ type: integer
+ required:
+ - title
+ type: object
+ proto.PlayletAddResp:
+ properties:
+ 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.PlayletChapterAddReq:
+ properties:
+ currentEpisode:
+ type: integer
+ hash:
+ type: string
+ height:
+ type: integer
+ md5:
+ type: string
+ mediaSize:
+ type: integer
+ mediaTime:
+ type: integer
+ payType:
+ $ref: '#/definitions/playletchaptermod.PayType'
+ playletId:
+ type: integer
+ ratio:
+ type: number
+ singleCoins:
+ type: integer
+ title:
+ type: string
+ videoUrl:
+ type: string
+ width:
+ type: integer
+ required:
+ - currentEpisode
+ - playletId
+ type: object
+ proto.PlayletChapterAddResp:
+ properties:
+ id:
+ type: integer
+ type: object
+ proto.PlayletChapterListPageResp:
+ properties:
+ list:
+ description: 集合列表
+ pageNum:
+ description: 页数
+ type: integer
+ pageSize:
+ description: 每页数
+ type: integer
+ totalCount:
+ description: 总共数量
+ type: integer
+ type: object
+ proto.PlayletChapterDelReq:
+ properties:
+ ids:
+ description: 章节ID集合,传1个表示单条删除,传多个表示批量删除
+ items:
+ type: integer
+ type: array
+ 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.PlayletChapterUpdateReq:
+ properties:
+ currentEpisode:
+ type: integer
+ hash:
+ type: string
+ height:
+ type: integer
+ id:
+ type: integer
+ md5:
+ type: string
+ mediaSize:
+ type: integer
+ mediaTime:
+ type: integer
+ payType:
+ $ref: '#/definitions/playletchaptermod.PayType'
+ ratio:
+ type: number
+ singleCoins:
+ type: integer
+ title:
+ type: string
+ videoUrl:
+ type: string
+ width:
+ 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.PlayletRecommendSetReq:
+ properties:
+ ids:
+ description: 短剧ID集合,传1个表示单条设置,传多个表示批量设置
+ items:
+ type: integer
+ type: array
+ isRecommend:
+ description: 是否推荐(true-推荐,false-不推荐)
+ type: boolean
+ 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集合-多个用逗号隔开,请求示例[28830,28829,28828,28827,28826,28825]
+ 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
+ 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
+ status:
+ type: boolean
+ required:
+ - id
+ type: object
+ proto.PlayletThemeAddReq:
+ properties:
+ icon:
+ type: string
+ name:
+ type: string
+ rank:
+ type: integer
+ status:
+ type: boolean
+ required:
+ - name
+ type: object
+ proto.PlayletThemeAddResp:
+ properties:
+ id:
+ type: integer
+ type: object
+ proto.PlayletThemeDelReq:
+ properties:
+ 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:
+ type: string
+ id:
+ type: integer
+ name:
+ type: string
+ rank:
+ type: integer
+ status:
+ type: boolean
+ required:
+ - id
+ type: object
+ proto.PlayletUpdateReq:
+ properties:
+ anchor:
+ type: string
+ banner:
+ type: string
+ contentType:
+ $ref: '#/definitions/playletmod.ContentType'
+ cover:
+ type: string
+ desc:
+ type: string
+ id:
+ type: integer
+ isFeatured:
+ description: 是否爆剧(true-是,false-否)
+ type: boolean
+ isRecommend:
+ type: boolean
+ preview:
+ type: string
+ recommendRank:
+ type: integer
+ showOnBanner:
+ type: boolean
+ status:
+ $ref: '#/definitions/playletmod.Status'
+ tagIds:
+ items:
+ type: integer
+ type: array
+ themeIds:
+ items:
+ type: integer
+ type: array
+ title:
+ type: string
+ topRank:
+ type: integer
+ totalEpisode:
+ type: integer
+ required:
+ - id
+ type: object
+ pubdef.LoginType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ LoginTypeAccount: 账号登陆
+ LoginTypeDevice: 设备登陆
+ LoginTypeEmail: 邮箱登陆
+ x-enum-varnames:
+ - LoginTypeDevice
+ - LoginTypeAccount
+ - LoginTypeEmail
+ 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
+ pubdef.UserType:
+ enum:
+ - 0
+ - 1
+ - 0
+ type: integer
+ x-enum-comments:
+ UserTypeGuest: 游客用户
+ UserTypeNormal: 普通用户(已注册)
+ x-enum-varnames:
+ - UserTypeGuest
+ - UserTypeNormal
+ - UserTypeNone
+ pubdef.VipOrderStatus:
+ enum:
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ VipOrderFailed: 支付失败
+ VipOrderPending: 待支付
+ VipOrderSuccess: 支付成功
+ x-enum-varnames:
+ - VipOrderPending
+ - VipOrderSuccess
+ - VipOrderFailed
+ pubdef.VipType:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ VipTypeMonth: 月卡
+ VipTypeNone: 没卡
+ VipTypeYear: 年卡
+ x-enum-varnames:
+ - VipTypeNone
+ - VipTypeMonth
+ - VipTypeYear
+ rechargepackagemod.PackageType:
+ enum:
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ PackageTypeCoin: 金币充值
+ PackageTypeVip: 会员畅享
+ x-enum-varnames:
+ - PackageTypeVip
+ - PackageTypeCoin
+ rechargepackagemod.Status:
+ enum:
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ StatusDisabled: 停用
+ StatusEnabled: 启用
+ x-enum-varnames:
+ - StatusEnabled
+ - StatusDisabled
+ 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_cointrans.CoinTransReq:
+ properties:
+ endTime:
+ description: 结束时间 2006-01-02 15:04:05
+ type: string
+ flowDir:
+ allOf:
+ - $ref: '#/definitions/cointransmod.FlowDir'
+ description: 收支方向:1收入 -1支出
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ startTime:
+ description: 开始时间 2006-01-02 15:04:05
+ type: string
+ tranType:
+ allOf:
+ - $ref: '#/definitions/cointransmod.TranType'
+ description: 交易类型
+ userId:
+ description: 用户ID
+ type: integer
+ type: object
+ srv_cointrans.CoinTransResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/cointransmod.CoinTrans'
+ type: array
+ total:
+ type: integer
+ 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:
+ description: 对象类型 10-短剧 11-短剧剧集
+ type: integer
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ reviewStatus:
+ type: integer
+ text:
+ description: 评论内容
+ type: string
+ userId:
+ description: 评论用户ID
+ type: integer
+ 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:
+ countLike:
+ description: 评论点赞数
+ type: integer
+ 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: 对象类型 10-短剧 11-短剧剧集
+ type: integer
+ 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
+ userAvatar:
+ description: 评论用户头像
+ type: string
+ userId:
+ description: 评论用户ID
+ type: integer
+ userName:
+ description: 用户名称
+ type: string
+ 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_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_recharge.BillCallbackParam:
+ properties:
+ oid:
+ type: string
+ payMoney:
+ type: integer
+ paymentAt:
+ type: string
+ successAt:
+ type: string
+ transNo:
+ type: string
+ type: object
+ srv_rechargepackage.RechargePackageAddReq:
+ properties:
+ baseCoin:
+ description: 基础金币数,会员套餐传0
+ type: integer
+ durationDays:
+ description: 会员有效天数,金币套餐传0
+ type: integer
+ giftCoin:
+ description: 赠送金币数
+ type: integer
+ name:
+ description: 展示标题
+ type: string
+ originalPrice:
+ description: 原价,单位分
+ type: integer
+ packageType:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.PackageType'
+ description: 套餐类型:1-会员畅享 2-金币充值
+ price:
+ description: 售价,单位分
+ type: integer
+ remark:
+ description: 后台备注
+ type: string
+ sort:
+ description: 排序,越小越靠前
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.Status'
+ description: 状态:1-启用 2-停用,默认1
+ required:
+ - name
+ - packageType
+ - price
+ type: object
+ srv_rechargepackage.RechargePackageDelReq:
+ properties:
+ id:
+ description: 套餐ID
+ type: integer
+ required:
+ - id
+ type: object
+ srv_rechargepackage.RechargePackageItem:
+ properties:
+ baseCoin:
+ description: 基础金币数
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ durationDays:
+ description: 会员有效天数
+ type: integer
+ durationHours:
+ description: 会员有效小时数,由durationDays计算
+ type: integer
+ giftCoin:
+ description: 赠送金币数
+ type: integer
+ id:
+ description: 套餐ID
+ type: integer
+ name:
+ description: 展示标题
+ type: string
+ originalPrice:
+ description: 原价,单位分
+ type: integer
+ packageType:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.PackageType'
+ description: 套餐类型:1-会员畅享 2-金币充值
+ price:
+ description: 售价,单位分
+ type: integer
+ remark:
+ description: 后台备注
+ type: string
+ sort:
+ description: 排序,越小越靠前
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.Status'
+ description: 状态:1-启用 2-停用
+ subTitle:
+ description: 展示副标题
+ type: string
+ totalCoin:
+ description: 到账总金币数,由baseCoin+giftCoin计算
+ type: integer
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ srv_rechargepackage.RechargePackageListReq:
+ properties:
+ id:
+ description: 套餐ID
+ type: integer
+ name:
+ description: 展示标题,模糊搜索
+ type: string
+ packageType:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.PackageType'
+ description: 套餐类型:1-会员畅享 2-金币充值
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.Status'
+ description: 状态:1-启用 2-停用
+ type: object
+ srv_rechargepackage.RechargePackageListResp:
+ properties:
+ list:
+ description: 当前页列表
+ items:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageItem'
+ type: array
+ total:
+ description: 总数量
+ type: integer
+ type: object
+ srv_rechargepackage.RechargePackageStatusSetReq:
+ properties:
+ ids:
+ description: 套餐ID集合
+ items:
+ type: integer
+ type: array
+ status:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.Status'
+ description: 状态:1-启用 2-停用
+ type: object
+ srv_rechargepackage.RechargePackageUpdateReq:
+ properties:
+ baseCoin:
+ description: 基础金币数
+ type: integer
+ durationDays:
+ description: 会员有效天数
+ type: integer
+ giftCoin:
+ description: 赠送金币数
+ type: integer
+ id:
+ description: 套餐ID
+ type: integer
+ name:
+ description: 展示标题
+ type: string
+ originalPrice:
+ description: 原价,单位分
+ type: integer
+ packageType:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.PackageType'
+ description: 套餐类型:1-会员畅享 2-金币充值
+ price:
+ description: 售价,单位分
+ type: integer
+ remark:
+ description: 后台备注
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/rechargepackagemod.Status'
+ description: 状态:1-启用 2-停用
+ required:
+ - id
+ type: object
+ srv_rechargepackage.VipPackageOption:
+ properties:
+ id:
+ description: 套餐ID
+ type: integer
+ name:
+ description: 套餐名称
+ type: string
+ 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:
+ pageNum:
+ type: integer
+ pageSize:
+ 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:
+ 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.GiftCoinReq:
+ properties:
+ coin:
+ description: 赠送金币数(正数=增加,负数=扣除)
+ type: integer
+ remark:
+ description: 备注(记录在流水表)
+ type: string
+ userId:
+ description: 目标用户ID
+ type: integer
+ required:
+ - coin
+ - userId
+ type: object
+ srv_user.UpdateVipReq:
+ properties:
+ packageId:
+ description: 会员畅享套餐ID
+ type: integer
+ userId:
+ description: 用户ID
+ type: integer
+ required:
+ - packageId
+ - userId
+ 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:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserType'
+ 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:
+ id:
+ description: 分类ID
+ type: integer
+ name:
+ description: 分类名称
+ type: string
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ type: object
+ srv_videocategory.VideoCategoryResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/videocategorymod.VideoCategory'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videocategory.VideoCategoryUpdate:
+ properties:
+ appId:
+ description: appId,唯一id
+ type: integer
+ contentType:
+ description: '内容:0: 默认 1:首页 2:今日'
+ type: integer
+ desc:
+ description: 描述
+ type: string
+ indexNow:
+ type: integer
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sortType:
+ description: 排序方式 0:上线时间 1:观看次数 2排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ title:
+ description: APP显示
+ type: string
+ updatedAt:
+ type: string
+ type: object
+ srv_videocategory.VideoCategoryUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_videocategory.VideoCategoryUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_videocreater.VideoCreaterDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videocreater.VideoCreaterReq:
+ properties:
+ id:
+ description: 分类ID
+ type: integer
+ name:
+ description: 分类名称
+ type: string
+ 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
+ indexNow:
+ type: integer
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ updatedAt:
+ type: string
+ type: object
+ srv_videocreater.VideoCreaterUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_videocreater.VideoCreaterUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ 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.SetFakeWatchTimesReq:
+ properties:
+ ids:
+ description: 视频ID集合,支持单个或批量
+ items:
+ type: integer
+ type: array
+ watchTimesMax:
+ description: 假观看次数随机最大值
+ type: integer
+ watchTimesMin:
+ description: 假观看次数随机最小值
+ type: integer
+ type: object
+ srv_videoinfo.VideoInfoDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videoinfo.VideoInfoReq:
+ properties:
+ categoryId:
+ description: 分类Id
+ type: integer
+ creatorId:
+ description: 作者Id
+ type: integer
+ creatorName:
+ description: 作者名称
+ type: string
+ endTime:
+ description: 创建时间-截止
+ type: string
+ id:
+ description: Id
+ type: integer
+ isH265:
+ description: 是否支持 H265
+ type: boolean
+ isHot:
+ description: 是否热门
+ 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=播放次数
+ type: integer
+ startTime:
+ description: 创建时间-开始
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.InformationSatus'
+ description: 帖子上架状态
+ tag:
+ description: 标签
+ type: integer
+ title:
+ description: 标题
+ type: string
+ topicId:
+ description: 专题Id
+ type: integer
+ 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:
+ addedTime:
+ type: string
+ categoryIds:
+ description: 分类Id
+ items:
+ type: integer
+ type: array
+ content:
+ type: string
+ 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
+ isTop:
+ description: 是否置顶
+ type: boolean
+ 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
+ sort:
+ description: 排序
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.InformationSatus'
+ description: 帖子上架状态
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: 帖子标题
+ type: string
+ topicIds:
+ description: 专题ID
+ items:
+ type: integer
+ type: array
+ 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_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.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
+ indexNow:
+ type: integer
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ 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_videotag.VideoTagInfoUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_videotag.VideoTagInfoUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_videotopic.VideoTopicDelReq:
+ properties:
+ id:
+ type: integer
+ required:
+ - id
+ type: object
+ srv_videotopic.VideoTopicReq:
+ properties:
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ videoCategoryId:
+ type: integer
+ type: object
+ srv_videotopic.VideoTopicResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/videotopicmod.VideoTopicModel'
+ type: array
+ total:
+ type: integer
+ type: object
+ srv_videotopic.VideoTopicUpdate:
+ properties:
+ desc:
+ description: 描述
+ type: string
+ image:
+ description: 背景
+ type: string
+ indexNow:
+ type: integer
+ isReIndexNow:
+ description: 是否重新提交索引
+ type: boolean
+ name:
+ description: 名称
+ type: string
+ rank:
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ status:
+ description: 上架状态
+ type: boolean
+ title:
+ description: APP显示
+ type: string
+ updatedAt:
+ type: string
+ videoCategoryId:
+ description: 视频分类id
+ type: integer
+ videoTopicType:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.MediaType'
+ description: 视频类型
+ type: object
+ srv_videotopic.VideoTopicUpdateReq:
+ properties:
+ id:
+ type: integer
+ param:
+ allOf:
+ - $ref: '#/definitions/srv_videotopic.VideoTopicUpdate'
+ description: appId,唯一id
+ required:
+ - id
+ type: object
+ srv_viporder.VipOrderReq:
+ properties:
+ endTime:
+ description: 创建时间-结束
+ type: string
+ orderType:
+ allOf:
+ - $ref: '#/definitions/vipordermod.OrderType'
+ description: 订单类型:1会员 2金币
+ pageNum:
+ type: integer
+ pageSize:
+ type: integer
+ startTime:
+ description: 创建时间-开始
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.VipOrderStatus'
+ description: 订单状态
+ tradeNo:
+ description: 订单号
+ type: string
+ userId:
+ description: 用户ID
+ type: integer
+ type: object
+ srv_viporder.VipOrderResp:
+ properties:
+ list:
+ items:
+ $ref: '#/definitions/vipordermod.VipOrder'
+ type: array
+ total:
+ 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
+ deviceBrand:
+ description: 设备品牌
+ type: string
+ deviceModel:
+ description: 设备型号
+ type: string
+ districtCode:
+ description: 商区码
+ type: string
+ email:
+ 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
+ merchantExpiredAt:
+ description: 历史遗留字段
+ type: string
+ modifyUserNameAt:
+ description: 上一次修改用户时间
+ type: string
+ nickName:
+ description: 昵称
+ type: string
+ oriVer:
+ description: 注册时的app版本
+ type: string
+ password:
+ description: 登陆密码
+ type: string
+ privacyMode:
+ description: 隐私模式:true-开启,false-关闭(默认)
+ type: boolean
+ 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
+ sessionId:
+ description: 会话ID
+ 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
+ systemName:
+ description: 系统名称
+ type: string
+ systemVersion:
+ description: 系统版本
+ type: string
+ type:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserType'
+ description: 类型
+ updatedAt:
+ description: 更新时间
+ type: string
+ vipType:
+ allOf:
+ - $ref: '#/definitions/pubdef.VipType'
+ description: Vip类型
+ 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
+ deviceBrand:
+ description: 设备品牌
+ type: string
+ deviceModel:
+ 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
+ sessionId:
+ description: 会话ID
+ 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
+ systemName:
+ description: 系统名称
+ type: string
+ systemVersion:
+ description: 系统版本
+ type: string
+ type:
+ allOf:
+ - $ref: '#/definitions/pubdef.UserType'
+ description: 类型
+ updatedAt:
+ description: 更新时间
+ type: string
+ vipType:
+ allOf:
+ - $ref: '#/definitions/pubdef.VipType'
+ description: Vip类型
+ type: object
+ videocategorymod.VideoCategory:
+ properties:
+ contentType:
+ description: '内容:0: 默认 1:首页 2:今日 3:合集'
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 描述
+ type: string
+ 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相关信息
+ sortType:
+ description: 排序方式 0:上线时间 1:观看次数 2排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ title:
+ description: APP显示
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ videocreatermod.VideoCreaterInfo:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ id:
+ type: integer
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ name:
+ description: 帖子标题
+ type: string
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ sort:
+ description: 排序
+ type: integer
+ status:
+ description: 上架状态
+ type: boolean
+ updatedAt:
+ description: 更新时间
+ type: string
+ type: object
+ videoinformationmod.FakeFlow:
+ properties:
+ watchTimes:
+ description: 假观看次数
+ type: integer
+ type: object
+ videoinformationmod.Flow:
+ properties:
+ comments:
+ description: 评论条数
+ type: integer
+ monthWatchs:
+ description: 本月观看次数
+ type: integer
+ showTimes:
+ description: 展示次数
+ type: integer
+ todayWatchs:
+ description: 当日观看次数
+ type: integer
+ watchTimes:
+ description: 观看次数
+ type: integer
+ weekWatchs:
+ 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.InformationSatus:
+ enum:
+ - 0
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ InformationSatusVerifyCancel: 下架
+ InformationSatusVerifyOK: 上架
+ InformationSatusWaiting: 初始化
+ x-enum-varnames:
+ - InformationSatusWaiting
+ - InformationSatusVerifyOK
+ - InformationSatusVerifyCancel
+ videoinformationmod.MediaType:
+ enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ type: integer
+ x-enum-comments:
+ MediaType_Advitise: 广告
+ MediaType_Collection: 合集
+ MediaType_Video: 视频(帖子)
+ MediaType_VideoV2: 视频
+ x-enum-varnames:
+ - MediaType_Video
+ - MediaType_Collection
+ - MediaType_Advitise
+ - MediaType_VideoV2
+ 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: H265 视频地址
+ type: string
+ height:
+ description: 分辨率高(视频才会存在)
+ type: integer
+ preMp4FileNameFs:
+ description: 预览MP4文件
+ type: string
+ taskId:
+ description: 视频ID(视频才会存在)
+ type: string
+ unencryptedPreMp4:
+ description: 不加密的预览MP4
+ type: string
+ url:
+ description: 视频URl
+ type: string
+ videoTime:
+ description: 影片长度
+ type: integer
+ width:
+ description: 分辨率宽(视频才会存在)
+ type: integer
+ type: object
+ videoinformationmod.VideoInformation:
+ properties:
+ addedTime:
+ description: 帖子上架时间
+ type: string
+ audioFile:
+ description: 音频文件路径
+ type: string
+ categoryIds:
+ description: 分类Id (帖子用)
+ items:
+ type: integer
+ type: array
+ content:
+ description: 富文本
+ type: string
+ 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/videoinformationmod.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
+ isTop:
+ description: 是否置顶
+ type: boolean
+ 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/videoinformationmod.Flow'
+ description: 真实流量
+ remark:
+ description: 备注
+ type: string
+ secondTags:
+ description: 二级标签标签
+ items:
+ type: string
+ type: array
+ secondUpdatedAt:
+ description: 二次修改时间
+ type: string
+ smartCover:
+ description: 智能封面
+ type: string
+ sort:
+ description: 排序
+ type: integer
+ status:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.InformationSatus'
+ description: 帖子上架状态
+ syncId:
+ description: 同步ID
+ type: string
+ tags:
+ description: 标签
+ items:
+ type: integer
+ type: array
+ title:
+ description: 帖子标题
+ type: string
+ topicIds:
+ description: 专题ID(视频用)
+ items:
+ type: integer
+ type: array
+ 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
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ 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
+ watchTimes:
+ description: 预计算:该标签下所有视频的累计播放量
+ type: integer
+ type: object
+ videotopicmod.VideoTopicModel:
+ properties:
+ createdAt:
+ description: 创建时间
+ type: string
+ desc:
+ description: 描述
+ type: string
+ id:
+ description: Id
+ type: integer
+ image:
+ description: 背景
+ type: string
+ indexNow:
+ description: 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ type: integer
+ name:
+ description: 专题名称
+ type: string
+ rank:
+ description: 排序
+ type: integer
+ seoInfo:
+ allOf:
+ - $ref: '#/definitions/pubdef.SeoValue'
+ description: Seo相关信息
+ status:
+ description: 上架状态
+ type: boolean
+ title:
+ description: APP显示
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ videoCategoryId:
+ description: 视频分类id
+ type: integer
+ videoTopicType:
+ allOf:
+ - $ref: '#/definitions/videoinformationmod.MediaType'
+ description: 视频类型
+ works:
+ description: 作品数
+ type: integer
+ type: object
+ vipordermod.OrderType:
+ enum:
+ - 1
+ - 2
+ type: integer
+ x-enum-comments:
+ OrderTypeCoin: 金币购买
+ OrderTypeVip: 会员购买
+ x-enum-varnames:
+ - OrderTypeVip
+ - OrderTypeCoin
+ vipordermod.VipOrder:
+ properties:
+ amount:
+ description: 支付金额(分)
+ type: integer
+ cardId:
+ description: ── VIP 专用 ──
+ type: integer
+ cardName:
+ description: 套餐名快照
+ type: string
+ coin:
+ description: 到账金币数(快照)
+ type: integer
+ coinCfgId:
+ description: ── 金币专用 ──
+ type: integer
+ createdAt:
+ description: 创建时间
+ type: string
+ finishedAt:
+ description: 完成时间
+ type: string
+ id:
+ description: 唯一ID
+ type: integer
+ ipAddr:
+ description: 客户端 IP
+ type: string
+ oid:
+ description: 第三方订单号
+ type: string
+ orderType:
+ allOf:
+ - $ref: '#/definitions/vipordermod.OrderType'
+ description: 订单类型:1会员 2金币
+ payChannel:
+ description: 支付渠道
+ type: string
+ payMode:
+ description: 支付方式:overage/alipay/wechat
+ type: string
+ payRate:
+ description: 费率
+ type: string
+ payUrl:
+ description: 支付链接
+ type: string
+ payUrlMode:
+ description: 链接打开方式
+ type: string
+ status:
+ allOf:
+ - $ref: '#/definitions/pubdef.VipOrderStatus'
+ description: 订单状态
+ sysType:
+ description: ios/android
+ type: string
+ tradeNo:
+ description: 本地订单号
+ type: string
+ updatedAt:
+ description: 更新时间
+ type: string
+ userId:
+ description: 用户ID
+ 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:
+ actressBackGround:
+ description: 女优通用背景图
+ type: string
+ advitiesTopicNum:
+ description: 广告间隔
+ type: integer
+ appAddr:
+ description: APP下载地址(落地页)
+ type: string
+ channelCode:
+ description: 渠道码
+ type: string
+ customerServiceHost:
+ 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
+ id:
+ type: integer
+ imgAppCdn:
+ description: 图片CDN(app使用)
+ type: string
+ imgAppUsCdn:
+ description: 图片CDN(海外app使用)
+ type: string
+ imgWebCdn:
+ description: 图片CDN(web使用)
+ type: string
+ 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
+ watermarkImage:
+ description: 图片水印文件
+ type: string
+ watermarkMedia:
+ description: 视频水印文件(底部)
+ type: string
+ watermarkMediaTop:
+ 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:
+ actressBackGround:
+ description: 女优通用背景图
+ type: string
+ advitiesTopicNum:
+ description: 广告间隔
+ type: integer
+ 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
+ enableH265:
+ description: 是否允许客户端使用 H265 地址播放
+ type: boolean
+ foreverUrlAddr:
+ description: 永久地址
+ type: string
+ freeVPN:
+ description: 免费vpn链接
+ 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
+ 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
+ watermarkImage:
+ description: 图片水印文件
+ type: string
+ watermarkMedia:
+ description: 视频水印文件(底部)
+ type: string
+ watermarkMediaTop:
+ 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
+ webanalytics.AnalyticsDailyStat:
+ properties:
+ list:
+ description: 当月数据
+ items:
+ $ref: '#/definitions/webanalytics.LastWebStatInfo'
+ type: array
+ type: object
+ webanalytics.AnalyticsStat:
+ properties:
+ todayList:
+ description: 当日数据
+ items:
+ $ref: '#/definitions/webanalytics.LastWebStatInfo'
+ type: array
+ weekList:
+ description: 环比数据
+ items:
+ $ref: '#/definitions/webanalytics.LastWebStatInfo'
+ type: array
+ yestodayList:
+ description: 同比数据
+ items:
+ $ref: '#/definitions/webanalytics.LastWebStatInfo'
+ type: array
+ type: object
+ webanalytics.LastWebStatInfo:
+ properties:
+ activeUsers:
+ description: 最近半小时活跃数
+ type: integer
+ appId:
+ description: appId
+ type: integer
+ appName:
+ description: APP名称
+ type: string
+ leadAmount:
+ description: 日导量充值
+ type: number
+ leadNewCount:
+ description: 日导量新增
+ type: number
+ sumDate:
+ description: 时间半小时
+ type: string
+ yestodayActiveUsers:
+ description: 活跃用户数
+ type: integer
+ yestodayNewUsers:
+ description: 新增数量
+ type: integer
+ yestodayPV:
+ description: 页面浏览量 (PV)
+ type: integer
+ yestodaySessions:
+ description: 会话数
+ type: integer
+ yestodayUV:
+ description: 独立用户数 (UV)
+ type: integer
+ 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/card/order/list:
+ post:
+ consumes:
+ - application/json
+ description: 分页查询充值订单,支持按 OrderType 筛选:1会员 2金币;不传 orderType 则返回全部类型
+ parameters:
+ - description: 查询参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_viporder.VipOrderReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_viporder.VipOrderResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 查询充值订单列表
+ tags:
+ - 会员卡管理
+ /api/web/coin/trans:
+ post:
+ consumes:
+ - application/json
+ description: 分页查询金币收支流水,支持按用户、收支方向、交易类型、时间范围筛选
+ parameters:
+ - description: 查询参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_cointrans.CoinTransReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_cointrans.CoinTransResp'
+ "400":
+ description: 参数错误
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 查询金币交易记录
+ tags:
+ - 金币管理
+ /api/web/contactus/handle:
+ post:
+ consumes:
+ - application/json
+ description: 批量更新联系我们处理状态
+ parameters:
+ - description: web端联系我们处理请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.ContactUsHandleReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.ContactUsHandleResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端处理联系我们 -- by 旺鸣
+ tags:
+ - web联系我们相关接口
+ /api/web/contactus/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询联系我们列表
+ parameters:
+ - description: 创建结束时间,格式:2026-06-09 00:00:00
+ in: query
+ name: createEndTime
+ type: string
+ - description: 创建开始时间,格式:2026-06-08 00:00:00
+ in: query
+ name: createStartTime
+ type: string
+ - description: 邮箱
+ in: query
+ name: email
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 处理状态,0-待处理、1-已处理
+ in: query
+ name: status
+ type: integer
+ - description: 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
+ in: query
+ name: type
+ type: integer
+ - description: 用户ID
+ in: query
+ name: userId
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.ContactUsListPageResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.ContactUsListResp'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端联系我们列表 -- 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/feedback/handle:
+ post:
+ consumes:
+ - application/json
+ description: 批量更新意见反馈处理状态
+ parameters:
+ - description: web端意见反馈处理请求
+ in: body
+ name: params
+ required: true
+ schema:
+ $ref: '#/definitions/proto.FeedbackHandleReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.FeedbackHandleResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端处理意见反馈 -- by 旺鸣
+ tags:
+ - web意见反馈相关接口
+ /api/web/feedback/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询用户意见反馈列表
+ parameters:
+ - description: 创建结束时间,格式:2026-06-09 00:00:00
+ in: query
+ name: createEndTime
+ type: string
+ - description: 创建开始时间,格式:2026-06-08 00:00:00
+ in: query
+ name: createStartTime
+ type: string
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - description: 处理状态,0-待处理、1-已处理
+ in: query
+ name: status
+ type: integer
+ - description: 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
+ in: query
+ name: type
+ type: integer
+ - description: 用户ID
+ in: query
+ name: userId
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ allOf:
+ - $ref: '#/definitions/proto.FeedbackListPageResp'
+ - properties:
+ list:
+ items:
+ $ref: '#/definitions/proto.FeedbackListResp'
+ type: array
+ type: object
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端意见反馈列表 -- by 旺鸣
+ tags:
+ - web意见反馈相关接口
+ /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/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
+ 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端新增短剧
+ 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/chapter/add:
+ post:
+ consumes:
+ - application/json
+ 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端新增章节
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/list:
+ get:
+ consumes:
+ - application/json
+ parameters:
+ - in: query
+ name: id
+ type: integer
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - in: query
+ name: playletId
+ type: integer
+ - enum:
+ - 0
+ - 1
+ - 2
+ - 3
+ in: query
+ name: watermarkStatus
+ type: integer
+ x-enum-comments:
+ WatermarkStatusDone: 已完成
+ WatermarkStatusPending: 未处理(待 mediafile/sync)
+ WatermarkStatusProcessing: 处理中(已提交加水印任务,待回写)
+ WatermarkStatusRegistered: 已登记(原片已登记,待 task/add)
+ x-enum-varnames:
+ - WatermarkStatusPending
+ - WatermarkStatusRegistered
+ - WatermarkStatusProcessing
+ - WatermarkStatusDone
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/proto.PlayletChapterListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端章节列表
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/chapter/del:
+ post:
+ consumes:
+ - application/json
+ description: 根据章节ID集合删除 playlet_chapter 记录,传1个ID表示单条删除,传多个ID表示批量删除;同时清理该剧集的点赞、收藏、解锁记录
+ 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/pay-type/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据章节ID集合批量更新 playlet_chapter 集合 payType 字段,传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/update:
+ post:
+ consumes:
+ - application/json
+ 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端更新章节
+ 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/list:
+ get:
+ consumes:
+ - application/json
+ description: 分页查询短剧列表
+ parameters:
+ - enum:
+ - 0
+ - 1
+ in: query
+ name: contentType
+ type: integer
+ x-enum-comments:
+ ContentTypeAlbum: 影集
+ ContentTypeTheater: 短剧
+ x-enum-varnames:
+ - ContentTypeTheater
+ - ContentTypeAlbum
+ - in: query
+ name: id
+ type: integer
+ - description: 是否爆剧(true-是,false-否)
+ in: query
+ name: isFeatured
+ type: boolean
+ - in: query
+ name: isRecommend
+ type: boolean
+ - in: query
+ name: pageNum
+ type: integer
+ - in: query
+ name: pageSize
+ type: integer
+ - in: query
+ name: showOnBanner
+ type: boolean
+ - enum:
+ - 0
+ - 1
+ in: query
+ name: status
+ type: integer
+ x-enum-comments:
+ StatusDown: 下架
+ StatusUp: 上架
+ x-enum-varnames:
+ - StatusDown
+ - StatusUp
+ - in: query
+ name: themeId
+ type: integer
+ - 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端短剧列表
+ 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端新增标签
+ 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端删除标签
+ 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端标签列表
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/tag/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 tagIds 字段,传1个ID表示单条设置,传多个ID表示批量设置;tagIds
+ 可设置多个标签,传空数组表示清空标签
+ 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端更新标签
+ 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端新增题材
+ 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端删除题材
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/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.PlayletThemeListPageResp'
+ "400":
+ description: 失败
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: web端题材列表
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/theme/set:
+ post:
+ consumes:
+ - application/json
+ description: 根据短剧ID集合批量更新 playlet 集合 themeIds 字段,传1个ID表示单条设置,传多个ID表示批量设置;themeIds
+ 可设置多个题材
+ 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端更新题材
+ tags:
+ - web短剧相关接口
+ /api/web/playlet/update:
+ post:
+ consumes:
+ - application/json
+ 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端更新短剧
+ 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/recharge/package/add:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 新增参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageAddReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 新增购买套餐 -- by 王鸣
+ tags:
+ - 套餐管理
+ /api/web/recharge/package/del:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 删除参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除购买套餐 -- by 王鸣
+ tags:
+ - 套餐管理
+ /api/web/recharge/package/list:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 查询参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageListReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageListResp'
+ security:
+ - ApiKeyAuth: []
+ summary: 查询购买套餐列表 -- by 王鸣
+ tags:
+ - 套餐管理
+ /api/web/recharge/package/status/set:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 设置状态参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageStatusSetReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 单/批量设置购买套餐状态 -- by 王鸣
+ tags:
+ - 套餐管理
+ /api/web/recharge/package/update:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 更新参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_rechargepackage.RechargePackageUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新购买套餐 -- by 王鸣
+ tags:
+ - 套餐管理
+ /api/web/recharge/package/vip/list:
+ post:
+ description: 只返回 PackageType=1 的启用套餐,字段仅包含 id 和 name,不分页
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ items:
+ $ref: '#/definitions/srv_rechargepackage.VipPackageOption'
+ type: array
+ security:
+ - ApiKeyAuth: []
+ summary: 查询会员畅享套餐选项 -- by 王鸣
+ tags:
+ - 套餐管理
+ /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: 媒体类型 4-短剧 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/stat/analytics:
+ post:
+ consumes:
+ - application/json
+ description: 统计数据
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/webanalytics.AnalyticsStat'
+ "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/webanalytics.AnalyticsDailyStat'
+ "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/third/paycenter/bill/callback:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 回调参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_recharge.BillCallbackParam'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: success
+ schema:
+ type: string
+ 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/giftcoin:
+ post:
+ consumes:
+ - application/json
+ description: coin 为正数表示赠送,为负数表示扣除;操作会自动记录金币流水
+ parameters:
+ - description: 赠送参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_user.GiftCoinReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ 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/user/vip/update:
+ post:
+ consumes:
+ - application/json
+ description: 根据会员畅享套餐ID修改用户会员信息,不处理赠送金币
+ parameters:
+ - description: 修改会员参数
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_user.UpdateVipReq'
+ 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.VideoCategoryUpdateReq'
+ 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.VideoCreaterUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个视频作者
+ 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/setFakeWatchTimes:
+ post:
+ consumes:
+ - application/json
+ description: 根据ids批量随机设置视频假观看次数
+ 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/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.VideoTagInfoUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个标签管理
+ tags:
+ - 标签管理
+ /api/web/videotopic/add:
+ post:
+ consumes:
+ - application/json
+ description: 添加一个视频专题
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/videotopicmod.VideoTopicModel'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 添加一个视频专题
+ tags:
+ - 视频专题
+ /api/web/videotopic/del:
+ post:
+ consumes:
+ - application/json
+ description: 删除一个视频专题
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videotopic.VideoTopicDelReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 删除一个视频专题
+ tags:
+ - 视频专题
+ /api/web/videotopic/list:
+ post:
+ consumes:
+ - application/json
+ description: 分组分页获取视频专题
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videotopic.VideoTopicReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ $ref: '#/definitions/srv_videotopic.VideoTopicResp'
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 分组分页获取视频专题
+ tags:
+ - 视频专题
+ /api/web/videotopic/update:
+ post:
+ consumes:
+ - application/json
+ description: 更新一个视频专题
+ parameters:
+ - description: 参数列表
+ in: body
+ name: param
+ required: true
+ schema:
+ $ref: '#/definitions/srv_videotopic.VideoTopicUpdateReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: 成功
+ schema:
+ type: string
+ "400":
+ description: 获取失败的返回结果
+ schema:
+ type: string
+ security:
+ - ApiKeyAuth: []
+ summary: 更新一个视频专题
+ tags:
+ - 视频专题
+ /wbapi/69cgw/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:
+ - 标签管理
+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..e5bc564
--- /dev/null
+++ b/web/middleware/auth/http.go
@@ -0,0 +1,283 @@
+package auth
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "sync"
+ "videoweb/pub/common"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/ipaddr"
+ "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/newenkey",
+ "/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",
+ "/wbapi",
+ "/api/web/third/paycenter",
+ }
+)
+
+// 读取角色信息
+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
+}
+
+// 验证后台账号IP
+func authMwIP(ctx *gin.Context) (int, error) {
+
+ cIP := ipaddr.ResolveClientIP(ctx)
+ 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
+ // 对指定 group 跳过
+ if strings.HasPrefix(ctx.Request.URL.Path, "/mdps/mdcom/api/web") {
+ ctx.Next()
+ 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)
+ }
+
+ // 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 := ipaddr.ResolveClientIP(ctx)
+ 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..0d42d83
--- /dev/null
+++ b/web/middleware/auth/log_list.go
@@ -0,0 +1,97 @@
+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/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/announcement/add", "公告-增加"},
+ {"/api/web/announcement/del", "公告-删除"},
+ {"/api/web/announcement/update", "公告-更新"},
+ {"/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/setFakeWatchTimes", "设置视频假观看次数"},
+ {"/api/web/videocollection/media/option", "修改合集里的视频列表"},
+ {"/api/web/videocollection/media/rank", "修改合集里的视频排序"},
+ {"/api/web/videoinfo/addcoll", "新增一个视频合集"},
+ {"/api/web/actress/add", "添加一个女优信息"},
+ {"/api/web/actress/del", "删除一个女优信息"},
+ {"/api/web/actress/update", "更新一个女优信息"},
+ {"/api/web/user/updatetype", "修改用户类型"},
+ {"/api/web/user/giftcoin", "赠送/扣除用户金币"},
+ {"/api/web/coin/recharge/add", "新增金币充值配置"},
+ {"/api/web/coin/recharge/del", "删除金币充值配置"},
+ {"/api/web/coin/recharge/update", "更新金币充值配置"},
+ {"/api/web/merchant/list", "手动拉取商家列表"},
+ {"/api/web/merchant/detail", "手动拉取商家详情"},
+ {"/api/web/feedback/handle", "处理用户意见反馈"},
+ {"/api/web/contactus/handle", "处理联系我们"},
+ {"/api/web/home/banner/add", "首页banner-新增"},
+ {"/api/web/home/banner/update", "首页banner-更新"},
+ {"/api/web/home/banner/del", "首页banner-删除"},
+ // 短剧
+ {"/api/web/playlet/add", "短剧-新增"},
+ {"/api/web/playlet/update", "短剧-更新"},
+ {"/api/web/playlet/status/set", "短剧-设置上下架"},
+ {"/api/web/playlet/recommend/set", "短剧-设置推荐"},
+ {"/api/web/playlet/featured/set", "短剧-设置爆剧"},
+ {"/api/web/playlet/operate-type/set", "短剧-设置运营类型"},
+ {"/api/web/playlet/banner/set", "短剧-设置banner展示"},
+ {"/api/web/playlet/theme/set", "短剧-设置题材"},
+ {"/api/web/playlet/theme/recommend/set", "短剧题材-设置推荐"},
+ {"/api/web/playlet/category/set", "短剧-设置分类"},
+ {"/api/web/playlet/tag/set", "短剧-设置标签"},
+ {"/api/web/playlet/pay-type/set", "短剧-设置支付方式"},
+ {"/api/web/playlet/watermark/reset", "短剧-重置水印"},
+ {"/api/web/playlet/slug/sync", "短剧-同步历史slug"},
+ {"/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/theme/add", "短剧题材-新增"},
+ {"/api/web/playlet/theme/update", "短剧题材-更新"},
+ {"/api/web/playlet/theme/del", "短剧题材-删除"},
+ {"/api/web/playlet/category/add", "短剧分类-新增"},
+ {"/api/web/playlet/category/update", "短剧分类-更新"},
+ {"/api/web/playlet/category/del", "短剧分类-删除"},
+ {"/api/web/playlet/tag/add", "短剧标签-新增"},
+ {"/api/web/playlet/tag/update", "短剧标签-更新"},
+ {"/api/web/playlet/tag/del", "短剧标签-删除"},
+ {"/api/web/resource/import", "短剧媒资-入库"},
+ {"/api/web/card/order/district/sync", "充值订单-同步历史渠道码"},
+}
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..5f1c685
--- /dev/null
+++ b/web/middleware/ipblacklist/ipblacklist.go
@@ -0,0 +1,18 @@
+package ipblacklist
+
+import (
+ "errors"
+ "net/http"
+ "videoweb/pub/common/ipaddr"
+ "videoweb/web/service/srv_ipblack"
+
+ "github.com/gin-gonic/gin"
+)
+
+func IpBlackListMiddleware(ctx *gin.Context) {
+ ip := ipaddr.ResolveClientIP(ctx)
+ if srv_ipblack.CheckIp(ip) {
+ ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
+ return
+ }
+}
diff --git a/web/middleware/limitHandler/login.go b/web/middleware/limitHandler/login.go
new file mode 100644
index 0000000..48053fa
--- /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(
+ "[91猎艳]\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/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/ather.go b/web/router/ather.go
new file mode 100644
index 0000000..4409320
--- /dev/null
+++ b/web/router/ather.go
@@ -0,0 +1,22 @@
+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)
+ }
+
+ stat := router.Group("/stat")
+ {
+ stat.GET("/analytics", webanalytics.GetAnalyticsStat)
+ stat.GET("/daily/analytics", webanalytics.GetAnalyticsDailyStat)
+ }
+}
diff --git a/web/router/card.go b/web/router/card.go
new file mode 100644
index 0000000..aa94290
--- /dev/null
+++ b/web/router/card.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/web/api/web_card"
+
+ "github.com/gin-gonic/gin"
+)
+
+func cardRouter(router *gin.RouterGroup) {
+ card := router.Group("/card")
+ {
+ card.POST("/order/list", web_card.GetOrders)
+ card.POST("/order/district/sync", web_card.SyncDistrictCode)
+ }
+}
diff --git a/web/router/coin.go b/web/router/coin.go
new file mode 100644
index 0000000..3064325
--- /dev/null
+++ b/web/router/coin.go
@@ -0,0 +1,14 @@
+package router
+
+import (
+ "videoweb/web/api/web_coin"
+
+ "github.com/gin-gonic/gin"
+)
+
+func coinRouter(router *gin.RouterGroup) {
+ coin := router.Group("/coin")
+ {
+ coin.POST("/trans", web_coin.GetCoinTrans)
+ }
+}
diff --git a/web/router/commend.go b/web/router/commend.go
new file mode 100644
index 0000000..7b4ada0
--- /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)
+ }
+}
diff --git a/web/router/contactus.go b/web/router/contactus.go
new file mode 100644
index 0000000..39ef148
--- /dev/null
+++ b/web/router/contactus.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/web/api/web_contactus"
+
+ "github.com/gin-gonic/gin"
+)
+
+func contactUsRouter(router *gin.RouterGroup) {
+ group := router.Group("/contactus")
+ {
+ group.GET("/list", web_contactus.List)
+ group.POST("/handle", web_contactus.Handle)
+ }
+}
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/feedback.go b/web/router/feedback.go
new file mode 100644
index 0000000..57a090f
--- /dev/null
+++ b/web/router/feedback.go
@@ -0,0 +1,15 @@
+package router
+
+import (
+ "videoweb/web/api/web_feedback"
+
+ "github.com/gin-gonic/gin"
+)
+
+func feedbackRouter(router *gin.RouterGroup) {
+ group := router.Group("/feedback")
+ {
+ group.GET("/list", web_feedback.List)
+ group.POST("/handle", web_feedback.Handle)
+ }
+}
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..919b5c5
--- /dev/null
+++ b/web/router/gin.go
@@ -0,0 +1,119 @@
+package router
+
+import (
+ "fmt"
+ "net/http"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "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"},
+ 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)
+ richTextContent(rootRouter)
+ commentRouter(rootRouter)
+ ipBlackRouter(rootRouter)
+ detailNavRouter(rootRouter)
+ announcementRouter(rootRouter)
+ cardRouter(rootRouter)
+ coinRouter(rootRouter)
+ rechargePackageRouter(rootRouter)
+ userRouter(rootRouter)
+ translationRouter(rootRouter)
+ mediaResourceRouter(rootRouter)
+ playletRouter(rootRouter)
+ homeBannerRouter(rootRouter)
+ feedbackRouter(rootRouter)
+ contactUsRouter(rootRouter)
+ }
+ // 第三方回调路由(免鉴权,挂在 engine 上而非 /api/web 分组)
+ thirdRouter(router)
+}
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/ipblack.go b/web/router/ipblack.go
new file mode 100644
index 0000000..b633a35
--- /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)
+ }
+}
diff --git a/web/router/mediaresource.go b/web/router/mediaresource.go
new file mode 100644
index 0000000..06ee43d
--- /dev/null
+++ b/web/router/mediaresource.go
@@ -0,0 +1,19 @@
+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("/jh/list", web_mediaresource.ResourceJHList)
+ group.GET("/jh/chapters", web_mediaresource.ResourceJHChapters)
+ group.POST("/jh/import", web_mediaresource.ResourceJHImport)
+ 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..faa6222
--- /dev/null
+++ b/web/router/playlet.go
@@ -0,0 +1,49 @@
+package router
+
+import (
+ "videoweb/web/api/web_playlet"
+ "videoweb/web/api/web_playletcategory"
+
+ "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("/status/set", web_playlet.SetStatus)
+ group.POST("/online-at/fill", web_playlet.FillOnlineAt)
+ group.POST("/recommend/set", web_playlet.SetRecommend)
+ group.POST("/featured/set", web_playlet.SetFeatured)
+ group.POST("/operate-type/set", web_playlet.SetOperateType)
+ group.POST("/banner/set", web_playlet.SetBanner)
+ group.POST("/theme/set", web_playlet.SetTheme)
+ group.POST("/category/set", web_playlet.SetCategory)
+ group.POST("/tag/set", web_playlet.SetTag)
+ group.POST("/like/virtual/set", web_playlet.SetVirtualLike)
+ group.POST("/pay-type/set", web_playlet.SetPayType)
+ group.POST("/watermark/reset", web_playlet.ResetWatermark)
+ group.POST("/slug/sync", web_playlet.SyncSlug)
+ 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/pay-type/set", web_playlet.ChapterSetPayType)
+ group.POST("/chapter/sync", web_playlet.ChapterSync)
+ group.GET("/category/list", web_playletcategory.List)
+ group.POST("/category/add", web_playletcategory.Add)
+ group.POST("/category/update", web_playletcategory.Update)
+ group.POST("/category/del", web_playletcategory.Del)
+ group.GET("/theme/list", web_playlet.ThemeList)
+ group.POST("/theme/add", web_playlet.ThemeAdd)
+ group.POST("/theme/update", web_playlet.ThemeUpdate)
+ group.POST("/theme/recommend/set", web_playlet.SetThemeRecommend)
+ group.POST("/theme/del", web_playlet.ThemeDel)
+ 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)
+ }
+}
diff --git a/web/router/rechargepackage.go b/web/router/rechargepackage.go
new file mode 100644
index 0000000..22acb5b
--- /dev/null
+++ b/web/router/rechargepackage.go
@@ -0,0 +1,19 @@
+package router
+
+import (
+ "videoweb/web/api/web_rechargepackage"
+
+ "github.com/gin-gonic/gin"
+)
+
+func rechargePackageRouter(router *gin.RouterGroup) {
+ group := router.Group("/recharge/package")
+ {
+ group.POST("/list", web_rechargepackage.List)
+ group.POST("/vip/list", web_rechargepackage.VipList)
+ group.POST("/add", web_rechargepackage.Add)
+ group.POST("/update", web_rechargepackage.Update)
+ group.POST("/del", web_rechargepackage.Delete)
+ group.POST("/status/set", web_rechargepackage.SetStatus)
+ }
+}
diff --git a/web/router/richtextcontent.go b/web/router/richtextcontent.go
new file mode 100644
index 0000000..45d704e
--- /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)
+ }
+}
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/third.go b/web/router/third.go
new file mode 100644
index 0000000..82d5947
--- /dev/null
+++ b/web/router/third.go
@@ -0,0 +1,17 @@
+package router
+
+import (
+ "videoweb/web/api/web_recharge"
+
+ "github.com/gin-gonic/gin"
+)
+
+func thirdRouter(r *gin.Engine) {
+ thirdGroup := r.Group("/api/web/third")
+ {
+ paycenterGroup := thirdGroup.Group("/paycenter")
+ {
+ paycenterGroup.POST("/bill/callback", web_recharge.PayCenterBillCallback)
+ }
+ }
+}
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..6e684cd
--- /dev/null
+++ b/web/router/user.go
@@ -0,0 +1,17 @@
+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)
+ user.POST("/giftcoin", web_user.GiftCoin)
+ user.POST("/vip/update", web_user.UpdateVip)
+ }
+}
diff --git a/web/router/video.go b/web/router/video.go
new file mode 100644
index 0000000..eecb287
--- /dev/null
+++ b/web/router/video.go
@@ -0,0 +1,56 @@
+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("/addcoll", web_video.AddOneVideoCollection)
+ advitise.POST("/setFakeWatchTimes", web_video.SetVideoFakeWatchTimes)
+ advitise.POST("/transcode", web_video.TranscodeVideo)
+ }
+ 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)
+ }
+ videoTopic := router.Group("/videotopic")
+ {
+ videoTopic.POST("/list", web_video.GetVideoTopic)
+ videoTopic.POST("/update", web_video.UpdateOneVideoTopic)
+ videoTopic.POST("/del", web_video.DelOneVideoTopic)
+ videoTopic.POST("/add", web_video.AddOneVideoTopic)
+ }
+
+ router.GET("/m3u8/*key", web_video.Play)
+ router.GET("/transm3u8/*key", web_video.TranscodeM3u8)
+ router.GET("/enkey", web_video.Enkey)
+ router.GET("/newenkey", web_video.NewEnkey)
+ router.GET("/lsjenkey", web_video.LsjEnkey)
+ router.GET("/videotag/hottag", web_video.GetVideoPlayData)
+}
diff --git a/web/router/wbvideodata.go b/web/router/wbvideodata.go
new file mode 100644
index 0000000..323a0f3
--- /dev/null
+++ b/web/router/wbvideodata.go
@@ -0,0 +1,20 @@
+package router
+
+import (
+ "videoweb/web/api/web_recharge"
+ "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/hgdj")
+ {
+ router.Use(limiter.Middleware())
+ //router.GET("/hottag", web_video.GetHotTagByPlayData)
+ router.GET("/ipstat", web_video.GetIpStatData)
+ router.GET("/recharge/summary", web_recharge.RechargeSummary)
+ }
+}
diff --git a/web/server/server.go b/web/server/server.go
new file mode 100644
index 0000000..f2ced4d
--- /dev/null
+++ b/web/server/server.go
@@ -0,0 +1,201 @@
+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/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.BucketVideoAddressV2)
+ initLaosiji()
+ mongos.StartLid()
+ initData()
+ 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 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..1fed794
--- /dev/null
+++ b/web/service/services.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "videoweb/web/service/srv_ipblack"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/skd"
+)
+
+func Start() {
+ skd.Start()
+ srv_ipblack.Init()
+ srv_mediaresource.Start()
+}
+
+func Stop() {
+ srv_mediaresource.Stop()
+ skd.Stop()
+}
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_announcement/announcement.go b/web/service/srv_announcement/announcement.go
new file mode 100644
index 0000000..14a900d
--- /dev/null
+++ b/web/service/srv_announcement/announcement.go
@@ -0,0 +1,101 @@
+package srv_announcement
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/announcementmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func GetAnnouncementList(ctx context.Context, req *GetAnnouncementListReq) (*GetAnnouncementListResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.Title != nil && *req.Title != "" {
+ filter[announcementmod.FN.Announcement.Title] = bson.M{"$regex": *req.Title, "$options": "i"}
+ }
+ if req.Status != nil {
+ filter[announcementmod.FN.Announcement.Status] = *req.Status
+ }
+
+ count, serr := announcementmod.CountAnnouncement(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ if count == 0 {
+ return &GetAnnouncementListResp{
+ Total: 0,
+ List: make(announcementmod.AnnouncementList, 0),
+ }, nil
+ }
+
+ opts := options.FindOptions{}
+ opts.SetSort(bson.D{
+ {Key: announcementmod.FN.Announcement.Sort, Value: -1},
+ {Key: announcementmod.FN.Announcement.CreatedAt, Value: -1},
+ })
+ opts.SetLimit(req.PageSize)
+ opts.SetSkip((req.PageNum - 1) * req.PageSize)
+
+ list, serr := announcementmod.FindManyAnnouncement(ctx, filter, &opts)
+ if serr != nil {
+ return nil, serr
+ }
+
+ return &GetAnnouncementListResp{
+ Total: count,
+ List: list,
+ }, nil
+}
+
+func AddAnnouncement(ctx context.Context, req *AddAnnouncementReq) *srvdef.SrvErr {
+ now := time.Now()
+ status := true
+ if req.Status != nil {
+ status = *req.Status
+ }
+ doc := &announcementmod.Announcement{
+ Id: mongos.Lid.Announcement.Get(),
+ Title: req.Title,
+ Content: req.Content,
+ Sort: req.Sort,
+ Status: status,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ return announcementmod.InsertOneAnnouncement(ctx, doc)
+}
+
+func UpdateAnnouncement(ctx context.Context, req *UpdateAnnouncementReq) *srvdef.SrvErr {
+ filter := bson.M{announcementmod.FN.Announcement.Id: req.Id}
+ updateFields := bson.M{
+ announcementmod.FN.Announcement.UpdatedAt: time.Now(),
+ }
+ if req.Param.Title != nil {
+ updateFields[announcementmod.FN.Announcement.Title] = *req.Param.Title
+ }
+ if req.Param.Content != nil {
+ updateFields[announcementmod.FN.Announcement.Content] = *req.Param.Content
+ }
+ if req.Param.Sort != nil {
+ updateFields[announcementmod.FN.Announcement.Sort] = *req.Param.Sort
+ }
+ if req.Param.Status != nil {
+ updateFields[announcementmod.FN.Announcement.Status] = *req.Param.Status
+ }
+ return announcementmod.UpdateOneAnnouncement(ctx, filter, bson.M{"$set": updateFields})
+}
+
+func DelAnnouncement(ctx context.Context, req *DelAnnouncementReq) *srvdef.SrvErr {
+ filter := bson.M{announcementmod.FN.Announcement.Id: req.Id}
+ count, serr := announcementmod.DelOneAnnouncement(ctx, filter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrAnnouncementNotExsit, "", "")
+ }
+ return nil
+}
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..b9a314d
--- /dev/null
+++ b/web/service/srv_app/app.go
@@ -0,0 +1,43 @@
+package srv_app
+
+import (
+ "time"
+)
+
+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"` // 排序
+}
diff --git a/web/service/srv_cfg/cfg.go b/web/service/srv_cfg/cfg.go
new file mode 100644
index 0000000..99a5748
--- /dev/null
+++ b/web/service/srv_cfg/cfg.go
@@ -0,0 +1,60 @@
+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
+}
diff --git a/web/service/srv_cfg/define.go b/web/service/srv_cfg/define.go
new file mode 100644
index 0000000..6db85ac
--- /dev/null
+++ b/web/service/srv_cfg/define.go
@@ -0,0 +1,90 @@
+package srv_cfg
+
+import (
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type CDNNode struct {
+ Url *string `json:"url" bson:"url,omitempty"`
+ Ratio *int `json:"ratio" bson:"ratio,omitempty"`
+}
+type SysCfgUpdate struct {
+ UnencryptedCdn *string `json:"unencryptedCdn" bson:"unencryptedCdn,omitempty"` // 不加密资源使用的cdn
+ SitemapDefaultImages *[]string `json:"sitemapDefaultImages" bson:"sitemapDefaultImages,omitempty"` // Sitemap里面默认封面图
+ PdfAddr *string `json:"pdfAddr" bson:"pdfAddr,omitempty"` // 回家的路PDF
+ ImgAppCdn *string `json:"imgAppCdn" bson:"imgAppCdn,omitempty"` // 图片CDN(app使用)
+ ImgAppUsCdn *string `json:"imgAppUsCdn" bson:"imgAppUsCdn,omitempty"` // 图片CDN(海外app使用)
+ ImgWebCdn *string `json:"imgWebCdn" bson:"imgWebCdn,omitempty"` // 图片CDN(web使用)
+ MediaWebCdn *string `json:"mediaWebCdn" bson:"mediaWebCdn,omitempty"` // 视频CDN(web使用)
+ MediaAppCdn *string `json:"mediaAppCdn" bson:"mediaAppCdn,omitempty"` // 视频CDN(app使用)
+ MediaAppUsCdn *string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn,omitempty"` // 视频CDN(海外app使用)
+ WatermarkImage *string `json:"watermarkImage" bson:"watermarkImage,omitempty"` // 图片水印文件
+ WatermarkMedia *string `json:"watermarkMedia" bson:"watermarkMedia,omitempty"` // 视频水印文件(底部右边)
+ WatermarkMediaTop *string `json:"watermarkMediaTop" bson:"watermarkMediaTop,omitempty"` // 视频水印文件(顶部左边)
+ WatermarkMediaTopRight *string `json:"watermarkMediaTopRight" bson:"watermarkMediaTopRight,omitempty"` // 视频水印文件(顶部右边)
+ WatermarkMediaBottomLeft *string `json:"watermarkMediaBottomLeft" bson:"watermarkMediaBottomLeft,omitempty"` // 视频水印文件(底部左边)
+ Mp4Addr *string `json:"mp4Addr" bson:"mp4Addr,omitempty"` // mp4地址(web使用)
+ CustomerServiceHost *string `json:"customerServiceHost" bson:"customerServiceHost,omitempty"` // 在线客服
+ NewUrlAddr *string `json:"newUrlAddr" bson:"newUrlAddr,omitempty"` //最新地址
+ NewUrlAddr1 *string `json:"newUrlAddr1" bson:"newUrlAddr1,omitempty"` //最新地址(备用地址1)
+ NewUrlAddr2 *string `json:"newUrlAddr2" bson:"newUrlAddr2,omitempty"` //最新地址(备用地址2)
+ ForeverUrlAddr *string `json:"foreverUrlAddr" bson:"foreverUrlAddr,omitempty"` //永久地址
+ GmailAddr *string `json:"gmailAddr" bson:"gmailAddr,omitempty"` //邮箱地址
+ AppAddr *string `json:"appAddr" bson:"appAddr,omitempty"` //APP下载地址(落地页)
+ OfficialAppAddr *string `json:"officialAppAddr" bson:"officialAppAddr,omitempty"` //官网app地址
+ WeiChart *string `json:"weiChart" bson:"weiChart,omitempty"` //微信群
+ QQ *string `json:"qq" bson:"qq,omitempty"` //qq群
+ TgGroup *string `json:"tgGroup" bson:"tgGroup,omitempty"` //tg群
+ PotaoGroup *string `json:"potaoGroup" bson:"potaoGroup,omitempty"` //土豆群
+ GitAddr *string `json:"gitAddr" bson:"gitAddr,omitempty"` //git地址
+ TwitterAddr *string `json:"twitterAddr" bson:"twitterAddr,omitempty"` //推特地址
+ Telegram *string `json:"telegram" bson:"telegram,omitempty"` //商务tg
+ TelegramAdd *string `json:"telegramAdd" bson:"telegramAdd,omitempty"` //加入我们tg
+ TranslationAddr *string `json:"translationAddr" bson:"translationAddr,omitempty"` //应用导航
+ //SEO
+ WebTitle *string `json:"webTitle" bson:"webTitle,omitempty"` // 网站SEO标题
+ WebKeyworld *[]string `json:"webKeyworld" bson:"webKeyworld,omitempty"` // 网站SEO关键字
+ WebDescription *string `json:"webDescription" bson:"webDescription,omitempty"` // 网站SEO描述
+ WebSiteName *string `json:"webSiteName" bson:"webSiteName,omitempty"` // 网站SEO网站名称
+ WebAddr *string `json:"webAddr" bson:"webAddr,omitempty"` // 网站SEO网站地址
+ DHNewUrlAddr1 *string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1,omitempty"` // 导航页-线路一
+ DHNewUrlAddr2 *string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2,omitempty"` // 导航页-线路2
+ DHNewUrlAddr3 *string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3,omitempty"` // 导航页-线路3
+ FreeVPN *string `json:"freeVPN" bson:"freeVPN,omitempty"` // 免费vpn链接
+ UseAddress *[]string `json:"useAddress" bson:"useAddress,omitempty"` // 所有使用中的域名
+ AdvitiesTopicNum *int64 `json:"advitiesTopicNum" bson:"advitiesTopicNum,omitempty"` // 广告间隔
+ DHTitle *string `json:"dhTitle" bson:"dhTitle,omitempty"` // 导航SEO标题
+ DHKeyworld *[]string `json:"dhKeyworld" bson:"dhKeyworld,omitempty"` // 导航SEO关键字
+ DHDescription *string `json:"dhDescription" bson:"dhDescription,omitempty"` // 导航SEO描述
+ DHSiteName *string `json:"dhSiteName" bson:"dhSiteName,omitempty"` // 导航SEO网站名称
+ ActressBackGround *string `json:"actressBackGround" bson:"actressBackGround,omitempty"` // 女优通用背景图
+ ChannelCode *string `json:"channelCode" bson:"channelCode,omitempty"` // 渠道码
+ EnableH265 *bool `json:"enableH265" bson:"enableH265,omitempty"` // 是否允许客户端使用 H265 地址播放
+}
+type SysCfgMapParam struct {
+ Key string `json:"key" bson:"key"` //Key
+ StringVal string `json:"stringVal" bson:"stringVal"` //字符串的值
+ IntVal int64 `json:"intVal" bson:"intVal"` //int64的值
+ Desc string `json:"desc" bson:"desc"` //配置描述
+}
+
+type UpdateSysCfgMapReq struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"` //ID
+ Param *SysCfgMapParam `json:"param" bson:"param"`
+}
+
+type DeleteSysCfgMapReq struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"` //ID
+}
+
+type GetSysCfgMapReq struct {
+ Key *string `json:"key" bson:"key"`
+ Desc *string `json:"desc" bson:"desc"`
+}
+
+type FloatingWindow struct {
+ Enable bool `json:"enable" bson:"enable"`
+ Image string `json:"image" bson:"image"` //浮窗图片
+ Url string `json:"url" bson:"url"` //浮窗URL
+ ImageClose string `json:"imageClose" bson:"imageClose"` //浮窗图片
+}
diff --git a/web/service/srv_cointrans/define.go b/web/service/srv_cointrans/define.go
new file mode 100644
index 0000000..7debf5e
--- /dev/null
+++ b/web/service/srv_cointrans/define.go
@@ -0,0 +1,26 @@
+package srv_cointrans
+
+import (
+ "time"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/pubdef"
+)
+
+type CoinTransReq struct {
+ pubdef.Page
+ UserId *int64 `json:"userId"` // 用户ID
+ FlowDir *cointransmod.FlowDir `json:"flowDir"` // 收支方向:1收入 -1支出
+ TranType *cointransmod.TranType `json:"tranType"` // 交易类型
+ StartTime *string `json:"startTime"` // 开始时间 2006-01-02 15:04:05
+ EndTime *string `json:"endTime"` // 结束时间 2006-01-02 15:04:05
+}
+
+type CoinTransResp struct {
+ Total int64 `json:"total"`
+ List cointransmod.CoinTransList `json:"list"`
+}
+
+func parseTime(s string) (time.Time, bool) {
+ t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
+ return t, err == nil
+}
diff --git a/web/service/srv_cointrans/trans.go b/web/service/srv_cointrans/trans.go
new file mode 100644
index 0000000..98dd5e5
--- /dev/null
+++ b/web/service/srv_cointrans/trans.go
@@ -0,0 +1,62 @@
+package srv_cointrans
+
+import (
+ "context"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/cointransmod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetCoinTrans 分页查询金币交易流水
+func GetCoinTrans(ctx context.Context, req *CoinTransReq) (*CoinTransResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+
+ if req.UserId != nil && *req.UserId > 0 {
+ filter[cointransmod.FN.CoinTrans.UserId] = *req.UserId
+ }
+ if req.FlowDir != nil {
+ filter[cointransmod.FN.CoinTrans.FlowDir] = *req.FlowDir
+ }
+ if req.TranType != nil {
+ filter[cointransmod.FN.CoinTrans.TranType] = *req.TranType
+ }
+
+ timeFilter := bson.M{}
+ if req.StartTime != nil && *req.StartTime != "" {
+ if t, ok := parseTime(*req.StartTime); ok {
+ timeFilter["$gte"] = t
+ }
+ }
+ if req.EndTime != nil && *req.EndTime != "" {
+ if t, ok := parseTime(*req.EndTime); ok {
+ timeFilter["$lte"] = t
+ }
+ }
+ if len(timeFilter) > 0 {
+ filter[cointransmod.FN.CoinTrans.CreatedAt] = timeFilter
+ }
+
+ total, serr := cointransmod.CountCoinTrans(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ resp := &CoinTransResp{Total: total}
+ if total == 0 {
+ resp.List = cointransmod.CoinTransList{}
+ return resp, nil
+ }
+
+ opt := options.Find().
+ SetSort(bson.D{{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1}}).
+ SetSkip((req.PageNum - 1) * req.PageSize).
+ SetLimit(req.PageSize)
+
+ list, serr := cointransmod.FindManyCoinTrans(ctx, filter, opt)
+ if serr != nil {
+ return nil, serr
+ }
+ resp.List = list
+ return resp, nil
+}
diff --git a/web/service/srv_commend/commend.go b/web/service/srv_commend/commend.go
new file mode 100644
index 0000000..f3fd3c7
--- /dev/null
+++ b/web/service/srv_commend/commend.go
@@ -0,0 +1,411 @@
+package srv_commend
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ commentObjectTypePlaylet int64 = 10 // 短剧评论
+ commentObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
+ commentReviewPass int = 1 // 审核通过
+ commentLevelFirst int64 = 1 // 一级评论
+)
+
+// SearchComandList 查询后台评论列表
+func SearchComandList(ctx context.Context, param *CommendSearch) (*CommendSearchResp, *srvdef.SrvErr) {
+
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+
+ // objectId 只表示业务对象ID,必须配合 objectType 才能准确区分短剧评论和剧集评论,避免不同集合ID碰撞。
+ if param.ObjectId != nil {
+ filter[commentmod.FN.Comment.ObjectId] = *param.ObjectId
+ }
+ if param.ObjectType != nil {
+ filter[commentmod.FN.Comment.ObjectType] = *param.ObjectType
+ }
+ if param.UserId != nil && *param.UserId > 0 {
+ filter[commentmod.FN.Comment.UserId] = *param.UserId
+ }
+
+ if param.Level != nil {
+ filter[commentmod.FN.Comment.Level] = *param.Level
+ }
+
+ if param.UserName != nil {
+ filter[commentmod.FN.Comment.UserName] = bson.M{"$regex": *param.UserName}
+ }
+ if param.Text != nil {
+ filter[commentmod.FN.Comment.Text] = bson.M{"$regex": *param.Text}
+ }
+
+ if param.ReviewStatus != nil {
+ filter[commentmod.FN.Comment.ReviewStatus] = *param.ReviewStatus
+ }
+
+ // 置顶评论优先展示,同一置顶级别下按发布时间倒序,保证后台审核和运营处理时优先看到最新评论。
+ sort := bson.D{
+ {commentmod.FN.Comment.TopRank, -1},
+ {commentmod.FN.Comment.CreatedAt, -1},
+ }
+
+ opt.SetSort(sort)
+ skip := (param.PageNum - 1) * param.PageSize
+ opt.SetSkip(skip)
+ opt.SetLimit(param.PageSize)
+
+ total, serr := commentmod.CountComments(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ if total == 0 {
+ return &CommendSearchResp{}, nil
+ }
+
+ commends, serr := commentmod.FindManyComment(ctx, filter, opt)
+ if serr != nil {
+ return nil, serr
+ }
+
+ resp := &CommendSearchResp{
+ Total: total,
+ List: make([]*PostCommentInfo, 0),
+ }
+
+ // countLike 是短剧评论点赞数的新字段,likes 保留旧字段兼容后台旧页面读取。
+ for _, commendInfo := range commends {
+ tmp := &PostCommentInfo{
+ Id: commendInfo.Id,
+ ObjectId: commendInfo.ObjectId,
+ ObjectType: commendInfo.ObjectType,
+ ObjectName: commendInfo.ObjectName,
+ UserId: commendInfo.UserId,
+ UserName: commendInfo.UserName,
+ UserAvatar: commendInfo.UserAvatar,
+ Level: commendInfo.Level,
+ ParentsId: commendInfo.ParentsId,
+ ReplyCommentId: commendInfo.ReplyCommentId,
+ ReplyUserName: commendInfo.ReplyUserName,
+ Text: commendInfo.Text,
+ ReviewStatus: commendInfo.ReviewStatus,
+ UpdatedAt: commendInfo.UpdatedAt,
+ Ip: commendInfo.Ip,
+ CreatedAt: commendInfo.CreatedAt,
+ CountLike: commendInfo.CountLike,
+ Likes: commendInfo.CountLike,
+ }
+ resp.List = append(resp.List, tmp)
+ }
+
+ return resp, nil
+}
+
+// DelCommend 删除一条评论
+func DelCommend(ctx context.Context, commendId int64) *srvdef.SrvErr {
+ // 先查出评论旧状态,再删除;后续扣减评论数需要依赖旧的 reviewStatus、level、objectType。
+ commendInfo, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId})
+ if serr != nil {
+ return serr
+ }
+ _, serr = commentmod.DelOneComment(ctx, bson.M{commentmod.FN.Comment.Id: commendId})
+ if serr != nil {
+ return serr
+ }
+
+ // 删除后按当前已审核通过评论数重算计数,避免历史增量失败或重复操作导致 countComment 不准。
+ if serr = refreshCommentCountByComment(ctx, commendInfo); serr != nil {
+ return serr
+ }
+
+ return nil
+}
+
+// DelCommend 删除用户所有评论
+func DelUserCommend(ctx context.Context, ip string) *srvdef.SrvErr {
+ // 批量删除前先查出全部评论,删除后才能按每条评论的旧状态补偿父评论或短剧/剧集评论数。
+ commendList, serr := commentmod.FindManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip})
+ if serr != nil {
+ return serr
+ }
+ _, serr = commentmod.DelManyComment(ctx, bson.M{commentmod.FN.Comment.Ip: ip})
+ if serr != nil {
+ return serr
+ }
+
+ commendId := make([]int64, 0)
+ // 删除用户全部评论后逐条重算受影响对象的计数;重复刷新同一对象只会 set 成相同值,不会重复扣减。
+ for _, commendInfo := range commendList {
+ if serr = refreshCommentCountByComment(ctx, commendInfo); serr != nil {
+ return serr
+ }
+ commendId = append(commendId, commendInfo.Id)
+ }
+ return nil
+}
+
+// AddPostComment 发布评论
+func AddPostComment(ctx context.Context, req *CommendReplay) *srvdef.SrvErr {
+ now := time.Now()
+ if req.Score > 10 {
+ log.Error("后台回复评论参数错误", log.Any("评分", req.Score))
+ return srvdef.New(srvdef.ErrParamError, "", "")
+ }
+
+ // 后台回复评论沿用旧的帖子评论逻辑,默认审核通过;短剧/剧集评论由 app 评论入口写入。
+ replyCommentId := int64(0)
+ if req.ParentsId > 0 {
+ replyCommentId = req.ReplyCommentId
+ if replyCommentId <= 0 {
+ replyCommentId = req.ParentsId
+ }
+ }
+ commentInfo := &commentmod.CommentModel{
+ Id: mongos.Lid.Comment.Get(),
+ ObjectId: req.ObjectId,
+ UserName: req.UserName,
+ ParentsId: req.ParentsId,
+ ReplyCommentId: replyCommentId,
+ Text: req.Text,
+ Level: 1,
+ UpdatedAt: now,
+ CreatedAt: now,
+ }
+
+ if commentInfo.ParentsId > 0 {
+ commentInfo.Level = 2
+ } else {
+ commentInfo.Level = 1
+ }
+ commentInfo.ReviewStatus = 1 // 管理端的默认审核通过
+
+ // 插入回复评论成功后,再异步增加父评论回复数,避免回复失败时污染父评论计数。
+ serr := commentmod.InsertOneComment(ctx, commentInfo)
+ if serr != nil {
+ log.Error("后台回复评论入库失败", log.Any("请求参数", req), log.Any("err", serr))
+ return serr
+ }
+
+ go func() {
+ if req.ParentsId > 0 {
+ filter := bson.M{
+ commentmod.FN.Comment.Id: req.ParentsId,
+ }
+ updateDoc := bson.M{
+ "$inc": bson.M{
+ commentmod.FN.Comment.CommentCount: 1,
+ },
+ }
+ serr = commentmod.UpdateOneComment(ctx, filter, updateDoc)
+ if serr != nil {
+ log.Error("后台回复评论更新父评论回复数失败", log.Any("请求参数", req), log.Any("err", serr))
+ return
+ }
+ }
+
+ }()
+ return nil
+}
+
+// 置顶评论
+func TopPostComment(ctx context.Context, id int64) *srvdef.SrvErr {
+ // 置顶排序采用递增 topRank,新的置顶评论排在当前最高 topRank 前面。
+ filter := bson.M{
+ commentmod.FN.Comment.Id: id,
+ }
+ comment, serr := commentmod.FindOneComment(ctx, filter)
+ if serr != nil {
+ return serr
+ }
+ // 获取当前对象下已置顶排序最高的一条评论,用于生成新的 topRank。
+ topRankComment, serr := getTopRankComment(ctx, comment.ObjectId)
+ if serr != nil {
+ return serr
+ }
+ if topRankComment.Id == id && topRankComment.TopRank != 0 {
+ return nil
+ }
+ serr = commentmod.UpdateOneComment(ctx, filter, bson.M{
+ "$set": bson.M{
+ commentmod.FN.Comment.TopRank: topRankComment.TopRank + 1,
+ },
+ })
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+func getTopRankComment(ctx context.Context, objectId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
+ // 只按 objectId 查是历史逻辑;短剧/剧集评论如果后续支持置顶,建议再补 objectType 避免ID碰撞。
+ filter := bson.M{
+ commentmod.FN.Comment.ObjectId: objectId,
+ }
+ opts := options.FindOne().SetSort(bson.M{
+ commentmod.FN.Comment.TopRank: -1,
+ })
+ comment, serr := commentmod.FindOneComment(ctx, filter, opts)
+ if serr != nil {
+ return nil, serr
+ }
+ return comment, nil
+}
+
+func ReviewComment(ctx context.Context, id int64, reviewStatus int) *srvdef.SrvErr {
+ // 审核前先读取评论旧状态,用旧状态和新状态判断是否需要同步评论计数。
+ comment, serr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: id})
+ if serr != nil {
+ return serr
+ }
+ updateDoc := bson.M{
+ commentmod.FN.Comment.ReviewStatus: reviewStatus,
+ }
+ filter := bson.M{
+ commentmod.FN.Comment.Id: id,
+ }
+ serr = commentmod.UpdateOneComment(ctx, filter, bson.M{"$set": updateDoc})
+ if serr != nil {
+ log.Error("审核评论失败", log.Any("评论ID", id), log.Any("审核状态", reviewStatus), log.Any("err", serr))
+ return serr
+ }
+ comment.ReviewStatus = reviewStatus
+ // 审核后按当前已通过评论数重算,重复审核同一状态也能修正历史漏加或漏减的 countComment。
+ if serr = refreshCommentCountByComment(ctx, comment); serr != nil {
+ return serr
+ }
+
+ return nil
+}
+
+// ReviewCommendBatch 批量更新
+func ReviewCommendBatch(ctx context.Context, commendIds *[]int64, reviewStatus int) *srvdef.SrvErr {
+ if commendIds == nil || len(*commendIds) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "评论ID不能为空", "")
+ }
+ // 批量审核也要先查旧数据,统一用每条评论的旧审核状态计算评论数增减。
+ commentList, serr := commentmod.FindManyComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: bson.M{"$in": *commendIds},
+ })
+ if serr != nil {
+ return serr
+ }
+ updateDoc := bson.M{
+ commentmod.FN.Comment.ReviewStatus: reviewStatus,
+ }
+ filter := bson.M{commentmod.FN.Comment.Id: bson.M{"$in": *commendIds}}
+ serr = commentmod.UpdateManyComment(ctx, filter, bson.M{"$set": updateDoc})
+ if serr != nil {
+ log.Error("批量审核评论失败", log.Any("评论ID集合", *commendIds), log.Any("审核状态", reviewStatus), log.Any("err", serr))
+ return serr
+ }
+ // 审核状态更新成功后再按对象重算计数,避免状态未更新成功时提前改动短剧/剧集评论数。
+ for _, comment := range commentList {
+ if comment == nil {
+ continue
+ }
+ if serr = refreshCommentCountByComment(ctx, comment); serr != nil {
+ return serr
+ }
+ }
+
+ return nil
+}
+
+func refreshCommentCountByComment(ctx context.Context, comment *commentmod.CommentModel) *srvdef.SrvErr {
+ // 根据评论归属重算对应计数:二级评论刷新父评论回复数,一级短剧/剧集评论刷新业务对象评论数。
+ if comment == nil {
+ return nil
+ }
+ if comment.Level > commentLevelFirst && comment.ParentsId > 0 {
+ return refreshParentCommentCount(ctx, comment.ParentsId)
+ }
+ if isPlayletCommentType(comment.ObjectType) {
+ return refreshCommentTargetCount(ctx, comment.ObjectType, comment.ObjectId)
+ }
+ return nil
+}
+
+func refreshParentCommentCount(ctx context.Context, parentId int64) *srvdef.SrvErr {
+ // 父评论回复数以当前审核通过的子评论数量为准,直接 set 可以修正历史增量不一致问题。
+ if parentId <= 0 {
+ return nil
+ }
+ count, serr := commentmod.CountComments(ctx, bson.M{
+ commentmod.FN.Comment.ParentsId: parentId,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ })
+ if serr != nil {
+ log.Error("统计父评论回复数失败", log.Any("父评论ID", parentId), log.E(serr))
+ return serr
+ }
+ serr = commentmod.UpdateOneComment(ctx, bson.M{
+ commentmod.FN.Comment.Id: parentId,
+ }, bson.M{
+ "$set": bson.M{commentmod.FN.Comment.CommentCount: count},
+ })
+ if serr != nil {
+ if serr.Code == srvdef.ErrNoData {
+ log.Warn("父评论不存在,跳过刷新回复数", log.Any("父评论ID", parentId), log.Any("回复数", count))
+ return nil
+ }
+ log.Error("刷新父评论回复数失败", log.Any("父评论ID", parentId), log.Any("回复数", count), log.E(serr))
+ return serr
+ }
+ return nil
+}
+
+func refreshCommentTargetCount(ctx context.Context, objectType, objectId int64) *srvdef.SrvErr {
+ // 短剧/剧集评论数以当前审核通过的一级评论数量为准,审核通过、驳回和删除都走同一套重算逻辑。
+ count, serr := commentmod.CountComments(ctx, bson.M{
+ commentmod.FN.Comment.ObjectId: objectId,
+ commentmod.FN.Comment.ObjectType: objectType,
+ commentmod.FN.Comment.ReviewStatus: commentReviewPass,
+ commentmod.FN.Comment.ParentsId: 0,
+ })
+ if serr != nil {
+ log.Error("统计短剧评论数失败", log.Any("对象类型", objectType), log.Any("对象ID", objectId), log.E(serr))
+ return serr
+ }
+ return setCommentTargetCount(ctx, objectType, objectId, count)
+}
+
+func setCommentTargetCount(ctx context.Context, objectType, objectId int64, count int64) *srvdef.SrvErr {
+ // 根据对象类型把重算后的评论数写回短剧或剧集集合。
+ switch objectType {
+ case commentObjectTypePlaylet:
+ serr := playletmod.UpdateOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: objectId,
+ }, bson.M{
+ "$set": bson.M{playletmod.FN.Playlet.CountComment: count},
+ })
+ if serr != nil {
+ log.Error("刷新短剧评论数失败", log.Any("短剧ID", objectId), log.Any("评论数", count), log.E(serr))
+ return serr
+ }
+ case commentObjectTypePlayletChapter:
+ serr := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
+ playletchaptermod.FN.PlayletChapter.Id: objectId,
+ }, bson.M{
+ "$set": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: count},
+ })
+ if serr != nil {
+ log.Error("刷新剧集评论数失败", log.Any("剧集ID", objectId), log.Any("评论数", count), log.E(serr))
+ return serr
+ }
+ }
+ return nil
+}
+
+func isPlayletCommentType(objectType int64) bool {
+ // 当前只有 10 和 11 两类评论会影响短剧业务计数,其他历史评论类型不参与本次同步逻辑。
+ return objectType == commentObjectTypePlaylet || objectType == commentObjectTypePlayletChapter
+}
diff --git a/web/service/srv_commend/define.go b/web/service/srv_commend/define.go
new file mode 100644
index 0000000..c80aba9
--- /dev/null
+++ b/web/service/srv_commend/define.go
@@ -0,0 +1,79 @@
+package srv_commend
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// CommendSearch 查询评论
+type CommendSearch struct {
+ ObjectId *int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectType *int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集
+ UserId *int64 `json:"userId"` // 评论用户ID
+ UserName *string `json:"userName" bson:"userName"` // 用户名称
+ Level *int64 `json:"level" bson:"level"` // 评论级别
+ Text *string `json:"text" bson:"text"` // 评论内容
+ ReviewStatus *int `json:"reviewStatus"`
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+}
+
+type PostCommentInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+ Ip string `json:"ip" bson:"ip"` // 评论IP
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectType int64 `json:"objectType" bson:"objectType"` // 对象类型 10-短剧 11-短剧剧集
+ ObjectName string `json:"objectName" bson:"objectName"` // 对象名
+ UserId int64 `json:"userId" bson:"userId"` // 评论用户ID
+ UserName string `json:"userName" bson:"userName"` // 用户名称
+ UserAvatar string `json:"userAvatar" bson:"userAvatar"` // 评论用户头像
+ Level int64 `json:"level" bson:"level"` // 评论级别
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ ReplyCommentId int64 `json:"replyCommentId" bson:"replyCommentId"` // 被回复评论ID
+ ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
+ Text string `json:"text" bson:"text"` // 评论内容
+ ReviewStatus int `json:"reviewStatus" bson:"reviewStatus"` // 审核状态 0 未审核 1审核成功 2 审核失败
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
+ TopRank int64 `json:"topRank" bson:"topRank"` // 置顶排序
+ Likes int64 `json:"likes" bson:"likes"` // 点赞数
+ CountLike int64 `json:"countLike" bson:"countLike"` // 评论点赞数
+ Status pubdef.UserStatus `json:"status" bson:"status"` // 评论用户的状态
+}
+
+type CommendSearchResp struct {
+ Total int64 `json:"total"`
+ List []*PostCommentInfo `json:"list"`
+}
+
+type CommendDelete struct {
+ Id *int64 `json:"id" bson:"_id" binding:"required"` // Id
+}
+
+type CommendReplay struct {
+ ObjectId int64 `json:"objectId"` // 帖子ID
+ UserName string `json:"userName"` // 用户名
+ ParentsId int64 `json:"parentsId"` // 一级评论ID
+ ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论ID
+ Text string `json:"text"` // 评论内容
+ Score int64 `json:"score"` // 评分
+}
+
+type CommendUpdate struct {
+ Score *int64 `json:"score" bson:"score,omitempty"` // 评分
+ Likes *int64 `json:"likes" bson:"likes,omitempty"` // 点赞数量
+}
+
+type CommendUpdateReq struct {
+ Param CommendUpdate `json:"param" bson:"param"` // 参数
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+}
+
+type CommendTopReq struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+}
+
+type CommendReviewReq struct {
+ Id int64 `json:"id"`
+ ReviewStatus int `json:"reviewStatus"`
+}
diff --git a/web/service/srv_contactus/contactus.go b/web/service/srv_contactus/contactus.go
new file mode 100644
index 0000000..759e450
--- /dev/null
+++ b/web/service/srv_contactus/contactus.go
@@ -0,0 +1,273 @@
+package srv_contactus
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/contactusmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ // contactUsDefaultPageSize web端联系我们列表默认每页数量
+ contactUsDefaultPageSize int64 = 10
+ // contactUsMaxPageSize web端联系我们列表最大每页数量,避免单次请求拉取过多数据
+ contactUsMaxPageSize int64 = 100
+)
+
+var (
+ // contactUsWebProjection 只读取web联系我们列表需要展示的字段,减少MongoDB传输开销。
+ contactUsWebProjection = bson.D{
+ {Key: contactusmod.FN.ContactUs.Id, Value: 1},
+ {Key: contactusmod.FN.ContactUs.UserId, Value: 1},
+ {Key: contactusmod.FN.ContactUs.Email, Value: 1},
+ {Key: contactusmod.FN.ContactUs.QuestionType, Value: 1},
+ {Key: contactusmod.FN.ContactUs.Title, Value: 1},
+ {Key: contactusmod.FN.ContactUs.Content, Value: 1},
+ {Key: contactusmod.FN.ContactUs.Attachments, Value: 1},
+ {Key: contactusmod.FN.ContactUs.Status, Value: 1},
+ {Key: contactusmod.FN.ContactUs.Remark, Value: 1},
+ {Key: contactusmod.FN.ContactUs.CreatedAt, Value: 1},
+ {Key: contactusmod.FN.ContactUs.UpdatedAt, Value: 1},
+ }
+
+ // contactUsWebSort web端联系我们列表按创建时间倒序返回。
+ contactUsWebSort = bson.D{
+ {Key: contactusmod.FN.ContactUs.CreatedAt, Value: -1},
+ {Key: contactusmod.FN.ContactUs.Id, Value: -1},
+ }
+)
+
+// ListContactUs 分页查询联系我们列表
+func ListContactUs(ctx context.Context, req *proto.ContactUsListReq) (*proto.ContactUsListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter, filterErr := buildContactUsListFilter(req)
+ if filterErr != nil {
+ return nil, filterErr
+ }
+ pageNum, pageSize := normalizeContactUsPage(req.PageNum, req.PageSize)
+ totalCount, err := contactusmod.CountContactUs(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.ContactUsListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.ContactUsListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := contactusmod.FindManyContactUs(ctx, filter, options.Find().
+ SetProjection(contactUsWebProjection).
+ SetSort(contactUsWebSort).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ userNameMap, err := buildContactUsUserNameMap(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ resp.List = contactUsListToWebResp(list, userNameMap)
+ return resp, nil
+}
+
+// HandleContactUs 批量更新联系我们处理状态
+func HandleContactUs(ctx context.Context, req *proto.ContactUsHandleReq) (*proto.ContactUsHandleResp, *srvdef.SrvErr) {
+ if req == nil || len(req.Ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ if req.Status != 0 && req.Status != 1 {
+ return nil, srvdef.New(srvdef.ErrParamError, "status无效")
+ }
+ ids, err := normalizeContactUsIds(req.Ids)
+ if err != nil {
+ return nil, err
+ }
+ matchedCount, modifiedCount, err := contactusmod.UpdateManyContactUsWithResult(ctx, bson.M{
+ contactusmod.FN.ContactUs.Id: bson.M{"$in": ids},
+ }, bson.M{
+ "$set": bson.M{
+ contactusmod.FN.ContactUs.Status: req.Status,
+ contactusmod.FN.ContactUs.Remark: strings.TrimSpace(req.Remark),
+ contactusmod.FN.ContactUs.UpdatedAt: time.Now(),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ if matchedCount == 0 {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &proto.ContactUsHandleResp{
+ MatchedCount: matchedCount,
+ ModifiedCount: modifiedCount,
+ }, nil
+}
+
+// buildContactUsListFilter 构造web端联系我们列表查询条件
+func buildContactUsListFilter(req *proto.ContactUsListReq) (bson.M, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.UserId != nil && *req.UserId > 0 {
+ filter[contactusmod.FN.ContactUs.UserId] = *req.UserId
+ }
+ if email := strings.TrimSpace(req.Email); email != "" {
+ filter[contactusmod.FN.ContactUs.Email] = bson.M{"$regex": regexp.QuoteMeta(email)}
+ }
+ if req.Type != nil {
+ if !isValidContactUsType(*req.Type) {
+ return nil, srvdef.New(srvdef.ErrParamError, "type无效")
+ }
+ filter[contactusmod.FN.ContactUs.QuestionType] = *req.Type
+ }
+ if req.Status != nil {
+ if *req.Status != 0 && *req.Status != 1 {
+ return nil, srvdef.New(srvdef.ErrParamError, "status无效")
+ }
+ filter[contactusmod.FN.ContactUs.Status] = *req.Status
+ }
+ createdAt, err := buildContactUsTimeFilter(req.CreateStartTime, req.CreateEndTime)
+ if err != nil {
+ return nil, err
+ }
+ if len(createdAt) > 0 {
+ filter[contactusmod.FN.ContactUs.CreatedAt] = createdAt
+ }
+ return filter, nil
+}
+
+// buildContactUsTimeFilter 按开始时间和结束时间构造createdAt范围查询
+func buildContactUsTimeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
+ filter := bson.M{}
+ startTime = strings.TrimSpace(startTime)
+ endTime = strings.TrimSpace(endTime)
+ if startTime != "" {
+ startAt, err := parseContactUsWebTime(startTime)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "createStartTime格式错误")
+ }
+ filter["$gte"] = startAt
+ }
+ if endTime != "" {
+ endAt, err := parseContactUsWebTime(endTime)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "createEndTime格式错误")
+ }
+ filter["$lte"] = endAt
+ }
+ return filter, nil
+}
+
+func parseContactUsWebTime(value string) (time.Time, error) {
+ return time.ParseInLocation("2006-01-02 15:04:05", value, time.Local)
+}
+
+func normalizeContactUsPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = contactUsDefaultPageSize
+ }
+ if pageSize > contactUsMaxPageSize {
+ pageSize = contactUsMaxPageSize
+ }
+ return pageNum, pageSize
+}
+
+func normalizeContactUsIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
+ ids := make([]int64, 0, len(rawIds))
+ seen := make(map[int64]struct{}, len(rawIds))
+ for _, id := range rawIds {
+ if id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ return ids, nil
+}
+
+func isValidContactUsType(questionType int32) bool {
+ return questionType >= 1 && questionType <= 5
+}
+
+// buildContactUsUserNameMap 批量查询用户名称,避免列表转换时逐条查询用户。
+func buildContactUsUserNameMap(ctx context.Context, list contactusmod.ContactUsList) (map[int64]string, *srvdef.SrvErr) {
+ userIds := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, item := range list {
+ if item == nil || item.UserId <= 0 {
+ continue
+ }
+ if _, ok := seen[item.UserId]; ok {
+ continue
+ }
+ seen[item.UserId] = struct{}{}
+ userIds = append(userIds, item.UserId)
+ }
+ userNameMap := make(map[int64]string, len(userIds))
+ if len(userIds) == 0 {
+ return userNameMap, nil
+ }
+ users, err := usermod.FindManyUser(ctx, bson.M{
+ usermod.FN.User.Id: bson.M{"$in": userIds},
+ }, options.Find().SetProjection(bson.M{
+ usermod.FN.User.Id: 1,
+ usermod.FN.User.NickName: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, user := range users {
+ if user != nil {
+ userNameMap[user.Id] = user.NickName
+ }
+ }
+ return userNameMap, nil
+}
+
+// contactUsListToWebResp 转换联系我们列表响应
+func contactUsListToWebResp(list contactusmod.ContactUsList, userNameMap map[int64]string) []*proto.ContactUsListResp {
+ resp := make([]*proto.ContactUsListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ resp = append(resp, &proto.ContactUsListResp{
+ Id: item.Id,
+ UserId: item.UserId,
+ UserName: userNameMap[item.UserId],
+ Email: item.Email,
+ Type: item.QuestionType,
+ Title: item.Title,
+ Content: item.Content,
+ Attachments: item.Attachments,
+ Status: item.Status,
+ Remark: item.Remark,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ return resp
+}
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_feedback/feedback.go b/web/service/srv_feedback/feedback.go
new file mode 100644
index 0000000..0f6dc3c
--- /dev/null
+++ b/web/service/srv_feedback/feedback.go
@@ -0,0 +1,271 @@
+package srv_feedback
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/feedbackmod"
+ "videoweb/pub/models/usermod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ // feedbackDefaultPageSize web端意见反馈列表默认每页数量
+ feedbackDefaultPageSize int64 = 10
+ // feedbackMaxPageSize web端意见反馈列表最大每页数量,避免单次请求拉取过多数据
+ feedbackMaxPageSize int64 = 100
+)
+
+var (
+ // feedbackWebProjection 只读取web意见反馈列表需要展示的字段,减少MongoDB传输开销。
+ feedbackWebProjection = bson.D{
+ {Key: feedbackmod.FN.Feedback.Id, Value: 1},
+ {Key: feedbackmod.FN.Feedback.UserId, Value: 1},
+ {Key: feedbackmod.FN.Feedback.FeedbackType, Value: 1},
+ {Key: feedbackmod.FN.Feedback.Content, Value: 1},
+ {Key: feedbackmod.FN.Feedback.Screenshots, Value: 1},
+ {Key: feedbackmod.FN.Feedback.Contact, Value: 1},
+ {Key: feedbackmod.FN.Feedback.Status, Value: 1},
+ {Key: feedbackmod.FN.Feedback.Remark, Value: 1},
+ {Key: feedbackmod.FN.Feedback.CreatedAt, Value: 1},
+ {Key: feedbackmod.FN.Feedback.UpdatedAt, Value: 1},
+ }
+
+ // feedbackWebSort web端意见反馈列表按创建时间倒序返回。
+ feedbackWebSort = bson.D{
+ {Key: feedbackmod.FN.Feedback.CreatedAt, Value: -1},
+ {Key: feedbackmod.FN.Feedback.Id, Value: -1},
+ }
+)
+
+// ListFeedback 分页查询意见反馈列表
+func ListFeedback(ctx context.Context, req *proto.FeedbackListReq) (*proto.FeedbackListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter, filterErr := buildFeedbackListFilter(req)
+ if filterErr != nil {
+ return nil, filterErr
+ }
+ pageNum, pageSize := normalizeFeedbackPage(req.PageNum, req.PageSize)
+ totalCount, err := feedbackmod.CountFeedback(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.FeedbackListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.FeedbackListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := feedbackmod.FindManyFeedback(ctx, filter, options.Find().
+ SetProjection(feedbackWebProjection).
+ SetSort(feedbackWebSort).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ userNameMap, err := buildFeedbackUserNameMap(ctx, list)
+ if err != nil {
+ return nil, err
+ }
+ resp.List = feedbackListToWebResp(list, userNameMap)
+ return resp, nil
+}
+
+// HandleFeedback 批量更新意见反馈处理状态
+func HandleFeedback(ctx context.Context, req *proto.FeedbackHandleReq) (*proto.FeedbackHandleResp, *srvdef.SrvErr) {
+ if req == nil || len(req.Ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ if req.Status != 0 && req.Status != 1 {
+ return nil, srvdef.New(srvdef.ErrParamError, "status无效")
+ }
+ ids, err := normalizeFeedbackIds(req.Ids)
+ if err != nil {
+ return nil, err
+ }
+ matchedCount, modifiedCount, err := feedbackmod.UpdateManyFeedbackWithResult(ctx, bson.M{
+ feedbackmod.FN.Feedback.Id: bson.M{"$in": ids},
+ }, bson.M{
+ "$set": bson.M{
+ feedbackmod.FN.Feedback.Status: req.Status,
+ feedbackmod.FN.Feedback.Remark: strings.TrimSpace(req.Remark),
+ feedbackmod.FN.Feedback.UpdatedAt: time.Now(),
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ if matchedCount == 0 {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &proto.FeedbackHandleResp{
+ MatchedCount: matchedCount,
+ ModifiedCount: modifiedCount,
+ }, nil
+}
+
+// buildFeedbackListFilter 构造web端意见反馈列表查询条件
+func buildFeedbackListFilter(req *proto.FeedbackListReq) (bson.M, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.UserId != nil && *req.UserId > 0 {
+ filter[feedbackmod.FN.Feedback.UserId] = *req.UserId
+ }
+ if req.Type != nil {
+ if !isValidFeedbackType(*req.Type) {
+ return nil, srvdef.New(srvdef.ErrParamError, "type无效")
+ }
+ filter[feedbackmod.FN.Feedback.FeedbackType] = *req.Type
+ }
+ if req.Status != nil {
+ if *req.Status != 0 && *req.Status != 1 {
+ return nil, srvdef.New(srvdef.ErrParamError, "status无效")
+ }
+ filter[feedbackmod.FN.Feedback.Status] = *req.Status
+ }
+ createdAt, err := buildFeedbackTimeFilter(req.CreateStartTime, req.CreateEndTime)
+ if err != nil {
+ return nil, err
+ }
+ if len(createdAt) > 0 {
+ filter[feedbackmod.FN.Feedback.CreatedAt] = createdAt
+ }
+ return filter, nil
+}
+
+// buildFeedbackTimeFilter 按开始时间和结束时间构造createdAt范围查询
+func buildFeedbackTimeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
+ filter := bson.M{}
+ startTime = strings.TrimSpace(startTime)
+ endTime = strings.TrimSpace(endTime)
+ if startTime != "" {
+ startAt, err := parseFeedbackWebTime(startTime)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "createStartTime格式错误")
+ }
+ filter["$gte"] = startAt
+ }
+ if endTime != "" {
+ endAt, err := parseFeedbackWebTime(endTime)
+ if err != nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "createEndTime格式错误")
+ }
+ filter["$lte"] = endAt
+ }
+ return filter, nil
+}
+
+func parseFeedbackWebTime(value string) (time.Time, error) {
+ return time.ParseInLocation("2006-01-02 15:04:05", value, time.Local)
+}
+
+func normalizeFeedbackPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = feedbackDefaultPageSize
+ }
+ if pageSize > feedbackMaxPageSize {
+ pageSize = feedbackMaxPageSize
+ }
+ return pageNum, pageSize
+}
+
+func normalizeFeedbackIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
+ ids := make([]int64, 0, len(rawIds))
+ seen := make(map[int64]struct{}, len(rawIds))
+ for _, id := range rawIds {
+ if id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ return ids, nil
+}
+
+func isValidFeedbackType(feedbackType int32) bool {
+ return feedbackType >= 1 && feedbackType <= 5
+}
+
+// buildFeedbackUserNameMap 批量查询反馈用户名称,避免列表转换时逐条查询用户。
+func buildFeedbackUserNameMap(ctx context.Context, list feedbackmod.FeedbackList) (map[int64]string, *srvdef.SrvErr) {
+ userIds := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, item := range list {
+ if item == nil || item.UserId <= 0 {
+ continue
+ }
+ if _, ok := seen[item.UserId]; ok {
+ continue
+ }
+ seen[item.UserId] = struct{}{}
+ userIds = append(userIds, item.UserId)
+ }
+ userNameMap := make(map[int64]string, len(userIds))
+ if len(userIds) == 0 {
+ return userNameMap, nil
+ }
+ users, err := usermod.FindManyUser(ctx, bson.M{
+ usermod.FN.User.Id: bson.M{"$in": userIds},
+ }, options.Find().SetProjection(bson.M{
+ usermod.FN.User.Id: 1,
+ usermod.FN.User.NickName: 1,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, user := range users {
+ if user != nil {
+ userNameMap[user.Id] = user.NickName
+ }
+ }
+ return userNameMap, nil
+}
+
+// feedbackListToWebResp 转换意见反馈列表响应
+func feedbackListToWebResp(list feedbackmod.FeedbackList, userNameMap map[int64]string) []*proto.FeedbackListResp {
+ resp := make([]*proto.FeedbackListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ screenshots := item.Screenshots
+ if screenshots == nil {
+ screenshots = make([]string, 0)
+ }
+ resp = append(resp, &proto.FeedbackListResp{
+ Id: item.Id,
+ UserId: item.UserId,
+ UserName: userNameMap[item.UserId],
+ Type: item.FeedbackType,
+ Content: item.Content,
+ Screenshots: screenshots,
+ Contact: item.Contact,
+ Status: item.Status,
+ Remark: item.Remark,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ return resp
+}
diff --git a/web/service/srv_homebanner/banner.go b/web/service/srv_homebanner/banner.go
new file mode 100644
index 0000000..c387168
--- /dev/null
+++ b/web/service/srv_homebanner/banner.go
@@ -0,0 +1,238 @@
+package srv_homebanner
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/homebannermod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultHomeBannerPageSize int64 = 10
+ maxHomeBannerPageSize int64 = 100
+)
+
+var homeBannerWebProjection = bson.D{
+ {Key: homebannermod.FN.HomeBanner.Id, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Name, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Image, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.VerticalImage, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Url, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Rank, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.Status, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.CreatedById, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.CreatedByName, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.UpdatedByName, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.CreatedAt, Value: 1},
+ {Key: homebannermod.FN.HomeBanner.UpdatedAt, Value: 1},
+}
+
+// ListHomeBanner 分页查询首页 banner 列表
+func ListHomeBanner(ctx context.Context, req *proto.HomeBannerListReq) (*proto.HomeBannerListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[homebannermod.FN.HomeBanner.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[homebannermod.FN.HomeBanner.Status] = *req.Status
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[homebannermod.FN.HomeBanner.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ pageNum, pageSize := normalizeHomeBannerPage(req.PageNum, req.PageSize)
+ totalCount, err := homebannermod.CountHomeBanner(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.HomeBannerListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.HomeBannerListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := homebannermod.FindManyHomeBanner(ctx, filter, options.Find().
+ SetProjection(homeBannerWebProjection).
+ SetSort(bson.D{
+ {Key: homebannermod.FN.HomeBanner.Rank, Value: -1},
+ {Key: homebannermod.FN.HomeBanner.UpdatedAt, Value: -1},
+ {Key: homebannermod.FN.HomeBanner.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.HomeBannerListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.HomeBannerListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Image: item.Image,
+ VerticalImage: item.VerticalImage,
+ Url: item.Url,
+ Rank: item.Rank,
+ Status: item.Status,
+ CreatedById: item.CreatedById,
+ CreatedByName: item.CreatedByName,
+ UpdatedByName: item.UpdatedByName,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddHomeBanner 新增首页 banner
+func AddHomeBanner(ctx context.Context, req *proto.HomeBannerAddReq, operatorId int64) (*proto.HomeBannerAddResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ name := strings.TrimSpace(req.Name)
+ image := strings.TrimSpace(req.Image)
+ if name == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
+ }
+ if image == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "图片不能为空", "")
+ }
+ if req.Status == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "状态不能为空", "")
+ }
+ operatorId, operatorName := resolveHomeBannerOperator(ctx, operatorId)
+ doc := &homebannermod.HomeBanner{
+ Name: name,
+ Image: image,
+ VerticalImage: strings.TrimSpace(req.VerticalImage),
+ Url: strings.TrimSpace(req.Url),
+ Rank: req.Rank,
+ Status: *req.Status,
+ CreatedById: operatorId,
+ CreatedByName: operatorName,
+ UpdatedByName: operatorName,
+ }
+ if err := homebannermod.InsertOneHomeBanner(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearHomeBannerCache()
+ return &proto.HomeBannerAddResp{Id: doc.Id}, nil
+}
+
+// UpdateHomeBanner 更新首页 banner
+func UpdateHomeBanner(ctx context.Context, req *proto.HomeBannerUpdateReq, operatorId int64) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "首页banner ID无效", "")
+ }
+ setDoc := bson.M{homebannermod.FN.HomeBanner.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "名称不能为空", "")
+ }
+ setDoc[homebannermod.FN.HomeBanner.Name] = name
+ }
+ if req.Image != nil {
+ image := strings.TrimSpace(*req.Image)
+ if image == "" {
+ return srvdef.New(srvdef.ErrParamError, "图片不能为空", "")
+ }
+ setDoc[homebannermod.FN.HomeBanner.Image] = image
+ }
+ if req.VerticalImage != nil {
+ setDoc[homebannermod.FN.HomeBanner.VerticalImage] = strings.TrimSpace(*req.VerticalImage)
+ }
+ if req.Url != nil {
+ setDoc[homebannermod.FN.HomeBanner.Url] = strings.TrimSpace(*req.Url)
+ }
+ if req.Rank != nil {
+ setDoc[homebannermod.FN.HomeBanner.Rank] = *req.Rank
+ }
+ if req.Status != nil {
+ setDoc[homebannermod.FN.HomeBanner.Status] = *req.Status
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ _, operatorName := resolveHomeBannerOperator(ctx, operatorId)
+ setDoc[homebannermod.FN.HomeBanner.UpdatedByName] = operatorName
+ if err := homebannermod.UpdateOneHomeBanner(ctx,
+ bson.M{homebannermod.FN.HomeBanner.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearHomeBannerCache()
+ return nil
+}
+
+// DelHomeBanner 删除首页 banner
+func DelHomeBanner(ctx context.Context, req *proto.HomeBannerDelReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "首页banner ID无效", "")
+ }
+ count, err := homebannermod.DelOneHomeBanner(ctx, bson.M{homebannermod.FN.HomeBanner.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ clearHomeBannerCache()
+ return nil
+}
+
+// normalizeHomeBannerPage 规范分页参数,限制单页数量避免大分页拖慢查询
+func normalizeHomeBannerPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultHomeBannerPageSize
+ }
+ if pageSize > maxHomeBannerPageSize {
+ pageSize = maxHomeBannerPageSize
+ }
+ return pageNum, pageSize
+}
+
+// resolveHomeBannerOperator 从后台账号表解析当前操作人名称
+func resolveHomeBannerOperator(ctx context.Context, operatorId int64) (int64, string) {
+ if operatorId <= 0 {
+ return 0, ""
+ }
+ acc, err := mwacc.FindOneAccByFilter(ctx, bson.M{
+ mwacc.FN.MwAcc.Id: operatorId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: mwacc.FN.MwAcc.Id, Value: 1},
+ {Key: mwacc.FN.MwAcc.UserName, Value: 1},
+ }))
+ if err != nil || acc == nil {
+ return operatorId, ""
+ }
+ return acc.Id, acc.UserName
+}
+
+// clearHomeBannerCache 清除短剧首页缓存
+func clearHomeBannerCache() {
+ playletcache.ClearHomeCache()
+}
diff --git a/web/service/srv_ipblack/define.go b/web/service/srv_ipblack/define.go
new file mode 100644
index 0000000..228c65c
--- /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"`
+}
diff --git a/web/service/srv_ipblack/ipblack.go b/web/service/srv_ipblack/ipblack.go
new file mode 100644
index 0000000..b3b47bb
--- /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
+}
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..6767ca2
--- /dev/null
+++ b/web/service/srv_mediaresource/cover_compress.go
@@ -0,0 +1,183 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "videoweb/pub/common/imgutil"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/storage/proxy"
+ "videoweb/web/service/srv_search"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// compressPlayletCoverOrEmpty 下载原封面并按 media Resize 规则处理。
+// 原图未超过 60KB,或原图是 GIF 时,直接返回原封面地址;其它格式超过阈值则压缩为 JPEG 后上传。
+// 失败返回空字符串,不阻断入库。
+func compressPlayletCoverOrEmpty(cover string) string {
+ cover = strings.TrimSpace(cover)
+ if cover == "" {
+ return ""
+ }
+ pathStr, err := uploadCompressedPlayletCover(cover)
+ if err != nil {
+ log.Warn("压缩短剧封面失败", log.Any("cover", cover), log.E(err))
+ return ""
+ }
+ return pathStr
+}
+
+// FillPlayletCompressedCover 为指定短剧补写压缩封面,已有值则跳过。
+func FillPlayletCompressedCover(ctx context.Context, playlet *playletmod.Playlet) bool {
+ if playlet == nil || playlet.Id <= 0 {
+ return false
+ }
+ if strings.TrimSpace(playlet.CompressedCover) != "" {
+ return false
+ }
+ cover := strings.TrimSpace(playlet.Cover)
+ if cover == "" {
+ return false
+ }
+ pathStr := compressPlayletCoverOrEmpty(cover)
+ if pathStr == "" {
+ return false
+ }
+ now := time.Now()
+ if err := playletmod.UpdateOnePlaylet(ctx,
+ bson.M{
+ playletmod.FN.Playlet.Id: playlet.Id,
+ "$or": []bson.M{
+ {playletmod.FN.Playlet.CompressedCover: bson.M{"$exists": false}},
+ {playletmod.FN.Playlet.CompressedCover: ""},
+ {playletmod.FN.Playlet.CompressedCover: nil},
+ },
+ },
+ bson.M{"$set": bson.M{
+ playletmod.FN.Playlet.CompressedCover: pathStr,
+ playletmod.FN.Playlet.UpdatedAt: now,
+ }},
+ ); err != nil {
+ log.Warn("回写短剧压缩封面失败", log.Any("playletId", playlet.Id), log.E(err))
+ return false
+ }
+ playlet.CompressedCover = pathStr
+ playletcache.ClearHomeCache()
+ latest, serr := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playlet.Id})
+ if serr == nil && latest != nil && latest.Status == playletmod.StatusUp {
+ if syncErr := srv_search.SyncPlayletStatusToES(ctx, []int64{playlet.Id}, latest.Status); syncErr != nil {
+ log.Warn("压缩封面后同步ES失败", log.Any("playletId", playlet.Id), log.E(syncErr))
+ }
+ }
+ return true
+}
+
+func uploadCompressedPlayletCover(cover string) (string, error) {
+ downloadURL, err := resolvePlayletCoverURL(cover)
+ if err != nil {
+ return "", err
+ }
+ downloadURL, err = encodeCoverDownloadURL(downloadURL)
+ if err != nil {
+ return "", err
+ }
+ data, err := downloadPlayletCover(downloadURL)
+ if err != nil {
+ return "", err
+ }
+ data = imgutil.DecryptImage(data)
+ if !imgutil.NeedCompress(data) {
+ return cover, nil
+ }
+ fileName := imgutil.SafeCoverFileName(downloadURL)
+ compressed, outName, err := imgutil.CompressCover(data, fileName)
+ if err != nil {
+ return "", err
+ }
+ pathStr, err := proxy.UploadAutopublishImage(outName, compressed)
+ if err != nil {
+ log.Warn("media上传压缩封面失败,回退图床", log.Any("cover", cover), log.E(err))
+ pathStr, err = proxy.UploadImg(outName, compressed, "")
+ if err != nil {
+ return "", fmt.Errorf("上传压缩封面失败: %w", err)
+ }
+ }
+ if strings.TrimSpace(pathStr) == "" {
+ return "", fmt.Errorf("上传压缩封面返回空路径")
+ }
+ return pathStr, nil
+}
+
+func resolvePlayletCoverURL(cover string) (string, error) {
+ if strings.HasPrefix(cover, "http://") || strings.HasPrefix(cover, "https://") {
+ return cover, nil
+ }
+ sysCfg, serr := cfg.FindOneSysCfg(context.Background(), bson.M{cfg.FN.SysCfg.Id: 1})
+ if serr != nil {
+ return "", fmt.Errorf("查询图片CDN失败: %s", serr.Error())
+ }
+ imageDomain := ""
+ if sysCfg != nil {
+ imageDomain = strings.TrimSpace(sysCfg.ImgWebCdn)
+ }
+ if imageDomain == "" {
+ return "", fmt.Errorf("图片CDN未配置,无法下载相对路径封面")
+ }
+ return netutil.MergeURL(imageDomain, cover), nil
+}
+
+// encodeCoverDownloadURL 把路径里的中文等非 ASCII 段转成百分号编码,避免下载失败或二次拼接异常。
+func encodeCoverDownloadURL(raw string) (string, error) {
+ raw = strings.TrimSpace(raw)
+ u, err := url.Parse(raw)
+ if err != nil {
+ return "", fmt.Errorf("解析封面地址失败: %w", err)
+ }
+ if u.Scheme == "" || u.Host == "" {
+ return "", fmt.Errorf("封面地址无效")
+ }
+ escaped := u.EscapedPath()
+ if escaped != "" {
+ if dec, err := url.PathUnescape(escaped); err == nil {
+ u.Path = dec
+ u.RawPath = ""
+ }
+ }
+ return u.String(), nil
+}
+
+func downloadPlayletCover(rawURL string) ([]byte, error) {
+ req, err := http.NewRequest(http.MethodGet, rawURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("下载封面失败: %w", err)
+ }
+ req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; cgdj-playlet-cover/1.0)")
+ req.Header.Set("Accept", "image/*")
+
+ cli := &http.Client{Timeout: 90 * time.Second}
+ resp, err := cli.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("下载封面失败: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("下载封面失败,状态码 %d", resp.StatusCode)
+ }
+ data, err := io.ReadAll(io.LimitReader(resp.Body, 20<<20))
+ if err != nil {
+ return nil, fmt.Errorf("下载封面失败: %w", err)
+ }
+ if len(data) == 0 {
+ return nil, fmt.Errorf("下载封面内容为空")
+ }
+ return data, nil
+}
diff --git a/web/service/srv_mediaresource/import_worker.go b/web/service/srv_mediaresource/import_worker.go
new file mode 100644
index 0000000..48c8798
--- /dev/null
+++ b/web/service/srv_mediaresource/import_worker.go
@@ -0,0 +1,267 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+
+ goredis "github.com/go-redis/redis"
+)
+
+const (
+ playletResourceImportQueueKeyFmt = "playlet:resource:import:queue:%d"
+ playletResourceImportLockKeyFmt = "playlet:resource:import:lock:%d:%d"
+ playletResourceJHImportQueueKeyFmt = "playlet:resource:jh:import:queue:%d"
+ playletResourceJHImportLockKeyFmt = "playlet:resource:jh:import:lock:%d:%d"
+ playletResourceImportLockTTL = 10 * time.Minute
+ playletResourceImportPopWait = 5 * time.Second
+ playletResourceImportRetryWait = time.Second
+)
+
+type playletResourceImportJob struct {
+ Ids []uint `json:"ids"`
+ OperatorId int64 `json:"operatorId"`
+ OperatorName string `json:"operatorName"`
+ CreateTime int64 `json:"createTime"`
+}
+
+var playletResourceImportWorker = struct {
+ mu sync.Mutex
+ cancel context.CancelFunc
+}{}
+
+func playletResourceImportQueueKey() string {
+ return fmt.Sprintf(playletResourceImportQueueKeyFmt, pubdef.AppId)
+}
+
+func playletResourceImportLockKey(mediaId uint) string {
+ return fmt.Sprintf(playletResourceImportLockKeyFmt, pubdef.AppId, mediaId)
+}
+
+func playletResourceJHImportQueueKey() string {
+ return fmt.Sprintf(playletResourceJHImportQueueKeyFmt, pubdef.AppId)
+}
+
+func playletResourceJHImportLockKey(mediaId uint) string {
+ return fmt.Sprintf(playletResourceJHImportLockKeyFmt, pubdef.AppId, mediaId)
+}
+
+// Start 启动短剧媒资导入消费者
+func Start() {
+ playletResourceImportWorker.mu.Lock()
+ defer playletResourceImportWorker.mu.Unlock()
+ if playletResourceImportWorker.cancel != nil {
+ return
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ playletResourceImportWorker.cancel = cancel
+ go runPlayletResourceImportWorker(ctx)
+ go runPlayletResourceJHImportWorker(ctx)
+ go runPlayletSerialSyncWorker(ctx)
+ go runPlayletCoverCompressWorker(ctx)
+ go func() {
+ time.Sleep(10 * time.Second)
+ EnqueuePlayletCoverCompressJobs()
+ }()
+ log.Info("短剧媒资后台消费者已启动")
+}
+
+// Stop 停止短剧媒资导入消费者
+func Stop() {
+ playletResourceImportWorker.mu.Lock()
+ defer playletResourceImportWorker.mu.Unlock()
+ if playletResourceImportWorker.cancel == nil {
+ return
+ }
+ playletResourceImportWorker.cancel()
+ playletResourceImportWorker.cancel = nil
+ log.Info("短剧媒资后台消费者已停止")
+}
+
+// pushPlayletResourceImportJob 投递短剧媒资导入任务
+func pushPlayletResourceImportJob(ctx context.Context, ids []uint, operatorId int64, operatorName string) *srvdef.SrvErr {
+ select {
+ case <-ctx.Done():
+ log.Error("投递短剧媒资导入任务取消", log.E(ctx.Err()))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ default:
+ }
+ job := playletResourceImportJob{
+ Ids: ids,
+ OperatorId: operatorId,
+ OperatorName: operatorName,
+ CreateTime: time.Now().Unix(),
+ }
+ payload, err := json.Marshal(job)
+ if err != nil {
+ log.Error("序列化短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ }
+ if err = redis.RPush(playletResourceImportQueueKey(), string(payload)); err != nil {
+ log.Error("投递短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ }
+ log.Info("投递短剧媒资导入任务成功", log.Any("ids", ids), log.Any("count", len(ids)))
+ return nil
+}
+
+// pushPlayletResourceJHImportJob 投递嘉华短剧媒资导入任务
+func pushPlayletResourceJHImportJob(ctx context.Context, ids []uint, operatorId int64, operatorName string) *srvdef.SrvErr {
+ select {
+ case <-ctx.Done():
+ log.Error("投递嘉华短剧媒资导入任务取消", log.E(ctx.Err()))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ default:
+ }
+ job := playletResourceImportJob{
+ Ids: ids,
+ OperatorId: operatorId,
+ OperatorName: operatorName,
+ CreateTime: time.Now().Unix(),
+ }
+ payload, err := json.Marshal(job)
+ if err != nil {
+ log.Error("序列化嘉华短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ }
+ if err = redis.RPush(playletResourceJHImportQueueKey(), string(payload)); err != nil {
+ log.Error("投递嘉华短剧媒资导入任务失败", log.Any("ids", ids), log.E(err))
+ return srvdef.New(srvdef.Failure, "提交同步任务失败", "提交同步任务失败")
+ }
+ log.Info("投递嘉华短剧媒资导入任务成功", log.Any("ids", ids), log.Any("count", len(ids)))
+ return nil
+}
+
+// runPlayletResourceImportWorker 循环消费短剧媒资导入队列
+func runPlayletResourceImportWorker(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ result, err := redis.Client().BLPop(playletResourceImportPopWait, playletResourceImportQueueKey()).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ continue
+ }
+ log.Error("消费短剧媒资导入任务失败", log.E(err))
+ time.Sleep(playletResourceImportRetryWait)
+ continue
+ }
+ if len(result) < 2 {
+ log.Error("短剧媒资导入任务格式异常", log.Any("result", result))
+ continue
+ }
+ handlePlayletResourceImportPayload(ctx, result[1])
+ }
+}
+
+// runPlayletResourceJHImportWorker 循环消费嘉华短剧媒资导入队列
+func runPlayletResourceJHImportWorker(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ result, err := redis.Client().BLPop(playletResourceImportPopWait, playletResourceJHImportQueueKey()).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ continue
+ }
+ log.Error("消费嘉华短剧媒资导入任务失败", log.E(err))
+ time.Sleep(playletResourceImportRetryWait)
+ continue
+ }
+ if len(result) < 2 {
+ log.Error("嘉华短剧媒资导入任务格式异常", log.Any("result", result))
+ continue
+ }
+ handlePlayletResourceJHImportPayload(ctx, result[1])
+ }
+}
+
+// handlePlayletResourceImportPayload 处理单个短剧媒资导入任务
+func handlePlayletResourceImportPayload(ctx context.Context, payload string) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("处理短剧媒资导入任务异常", log.Any("err", err), log.Any("payload", payload))
+ }
+ }()
+ var job playletResourceImportJob
+ if err := json.Unmarshal([]byte(payload), &job); err != nil {
+ log.Error("解析短剧媒资导入任务失败", log.Any("payload", payload), log.E(err))
+ return
+ }
+ ids := dedupePlayletResourceIds(job.Ids)
+ if len(ids) == 0 {
+ log.Error("短剧媒资导入任务没有有效媒资ID", log.Any("payload", payload))
+ return
+ }
+ log.Info("开始处理短剧媒资导入任务", log.Any("ids", ids), log.Any("count", len(ids)), log.Any("createTime", job.CreateTime),
+ log.Any("operatorId", job.OperatorId), log.Any("operatorName", job.OperatorName))
+ resp := importPlayletResourceBatch(ctx, ids, job.OperatorId, job.OperatorName)
+ log.Info("短剧媒资导入任务处理完成",
+ log.Any("ids", ids),
+ log.Any("successCount", len(resp.Success)),
+ log.Any("failedCount", len(resp.Failed)),
+ log.Any("failed", resp.Failed))
+}
+
+// lockPlayletResourceImportMedia 锁定单条媒资导入
+func lockPlayletResourceImportMedia(mediaId uint) (bool, error) {
+ return redis.Setnx_NewOK(playletResourceImportLockKey(mediaId), true, playletResourceImportLockTTL)
+}
+
+// unlockPlayletResourceImportMedia 释放单条媒资导入锁
+func unlockPlayletResourceImportMedia(mediaId uint) {
+ if err := redis.Del(playletResourceImportLockKey(mediaId)); err != nil {
+ log.Error("释放短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
+ }
+}
+
+// lockPlayletResourceJHImportMedia 锁定单条嘉华媒资导入
+func lockPlayletResourceJHImportMedia(mediaId uint) (bool, error) {
+ return redis.Setnx_NewOK(playletResourceJHImportLockKey(mediaId), true, playletResourceImportLockTTL)
+}
+
+// unlockPlayletResourceJHImportMedia 释放单条嘉华媒资导入锁
+func unlockPlayletResourceJHImportMedia(mediaId uint) {
+ if err := redis.Del(playletResourceJHImportLockKey(mediaId)); err != nil {
+ log.Error("释放嘉华短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
+ }
+}
+
+// handlePlayletResourceJHImportPayload 处理单个嘉华短剧媒资导入任务
+func handlePlayletResourceJHImportPayload(ctx context.Context, payload string) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("处理嘉华短剧媒资导入任务异常", log.Any("err", err), log.Any("payload", payload))
+ }
+ }()
+ var job playletResourceImportJob
+ if err := json.Unmarshal([]byte(payload), &job); err != nil {
+ log.Error("解析嘉华短剧媒资导入任务失败", log.Any("payload", payload), log.E(err))
+ return
+ }
+ ids := dedupePlayletResourceIds(job.Ids)
+ if len(ids) == 0 {
+ log.Error("嘉华短剧媒资导入任务没有有效媒资ID", log.Any("payload", payload))
+ return
+ }
+ log.Info("开始处理嘉华短剧媒资导入任务", log.Any("ids", ids), log.Any("count", len(ids)), log.Any("createTime", job.CreateTime),
+ log.Any("operatorId", job.OperatorId), log.Any("operatorName", job.OperatorName))
+ resp := importPlayletJHResourceBatch(ctx, ids, job.OperatorId, job.OperatorName)
+ log.Info("嘉华短剧媒资导入任务处理完成",
+ log.Any("ids", ids),
+ log.Any("successCount", len(resp.Success)),
+ log.Any("failedCount", len(resp.Failed)),
+ log.Any("failed", resp.Failed))
+}
diff --git a/web/service/srv_mediaresource/media_center.go b/web/service/srv_mediaresource/media_center.go
new file mode 100644
index 0000000..33b3725
--- /dev/null
+++ b/web/service/srv_mediaresource/media_center.go
@@ -0,0 +1,249 @@
+package srv_mediaresource
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strconv"
+ "videoweb/pub/common"
+ "videoweb/pub/common/httputil"
+ "videoweb/pub/common/log"
+ "videoweb/web/api/proto"
+)
+
+const (
+ // playletMediaListErrMsg 短剧媒资列表接口统一错误文案
+ playletMediaListErrMsg = "短剧媒资列表查询失败"
+ // playletMediaSyncErrMsg 短剧媒资同步接口统一错误文案
+ playletMediaSyncErrMsg = "短剧媒资同步失败"
+ // playletMediaDeletedCode 媒资中心资源已删除业务码
+ playletMediaDeletedCode = 7003
+)
+
+// playletMediaCenterService 短剧媒资中心客户端
+type playletMediaCenterService struct {
+ url string // 媒资中心接口域名
+ proId string // 媒资中心商户号
+ secret string // 媒资中心签名密钥,当前接口按源项目逻辑暂不签名
+}
+
+// playletMediaCenterConfig 创建短剧媒资中心客户端所需配置
+type playletMediaCenterConfig struct {
+ Url string // 媒资中心接口域名
+ ProId string // 媒资中心商户号
+ Secret string // 媒资中心签名密钥
+}
+
+// playletMediaCenterMsg 媒资中心统一响应结构
+type playletMediaCenterMsg struct {
+ Code int `json:"code"` // 媒资中心业务码,0 表示成功
+ Msg string `json:"msg"` // 媒资中心业务消息
+ Data json.RawMessage `json:"data"` // 媒资中心业务数据,校验 code 成功后再解析
+}
+
+// playletMediaPagination 媒资中心列表分页信息。
+type playletMediaPagination struct {
+ IsLastPage bool `json:"is_last_page"` // 是否最后一页
+ Page uint `json:"page"` // 当前页码
+ Size uint `json:"size"` // 每页数量
+ Count uint `json:"count"` // 总数量
+}
+
+// playletMediaListReq 媒资中心 /services/media/list 查询参数
+type playletMediaListReq struct {
+ MediaType int `json:"media_type"` // 媒体类型:1-动漫 2-漫画 4-漫画短剧 5-短剧 6-普通短剧
+ Id *uint `json:"id"` // 媒资中心资源ID
+ VloggerId *uint `json:"vlogger_id"` // UP主ID
+ IsUp *int `json:"is_up"` // 是否UP主资源:0-否 1-是
+ Title *string `json:"title"` // 标题筛选
+ Tag *string `json:"tag"` // 标签筛选
+ SyncType int `json:"sync_type"` // 同步类型:1-全量 2-增量
+ Page uint `json:"page"` // 页码
+ Size uint `json:"size"` // 每页数量
+}
+
+// playletMediaListResp 媒资中心列表响应 data 节点
+type playletMediaListResp struct {
+ Data []*proto.PlayletResourceListItem `json:"data"` // 短剧媒资列表
+ Pagination playletMediaPagination `json:"pagination"` // 分页信息
+}
+
+// playletMediaJHListResp 媒资中心嘉华短剧列表响应 data 节点
+type playletMediaJHListResp struct {
+ Data []*proto.PlayletResourceJHListItem `json:"data"` // 嘉华短剧媒资列表
+ Pagination playletMediaPagination `json:"pagination"` // 分页信息
+}
+
+// playletMediaContent 媒资中心短剧章节内容
+type playletMediaContent struct {
+ ID uint `json:"id"` // 媒资内容ID
+ SaleCoin uint `json:"sale_coin"` // 单集售价
+ VloggerId uint `json:"vlogger_id"` // UP主ID
+ Cover string `json:"cover"` // 封面
+ MediaId uint `json:"video_id"` // 媒资中心资源ID
+ MediaType int `json:"media_type"` // 媒体类型
+ EpisodeNumber int `json:"episode_number"` // 第几集
+ Title string `json:"title"` // 章节名
+ Text string `json:"text"` // 文本内容
+ Urls string `json:"urls"` // 资源地址集
+ Height uint `json:"height"` // 高
+ Width uint `json:"width"` // 宽
+ Ratio float64 `json:"ratio"` // 宽高比
+ MediaSize uint `json:"media_size"` // 资源大小
+ PlayTime uint `json:"play_time"` // 播放时长
+ M3u8Url string `json:"m3u8_url"` // m3u8视频地址
+ AudioUrl string `json:"audio_url"` // 音频地址
+ FsResourceId string `json:"fs_resource_id"` // 文件服务资源ID
+ FsResourceStatus string `json:"fs_resource_status"` // 文件服务资源状态
+ HashId string `json:"hash_id"` // 资源hash
+ DataVersion int8 `json:"data_version"` // 数据版本
+ TerminalCdo string `json:"terminal_cdo"` // 上传者
+ CreateTime int64 `json:"create_time"` // 创建时间
+ UpdateTime int64 `json:"update_time"` // 更新时间
+}
+
+// playletMediaSyncReq 媒资中心 /services/media/sync 查询参数
+type playletMediaSyncReq struct {
+ Id uint `json:"id"` // 媒资中心资源ID
+ SyncTime int64 `json:"sync_time"` // 增量同步时间,0 表示拉全量详情
+}
+
+// playletMediaSyncResp 媒资中心同步响应 data 节点
+type playletMediaSyncResp struct {
+ IsDelete bool `json:"-"` // 是否已从媒资中心删除
+ Media *proto.PlayletResourceListItem `json:"media"` // 短剧主资源
+ MediaContentList []playletMediaContent `json:"media_content_list"` // 短剧章节资源
+}
+
+// newPlayletMediaCenterService 创建短剧媒资中心客户端
+func newPlayletMediaCenterService(c playletMediaCenterConfig) *playletMediaCenterService {
+ return &playletMediaCenterService{
+ url: c.Url,
+ proId: c.ProId,
+ secret: c.Secret,
+ }
+}
+
+// mediaListParams 构造媒资中心列表查询参数
+func (m *playletMediaCenterService) mediaListParams(p *playletMediaListReq) map[string]string {
+ params := map[string]string{
+ "media_type": strconv.Itoa(p.MediaType),
+ "sync_type": strconv.Itoa(p.SyncType),
+ "page": strconv.FormatUint(uint64(p.Page), 10),
+ "size": strconv.FormatUint(uint64(p.Size), 10),
+ "pro_id": m.proId,
+ "sign": m.sign(),
+ }
+ if p.Id != nil {
+ params["id"] = strconv.FormatUint(uint64(*p.Id), 10)
+ }
+ if p.VloggerId != nil {
+ params["vlogger_id"] = strconv.FormatUint(uint64(*p.VloggerId), 10)
+ }
+ if p.IsUp != nil {
+ params["is_up"] = strconv.Itoa(*p.IsUp)
+ }
+ if p.Title != nil {
+ params["title"] = *p.Title
+ }
+ if p.Tag != nil {
+ params["tag"] = *p.Tag
+ }
+ return params
+}
+
+// mediaSyncParams 构造媒资中心同步查询参数
+func (m *playletMediaCenterService) mediaSyncParams(p *playletMediaSyncReq) map[string]string {
+ return map[string]string{
+ "id": strconv.FormatUint(uint64(p.Id), 10),
+ "sync_time": strconv.FormatInt(p.SyncTime, 10),
+ "pro_id": m.proId,
+ "sign": m.sign(),
+ }
+}
+
+// mediaList 查询短剧媒资列表
+func (m *playletMediaCenterService) mediaList(p *playletMediaListReq) (result playletMediaListResp, err error) {
+ var retMsg playletMediaCenterMsg
+ params := m.mediaListParams(p)
+ path := "/services/media/list"
+ log.Info("查询短剧媒资列表", log.Any("params", params))
+ code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
+ if err != nil {
+ log.Error("查询短剧媒资列表请求失败", log.Any("code", code), log.E(err))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if code != http.StatusOK {
+ log.Error("查询短剧媒资列表状态码异常", log.Any("code", code))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if retMsg.Code != 0 {
+ log.Error("查询短剧媒资列表业务码异常", log.Any("retMsg", retMsg))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if err = json.Unmarshal(retMsg.Data, &result); err != nil {
+ log.Error("解析短剧媒资列表失败", log.E(err))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ return result, nil
+}
+
+// mediaJHList 查询嘉华短剧媒资列表
+func (m *playletMediaCenterService) mediaJHList(p *playletMediaListReq) (result playletMediaJHListResp, err error) {
+ var retMsg playletMediaCenterMsg
+ params := m.mediaListParams(p)
+ path := "/services/media/list"
+ log.Info("查询嘉华短剧媒资列表", log.Any("params", params))
+ code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
+ if err != nil {
+ log.Error("查询嘉华短剧媒资列表请求失败", log.Any("code", code), log.E(err))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if code != http.StatusOK {
+ log.Error("查询嘉华短剧媒资列表状态码异常", log.Any("code", code))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if retMsg.Code != 0 {
+ log.Error("查询嘉华短剧媒资列表业务码异常", log.Any("retMsg", retMsg))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ if err = json.Unmarshal(retMsg.Data, &result); err != nil {
+ log.Error("解析嘉华短剧媒资列表失败", log.E(err))
+ return result, errors.New(playletMediaListErrMsg)
+ }
+ return result, nil
+}
+
+// mediaSync 查询短剧媒资详情和章节内容
+func (m *playletMediaCenterService) mediaSync(p *playletMediaSyncReq) (result playletMediaSyncResp, err error) {
+ var retMsg playletMediaCenterMsg
+ params := m.mediaSyncParams(p)
+ path := "/services/media/sync"
+ code, err := httputil.Client().GetWithJResp(&retMsg, common.BindUrl(m.url, path), nil, params)
+ if err != nil {
+ log.Error("同步短剧媒资详情请求失败", log.Any("params", params), log.Any("code", code), log.E(err))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ if code != http.StatusOK {
+ log.Error("同步短剧媒资详情状态码异常", log.Any("params", params), log.Any("code", code))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ if retMsg.Code == playletMediaDeletedCode {
+ result.IsDelete = true
+ return result, nil
+ }
+ if retMsg.Code != 0 {
+ log.Error("同步短剧媒资详情业务码异常", log.Any("params", params), log.Any("retMsg", retMsg))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ if err = json.Unmarshal(retMsg.Data, &result); err != nil {
+ log.Error("解析短剧媒资详情失败", log.Any("params", params), log.E(err))
+ return result, errors.New(playletMediaSyncErrMsg)
+ }
+ return result, nil
+}
+
+// sign 生成媒资中心签名
+func (m *playletMediaCenterService) sign() string {
+ return ""
+}
diff --git a/web/service/srv_mediaresource/resource.go b/web/service/srv_mediaresource/resource.go
new file mode 100644
index 0000000..4f73c22
--- /dev/null
+++ b/web/service/srv_mediaresource/resource.go
@@ -0,0 +1,1433 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "math/rand"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "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
+ resourceAddStatusScanMaxPages = 20
+ resourceAddedIDBatchSize = 100
+ playletResourceImportMsg = "同步任务已提交,后台正在处理"
+ playletHeatMinValue = 1000
+ playletHeatMaxValue = 5000
+ playletVirtualLikeMinValue = 10
+ playletVirtualLikeMaxValue = 20
+
+ playletResourceMediaTypeAnime = 1 // 动漫
+ playletResourceMediaTypeManga = 2 // 漫画
+ 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, srvErr := resolvePlayletResourceMediaType(req.MediaType)
+ if srvErr != nil {
+ return nil, srvErr
+ }
+ // MediaCenterService 当前只保存配置值,按请求创建成本很低,也避免全局对象在配置热更新场景下读到旧配置
+ ms := newPlayletMediaCenterFromConfig()
+ if req.IsAdd != nil {
+ return listPlayletResourceByAddStatus(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
+ }
+ 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
+}
+
+// ListPlayletJHResource 分页查询嘉华短剧媒资列表
+func ListPlayletJHResource(ctx context.Context, req *proto.PlayletResourceJHListReq) (*proto.PlayletResourceJHListResp, *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
+ }
+ ms := newPlayletJHMediaCenterFromConfig()
+ if req.IsAdd != nil {
+ return listPlayletJHResourceByAddStatus(ctx, ms, req, syncType, pageNumber, pageSize)
+ }
+ data, err := ms.mediaJHList(&playletMediaListReq{
+ MediaType: playletResourceMediaTypeShort,
+ 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 := fillPlayletJHResourceAddStatus(ctx, data.Data); err != nil {
+ return nil, err
+ }
+ return &proto.PlayletResourceJHListResp{
+ List: data.Data,
+ Total: data.Pagination.Count,
+ }, nil
+}
+
+// ListPlayletResourceChapters 查询短剧媒资剧集信息(复用入库时的 mediaSync)
+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
+}
+
+// ListPlayletJHResourceChapters 查询嘉华短剧媒资剧集信息
+func ListPlayletJHResourceChapters(ctx context.Context, req *proto.PlayletResourceChapterListReq) (*proto.PlayletResourceChapterListResp, *srvdef.SrvErr) {
+ if req == nil || req.Id == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
+ }
+ ms := newPlayletJHMediaCenterFromConfig()
+ 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, playletResourceMediaTypeManga, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
+ return true
+ default:
+ return false
+ }
+}
+
+// validPlayletImportMediaType 允许入库的媒资类型。
+func validPlayletImportMediaType(mediaType int) bool {
+ switch mediaType {
+ case playletResourceMediaTypeAnime,
+ playletResourceMediaTypeManga, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
+ return true
+ default:
+ return false
+ }
+}
+
+// contentTypeFromMediaType 媒资中心 media_type 映射为本项目 contentType。
+// 1-动漫 → 3;2-漫画 → 4;4-漫画短剧 → 2;5-短剧 → 0;6-普通短剧 → 1。
+func contentTypeFromMediaType(mediaType int) playletmod.ContentType {
+ switch mediaType {
+ case playletResourceMediaTypeAnime:
+ return playletmod.ContentTypeAnime
+ case playletResourceMediaTypeManga:
+ return playletmod.ContentTypeManga
+ 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 {
+ // 只收集当前页有效媒资ID,空数据和无效ID不参与本地查询。
+ if item == nil || item.ID == 0 {
+ continue
+ }
+ // 当前页可能出现重复媒资ID,去重后可以减少 Mongo $in 查询条件。
+ if _, ok := seen[item.ID]; ok {
+ continue
+ }
+ seen[item.ID] = struct{}{}
+ mediaIds = append(mediaIds, item.ID)
+ }
+ if len(mediaIds) == 0 {
+ return nil
+ }
+ // 按当前页媒资ID批量查询本地短剧库,避免逐条查询影响列表性能。
+ filter := applyPlayletMediaSourceFilter(bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": mediaIds},
+ }, playletmod.MediaSourceHG)
+ playletList, err := playletmod.FindManyPlaylet(ctx, filter)
+ 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 {
+ // mediaCenterId 是本地短剧表保存的媒资中心ID,用它判断当前媒资是否已入库。
+ if item == nil || item.MediaCenterId == 0 {
+ continue
+ }
+ addedMap[item.MediaCenterId] = true
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ // map 没命中时 bool 零值为 false,表示未加入后台短剧库。
+ item.IsAdd = addedMap[item.ID]
+ }
+ return nil
+}
+
+// fillPlayletJHResourceAddStatus 回填嘉华媒资列表是否已入库
+func fillPlayletJHResourceAddStatus(ctx context.Context, list []*proto.PlayletResourceJHListItem) *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
+ }
+ importedMap, err := findImportedPlayletResourceMap(ctx, mediaIds, playletmod.MediaSourceDefault)
+ if err != nil {
+ return err
+ }
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ item.IsAdd = importedMap[item.ID]
+ }
+ return nil
+}
+
+// listPlayletJHResourceByAddStatus 按是否已入库过滤嘉华媒资列表
+func listPlayletJHResourceByAddStatus(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceJHListReq,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
+ if *req.IsAdd && !playletJHResourceRequiresUpstreamAddStatusScan(req, syncType) {
+ return listAddedPlayletJHResourceByLocalFilter(ctx, ms, req, syncType, pageNumber, pageSize)
+ }
+ return scanPlayletJHResourceByAddStatus(ctx, ms, req, syncType, pageNumber, pageSize)
+}
+
+// playletJHResourceRequiresUpstreamAddStatusScan 判断嘉华列表是否必须扫描上游才能保证筛选结果准确。
+func playletJHResourceRequiresUpstreamAddStatusScan(req *proto.PlayletResourceJHListReq, syncType int) bool {
+ if syncType != defaultResourceSyncType {
+ return true
+ }
+ return req != nil && req.Tag != nil && strings.TrimSpace(*req.Tag) != ""
+}
+
+// listAddedPlayletJHResourceByLocalFilter 查询已入库嘉华媒资,优先走本地playlet表分页
+func listAddedPlayletJHResourceByLocalFilter(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceJHListReq,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
+ filter := buildAddedPlayletJHResourceFilter(req)
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletResourceJHListResp{
+ List: make([]*proto.PlayletResourceJHListItem, 0, pageSize),
+ Total: uint(totalCount),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ start := int64((pageNumber - 1) * pageSize)
+ needCount := int64(pageNumber * pageSize)
+ matchedCount := int64(0)
+ for batchStart := int64(0); batchStart < totalCount && matchedCount < needCount; batchStart += resourceAddedIDBatchSize {
+ list, err := queryAddedPlayletResourceIds(ctx, filter, batchStart)
+ if err != nil {
+ return nil, err
+ }
+ if len(list) == 0 {
+ break
+ }
+ items, err := queryAddedPlayletJHResourceItems(ms, list, syncType)
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range items {
+ if item == nil {
+ continue
+ }
+ if matchedCount >= start && len(resp.List) < pageSize {
+ resp.List = append(resp.List, item)
+ }
+ matchedCount++
+ if matchedCount >= needCount {
+ break
+ }
+ }
+ }
+ return resp, nil
+}
+
+// buildAddedPlayletJHResourceFilter 构建本地已入库嘉华媒资查询条件。
+func buildAddedPlayletJHResourceFilter(req *proto.PlayletResourceJHListReq) bson.M {
+ filter := applyPlayletMediaSourceFilter(bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
+ playletmod.FN.Playlet.ContentType: contentTypeFromMediaType(playletResourceMediaTypeShort),
+ }, playletmod.MediaSourceDefault)
+ if req != nil && req.Title != nil {
+ if title := strings.TrimSpace(*req.Title); title != "" {
+ filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
+ }
+ }
+ return filter
+}
+
+// queryAddedPlayletJHResourceItems 按本地短剧的媒资中心ID回查嘉华媒资数据。
+func queryAddedPlayletJHResourceItems(ms *playletMediaCenterService, list playletmod.PlayletList, syncType int) ([]*proto.PlayletResourceJHListItem, *srvdef.SrvErr) {
+ result := make([]*proto.PlayletResourceJHListItem, 0, len(list))
+ for _, item := range list {
+ if item == nil || item.MediaCenterId == 0 {
+ continue
+ }
+ media, err := queryPlayletJHResourceByMediaCenterId(ms, item.MediaCenterId, syncType)
+ if err != nil {
+ return nil, err
+ }
+ if media == nil {
+ continue
+ }
+ media.IsAdd = true
+ result = append(result, media)
+ }
+ return result, nil
+}
+
+// queryPlayletJHResourceByMediaCenterId 按媒资中心ID查询单条嘉华媒资列表数据。
+func queryPlayletJHResourceByMediaCenterId(ms *playletMediaCenterService, mediaId uint, syncType int) (*proto.PlayletResourceJHListItem, *srvdef.SrvErr) {
+ data, err := ms.mediaJHList(&playletMediaListReq{
+ MediaType: playletResourceMediaTypeShort,
+ Id: &mediaId,
+ SyncType: syncType,
+ Page: 1,
+ Size: 1,
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ for _, item := range data.Data {
+ if item != nil && item.ID == mediaId {
+ return item, nil
+ }
+ }
+ if len(data.Data) > 0 {
+ return data.Data[0], nil
+ }
+ return nil, nil
+}
+
+// scanPlayletJHResourceByAddStatus 扫描嘉华媒资分页并按入库状态精确过滤。
+func scanPlayletJHResourceByAddStatus(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceJHListReq,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
+ offset := (pageNumber - 1) * pageSize
+ list := make([]*proto.PlayletResourceJHListItem, 0, pageSize)
+ matchedCount := 0
+ scanPage := uint(1)
+ scanSize := uint(maxResourcePageSize)
+ for {
+ data, err := ms.mediaJHList(&playletMediaListReq{
+ MediaType: playletResourceMediaTypeShort,
+ Title: req.Title,
+ Tag: req.Tag,
+ SyncType: syncType,
+ Page: scanPage,
+ Size: scanSize,
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ if err := fillPlayletJHResourceAddStatus(ctx, data.Data); err != nil {
+ return nil, err
+ }
+ for _, item := range data.Data {
+ if item == nil || item.IsAdd != *req.IsAdd {
+ continue
+ }
+ if matchedCount >= offset && len(list) < pageSize {
+ list = append(list, item)
+ }
+ matchedCount++
+ }
+ if data.Pagination.IsLastPage || len(data.Data) == 0 {
+ break
+ }
+ if data.Pagination.Count > 0 && scanPage*scanSize >= data.Pagination.Count {
+ break
+ }
+ scanPage++
+ }
+ return &proto.PlayletResourceJHListResp{
+ List: list,
+ Total: uint(matchedCount),
+ }, nil
+}
+
+// listPlayletResourceByAddStatus 按是否已加入后台短剧库过滤媒资列表,并按过滤后的结果分页
+func listPlayletResourceByAddStatus(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceListReq,
+ mediaType int,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
+ if *req.IsAdd && !playletResourceRequiresUpstreamAddStatusScan(req, syncType) {
+ return listAddedPlayletResourceByLocalFilter(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
+ }
+ if !*req.IsAdd && !playletResourceRequiresUpstreamAddStatusScan(req, syncType) {
+ return listNotAddedPlayletResourceByLocalEstimate(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
+ }
+ return scanPlayletResourceByAddStatus(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
+}
+
+// listAddedPlayletResourceByLocalFilter 查询已加入后台短剧库的媒资,优先走本地playlet表分页
+func listAddedPlayletResourceByLocalFilter(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceListReq,
+ mediaType int,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
+ filter := buildAddedPlayletResourceFilter(req, mediaType)
+ totalCount, err := playletmod.CountPlaylet(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletResourceListResp{
+ List: make([]*proto.PlayletResourceListItem, 0, pageSize),
+ Total: uint(totalCount),
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ start := int64((pageNumber - 1) * pageSize)
+ needCount := int64(pageNumber * pageSize)
+ matchedCount := int64(0)
+ for batchStart := int64(0); batchStart < totalCount && matchedCount < needCount; batchStart += resourceAddedIDBatchSize {
+ list, err := queryAddedPlayletResourceIds(ctx, filter, batchStart)
+ if err != nil {
+ return nil, err
+ }
+ if len(list) == 0 {
+ break
+ }
+ items, err := queryAddedPlayletResourceItems(ms, list, mediaType, syncType)
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range items {
+ if item == nil {
+ continue
+ }
+ if matchedCount >= start && len(resp.List) < pageSize {
+ resp.List = append(resp.List, item)
+ }
+ matchedCount++
+ if matchedCount >= needCount {
+ break
+ }
+ }
+ }
+ return resp, nil
+}
+
+// listNotAddedPlayletResourceByLocalEstimate 查询未加入媒资,total按上游总数减本地已加入数估算。
+func listNotAddedPlayletResourceByLocalEstimate(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceListReq,
+ mediaType int,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
+ scanSize := uint(maxResourcePageSize)
+ firstPage, err := ms.mediaList(&playletMediaListReq{
+ MediaType: mediaType,
+ Title: req.Title,
+ SyncType: syncType,
+ Page: 1,
+ Size: scanSize,
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ addedTotal, countErr := playletmod.CountPlaylet(ctx, buildAddedPlayletResourceFilter(req, mediaType))
+ if countErr != nil {
+ return nil, countErr
+ }
+ total := int64(firstPage.Pagination.Count) - addedTotal
+ if total < 0 {
+ total = 0
+ }
+ resp := &proto.PlayletResourceListResp{
+ List: make([]*proto.PlayletResourceListItem, 0, pageSize),
+ Total: uint(total),
+ }
+ start := (pageNumber - 1) * pageSize
+ needCount := start + pageSize
+ matchedCount := 0
+ lastPage := calcPlayletResourceLastPage(firstPage.Pagination.Count, scanSize)
+ scanLimit := lastPage
+ if scanLimit > resourceAddStatusScanMaxPages {
+ scanLimit = resourceAddStatusScanMaxPages
+ }
+ for scanPage := uint(1); scanPage <= scanLimit; scanPage++ {
+ currentPage := firstPage
+ if scanPage > 1 {
+ data, err := ms.mediaList(&playletMediaListReq{
+ MediaType: mediaType,
+ Title: req.Title,
+ SyncType: syncType,
+ Page: scanPage,
+ Size: scanSize,
+ })
+ if err != nil {
+ log.Warn("查询未加入短剧媒资后续分页失败,停止继续扫描", log.Any("页码", scanPage), log.E(err))
+ break
+ }
+ currentPage = data
+ }
+ if len(currentPage.Data) == 0 {
+ break
+ }
+ if err := fillPlayletResourceByAddStatusPage(ctx, currentPage.Data, false, start, pageSize, resp, &matchedCount); err != nil {
+ return nil, err
+ }
+ if matchedCount >= needCount || currentPage.Pagination.IsLastPage {
+ break
+ }
+ }
+ if scanLimit < lastPage && matchedCount < needCount {
+ log.Warn("短剧媒资加入状态筛选达到最大扫描页数", log.Any("最大扫描页数", resourceAddStatusScanMaxPages), log.Any("上游总页数", lastPage), log.Any("命中数量", matchedCount), log.Any("需要数量", needCount))
+ }
+ return resp, nil
+}
+
+// scanPlayletResourceByAddStatus 扫描媒资中心分页并按加入状态精确过滤,适用于本地无法独立判断的筛选。
+func scanPlayletResourceByAddStatus(
+ ctx context.Context,
+ ms *playletMediaCenterService,
+ req *proto.PlayletResourceListReq,
+ mediaType int,
+ syncType int,
+ pageNumber int,
+ pageSize int,
+) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
+ offset := (pageNumber - 1) * pageSize
+ list := make([]*proto.PlayletResourceListItem, 0, pageSize)
+ matchedCount := 0
+ scanPage := uint(1)
+ scanSize := uint(maxResourcePageSize)
+ for {
+ data, err := ms.mediaList(&playletMediaListReq{
+ MediaType: mediaType,
+ Title: req.Title,
+ Tag: req.Tag,
+ SyncType: syncType,
+ Page: scanPage,
+ Size: scanSize,
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ if err := fillPlayletResourceAddStatus(ctx, data.Data); err != nil {
+ return nil, err
+ }
+ for _, item := range data.Data {
+ if item == nil || item.IsAdd != *req.IsAdd {
+ continue
+ }
+ if matchedCount >= offset && len(list) < pageSize {
+ list = append(list, item)
+ }
+ matchedCount++
+ }
+ if data.Pagination.IsLastPage || len(data.Data) == 0 {
+ break
+ }
+ if data.Pagination.Count > 0 && scanPage*scanSize >= data.Pagination.Count {
+ break
+ }
+ scanPage++
+ }
+ return &proto.PlayletResourceListResp{
+ List: list,
+ Total: uint(matchedCount),
+ }, nil
+}
+
+// calcPlayletResourceLastPage 计算上游媒资列表最后一页页码
+func calcPlayletResourceLastPage(total uint, pageSize uint) uint {
+ if total == 0 || pageSize == 0 {
+ return 0
+ }
+ return (total + pageSize - 1) / pageSize
+}
+
+// playletResourceRequiresUpstreamAddStatusScan 判断是否必须扫描上游才能保证筛选结果准确。
+func playletResourceRequiresUpstreamAddStatusScan(req *proto.PlayletResourceListReq, syncType int) bool {
+ if syncType != defaultResourceSyncType {
+ return true
+ }
+ return req != nil && req.Tag != nil && strings.TrimSpace(*req.Tag) != ""
+}
+
+// buildAddedPlayletResourceFilter 构建本地已加入短剧媒资查询条件。
+func buildAddedPlayletResourceFilter(req *proto.PlayletResourceListReq, mediaType int) bson.M {
+ filter := applyPlayletMediaSourceFilter(bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
+ playletmod.FN.Playlet.ContentType: contentTypeFromMediaType(mediaType),
+ }, playletmod.MediaSourceHG)
+ if req != nil && req.Title != nil {
+ if title := strings.TrimSpace(*req.Title); title != "" {
+ filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
+ }
+ }
+ return filter
+}
+
+// queryAddedPlayletResourceIds 分批查询已加入短剧的媒资中心ID。
+func queryAddedPlayletResourceIds(ctx context.Context, filter bson.M, skip int64) (playletmod.PlayletList, *srvdef.SrvErr) {
+ list, err := playletmod.FindManyPlaylet(ctx, filter, options.Find().
+ SetProjection(bson.D{{Key: playletmod.FN.Playlet.MediaCenterId, Value: 1}}).
+ SetSort(bson.D{
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }).
+ SetSkip(skip).
+ SetLimit(resourceAddedIDBatchSize))
+ if err != nil {
+ log.Warn("查询已加入短剧媒资ID失败", log.Any("filter", filter), log.E(err))
+ return nil, err
+ }
+ return list, nil
+}
+
+// queryAddedPlayletResourceItems 按本地短剧的媒资中心ID回查媒资数据。
+func queryAddedPlayletResourceItems(ms *playletMediaCenterService, list playletmod.PlayletList, mediaType int, syncType int) ([]*proto.PlayletResourceListItem, *srvdef.SrvErr) {
+ result := make([]*proto.PlayletResourceListItem, 0, len(list))
+ for _, item := range list {
+ if item == nil || item.MediaCenterId == 0 {
+ continue
+ }
+ media, err := queryPlayletResourceByMediaCenterId(ms, item.MediaCenterId, mediaType, syncType)
+ if err != nil {
+ return nil, err
+ }
+ if media == nil {
+ continue
+ }
+ media.IsAdd = true
+ result = append(result, media)
+ }
+ return result, nil
+}
+
+// queryPlayletResourceByMediaCenterId 按媒资中心ID查询单条媒资列表数据。
+func queryPlayletResourceByMediaCenterId(ms *playletMediaCenterService, mediaId uint, mediaType int, syncType int) (*proto.PlayletResourceListItem, *srvdef.SrvErr) {
+ data, err := ms.mediaList(&playletMediaListReq{
+ MediaType: mediaType,
+ Id: &mediaId,
+ SyncType: syncType,
+ Page: 1,
+ Size: 1,
+ })
+ if err != nil {
+ return nil, srvdef.New(srvdef.Failure, err.Error(), "")
+ }
+ for _, item := range data.Data {
+ if item != nil && item.ID == mediaId {
+ return item, nil
+ }
+ }
+ if len(data.Data) > 0 {
+ return data.Data[0], nil
+ }
+ return nil, nil
+}
+
+// fillPlayletResourceByAddStatusPage 按加入状态累计过滤,并截取请求页数据。
+func fillPlayletResourceByAddStatusPage(ctx context.Context, list []*proto.PlayletResourceListItem, wantAdd bool, start int, pageSize int, resp *proto.PlayletResourceListResp, matchedCount *int) *srvdef.SrvErr {
+ if err := fillPlayletResourceAddStatus(ctx, list); err != nil {
+ return err
+ }
+ for _, item := range list {
+ if item == nil || item.IsAdd != wantAdd {
+ continue
+ }
+ if *matchedCount >= start && len(resp.List) < pageSize {
+ resp.List = append(resp.List, item)
+ }
+ *matchedCount = *matchedCount + 1
+ }
+ 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
+}
+
+// ImportPlayletJHResource 提交嘉华短剧媒资入库任务
+func ImportPlayletJHResource(ctx context.Context, req *proto.PlayletResourceUseReq) (string, *srvdef.SrvErr) {
+ ids, err := validatePlayletResourceImportReq(req)
+ if err != nil {
+ return "", err
+ }
+ operatorId, operatorName := resolveImportOperator(ctx)
+ if err := pushPlayletResourceJHImportJob(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, playletmod.MediaSourceHG)
+ 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
+}
+
+// importPlayletJHResourceBatch 批量处理嘉华短剧媒资入库任务
+func importPlayletJHResourceBatch(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, playletmod.MediaSourceDefault)
+ if err != nil {
+ for _, id := range ids {
+ resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
+ MediaId: id,
+ Reason: "查询导入状态失败",
+ })
+ }
+ return resp
+ }
+ ms := newPlayletJHMediaCenterFromConfig()
+ for _, id := range ids {
+ if importedMap[id] {
+ resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
+ MediaId: id,
+ Reason: "该媒资已导入",
+ })
+ continue
+ }
+ success, reason := importSinglePlayletJHResource(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, mediaSource string) (map[uint]bool, *srvdef.SrvErr) {
+ result := make(map[uint]bool, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ filter := applyPlayletMediaSourceFilter(bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": ids},
+ }, mediaSource)
+ playletList, err := playletmod.FindManyPlaylet(ctx, filter)
+ if err != nil {
+ log.Error("批量查询短剧媒资导入状态失败", log.Any("mediaIds", ids), log.Any("err", err))
+ return nil, err
+ }
+ for _, item := range playletList {
+ // 只记录有效的媒资中心ID,供批量任务跳过已入库数据。
+ 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,
+ })
+}
+
+func newPlayletJHMediaCenterFromConfig() *playletMediaCenterService {
+ return newPlayletMediaCenterService(playletMediaCenterConfig{
+ Url: webg.Conf.PlayletMediaResourceDBCfg.BaseUrl,
+ ProId: webg.Conf.PlayletMediaResourceDBCfg.Merchat,
+ Secret: webg.Conf.PlayletMediaResourceDBCfg.Secret,
+ })
+}
+
+func newPlayletMediaCenterBySource(mediaSource string) *playletMediaCenterService {
+ if mediaSource == playletmod.MediaSourceHG {
+ return newPlayletMediaCenterFromConfig()
+ }
+ return newPlayletJHMediaCenterFromConfig()
+}
+
+func applyPlayletMediaSourceFilter(filter bson.M, mediaSource string) bson.M {
+ if filter == nil {
+ filter = bson.M{}
+ }
+ if mediaSource == playletmod.MediaSourceHG {
+ filter[playletmod.FN.Playlet.MediaSource] = playletmod.MediaSourceHG
+ return filter
+ }
+ filter["$or"] = []bson.M{
+ {playletmod.FN.Playlet.MediaSource: playletmod.MediaSourceDefault},
+ {playletmod.FN.Playlet.MediaSource: bson.M{"$exists": false}},
+ }
+ return filter
+}
+
+// validatePlayletResourceImportReq 校验短剧媒资入库请求
+func validatePlayletResourceImportReq(req *proto.PlayletResourceUseReq) ([]uint, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ // 过滤空ID和重复ID,避免重复导入同一条媒资。
+ 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 {
+ // 0 不是有效媒资ID,直接丢弃。
+ if id == 0 {
+ continue
+ }
+ // 同一批次重复选择的ID只处理一次。
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
+
+type playletResourceImportLockFunc func(uint) (bool, error)
+type playletResourceImportUnlockFunc func(uint)
+
+// importSinglePlayletResource 导入单条短剧媒资
+func importSinglePlayletResource(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string) (*proto.PlayletResourceUseSuccess, string) {
+ return importSinglePlayletResourceWithLock(ctx, ms, mediaId, operatorId, operatorName, playletmod.MediaSourceHG, lockPlayletResourceImportMedia, unlockPlayletResourceImportMedia)
+}
+
+// importSinglePlayletJHResource 导入单条嘉华短剧媒资
+func importSinglePlayletJHResource(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string) (*proto.PlayletResourceUseSuccess, string) {
+ return importSinglePlayletResourceWithLock(ctx, ms, mediaId, operatorId, operatorName, playletmod.MediaSourceDefault, lockPlayletResourceJHImportMedia, unlockPlayletResourceJHImportMedia)
+}
+
+// importSinglePlayletResourceWithLock 导入单条短剧媒资并使用指定锁
+func importSinglePlayletResourceWithLock(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string, mediaSource string, lockFn playletResourceImportLockFunc, unlockFn playletResourceImportUnlockFunc) (*proto.PlayletResourceUseSuccess, string) {
+ locked, err := lockFn(mediaId)
+ if err != nil {
+ log.Error("获取短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
+ return nil, "获取导入锁失败"
+ }
+ if !locked {
+ return nil, "该媒资正在导入"
+ }
+ defer unlockFn(mediaId)
+
+ // 本地已导入的媒资不再请求媒资中心,减少无效网络调用。
+ if reason := checkPlayletResourceImported(ctx, mediaId, mediaSource); reason != "" {
+ return nil, reason
+ }
+ // sync_time 传 0,表示拉取该媒资当前完整详情和章节列表。
+ 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, "媒资章节为空"
+ }
+
+ // 标签按媒资中心 tags_text 自动生成或复用,不再由请求参数控制。
+ //tagIds, serr := buildPlayletResourceTagIds(ctx, result.Media.TagsText)
+ //if serr != nil {
+ // log.Error("处理短剧媒资标签失败", log.Any("mediaId", mediaId), log.Any("err", serr))
+ // return nil, "处理标签失败"
+ //}
+ // 将媒资中心主资源转换成本项目短剧主表模型。
+ playlet := buildPlayletFromMedia(result.Media, nil, operatorId, operatorName)
+ playlet.MediaSource = mediaSource
+ if serr := ensureImportedPlayletSlug(ctx, playlet); serr != nil {
+ log.Error("生成短剧媒资slug失败", log.Any("mediaId", mediaId), log.Any("err", serr))
+ return nil, "生成slug失败"
+ }
+ playlet.MediaSyncTime = maxPlayletMediaContentUpdateTime(0, result.MediaContentList)
+ // 原图继续写入 Cover;压缩成功才写 CompressedCover,失败保持空。
+ playlet.CompressedCover = compressPlayletCoverOrEmpty(playlet.Cover)
+ if playlet.Title == "" {
+ return nil, "媒资标题为空"
+ }
+ // 章节转换会过滤没有播放地址的数据,过滤后为空则不写主表。
+ chapters := buildPlayletChaptersFromMedia(result.MediaContentList)
+ if len(chapters) == 0 {
+ return nil, "媒资章节为空"
+ }
+ // 先插入主表,拿到本项目自增 playlet.Id 后再写章节。
+ if serr := playletmod.InsertOnePlaylet(ctx, playlet); serr != nil {
+ log.Error("短剧媒资主表入库失败", log.Any("mediaId", mediaId), log.Any("err", serr))
+ return nil, "主资源入库失败"
+ }
+ for _, chapter := range chapters {
+ // 每个章节都绑定刚生成的短剧ID。
+ chapter.PlayletId = playlet.Id
+ }
+ // 章节批量写入,减少同一部短剧大量章节逐条访问 Mongo 的开销。
+ if serr := playletchaptermod.InsertManyPlayletChapter(ctx, chapters); serr != nil {
+ log.Error("短剧媒资章节批量入库失败", log.Any("mediaId", mediaId), log.Any("playletId", playlet.Id), log.Any("chapterCount", len(chapters)), log.Any("err", serr))
+ // 批量写入可能已经落入部分章节,按预分配的章节ID回滚,避免半成品短剧留库。
+ 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 {
+ // 批量插入前已预分配章节ID,回滚时只删除本次任务生成的数据。
+ if chapter == nil || chapter.Id == 0 {
+ continue
+ }
+ ids = append(ids, chapter.Id)
+ }
+ return ids
+}
+
+// checkPlayletResourceImported 检查指定来源媒资是否已经导入到本项目短剧表
+func checkPlayletResourceImported(ctx context.Context, mediaId uint, mediaSource string) string {
+ // mediaCenterId 是媒资中心ID,不是本项目自增ID,用它判断是否导入过。
+ filter := applyPlayletMediaSourceFilter(bson.M{playletmod.FN.Playlet.MediaCenterId: mediaId}, mediaSource)
+ _, err := playletmod.FindOnePlaylet(ctx, filter)
+ if err == nil {
+ return "该媒资已导入"
+ }
+ if err.Code != srvdef.ErrNoData {
+ log.Error("查询短剧媒资导入状态失败", log.Any("mediaId", mediaId), log.Any("err", err))
+ return "查询导入状态失败"
+ }
+ return ""
+}
+
+// buildPlayletResourceTagIds 生成短剧入库标签ID列表
+func buildPlayletResourceTagIds(ctx context.Context, tagsText string) ([]int64, *srvdef.SrvErr) {
+ tagIds := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ // 解析媒资中心 tags_text,并自动创建或复用本项目标签。
+ for _, tagName := range splitPlayletResourceTags(tagsText) {
+ tagId, err := findOrCreatePlayletTag(ctx, tagName)
+ if err != nil {
+ return nil, err
+ }
+ if tagId <= 0 {
+ continue
+ }
+ if _, ok := seen[tagId]; ok {
+ continue
+ }
+ seen[tagId] = struct{}{}
+ tagIds = append(tagIds, tagId)
+ }
+ return tagIds, nil
+}
+
+// splitPlayletResourceTags 拆分媒资中心标签文本
+func splitPlayletResourceTags(tagsText string) []string {
+ if strings.TrimSpace(tagsText) == "" {
+ return nil
+ }
+ // 兼容媒资中心可能返回的多种标签分隔符。
+ parts := strings.FieldsFunc(tagsText, func(r rune) bool {
+ return r == ',' || r == ',' || r == ';' || r == ';' || r == '|'
+ })
+ result := make([]string, 0, len(parts))
+ seen := make(map[string]struct{}, len(parts))
+ for _, part := range parts {
+ tagName := strings.TrimSpace(part)
+ // 空标签不入库,避免产生脏标签。
+ if tagName == "" {
+ continue
+ }
+ // 同一个媒资标签文本里重复的标签只保留一次。
+ if _, ok := seen[tagName]; ok {
+ continue
+ }
+ seen[tagName] = struct{}{}
+ result = append(result, tagName)
+ }
+ return result
+}
+
+// findOrCreatePlayletTag 查询或创建短剧标签
+func findOrCreatePlayletTag(ctx context.Context, tagName string) (int64, *srvdef.SrvErr) {
+ // 先按名称查询,存在则直接复用本项目标签ID。
+ tag, err := playlettagmod.FindOnePlayletTag(ctx, bson.M{playlettagmod.FN.PlayletTag.Name: tagName})
+ if err == nil {
+ return tag.Id, nil
+ }
+ if err.Code != srvdef.ErrNoData {
+ return 0, err
+ }
+ // 标签不存在时创建默认上架标签,方便导入后的短剧直接关联。
+ tag = &playlettagmod.PlayletTag{
+ Name: tagName,
+ Status: true,
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(nil, tag.Name)
+ tag.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
+ exist, _ := playlettagmod.FindOnePlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Slug: slug,
+ })
+ return exist != nil
+ })
+ if err = playlettagmod.InsertOnePlayletTag(ctx, tag); err != nil {
+ return 0, err
+ }
+ return tag.Id, nil
+}
+
+// buildPlayletFromMedia 将媒资中心主资源转换为本项目 playlet 主表模型
+func buildPlayletFromMedia(media *proto.PlayletResourceListItem, tagIds []int64, operatorId int64, operatorName string) *playletmod.Playlet {
+ // 封面优先使用竖图,适配短剧列表和详情页常见展示比例。
+ cover := strings.TrimSpace(media.VerticalCover)
+ if cover == "" {
+ // 竖图缺失时回退到横图,避免短剧没有封面。
+ cover = strings.TrimSpace(media.HorizontalCover)
+ }
+ return &playletmod.Playlet{
+ // 保存媒资中心ID,用于重复导入拦截和后续追溯来源。
+ 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,
+ TagIds: tagIds,
+ ContentType: contentTypeFromMediaType(media.MediaType),
+ OperateType: playletmod.OperateTypePlaylet,
+ TotalEpisode: int64(media.TotalEpisode),
+ UpdateStatus: normalizePlayletUpdateStatus(media.UpdateStatus),
+ HeatValue: randomPlayletHeatValue(),
+ VirtualCountLike: randomPlayletVirtualLike(),
+ Status: playletmod.StatusDown,
+ OperatorId: operatorId,
+ OperatorName: operatorName,
+ }
+}
+
+func ensureImportedPlayletSlug(ctx context.Context, playlet *playletmod.Playlet) *srvdef.SrvErr {
+ if playlet == nil {
+ return srvdef.New(srvdef.ErrParamError, "短剧数据为空", "")
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(playlet.SeoInfo.Keyworld, playlet.Title)
+ playlet.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
+ exist, _ := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Slug: slug,
+ })
+ return exist != nil
+ })
+ return nil
+}
+
+func randomPlayletHeatValue() int64 {
+ return int64(rand.Intn(playletHeatMaxValue-playletHeatMinValue+1) + playletHeatMinValue)
+}
+
+func randomPlayletVirtualLike() int64 {
+ return int64(rand.Intn(playletVirtualLikeMaxValue-playletVirtualLikeMinValue+1) + playletVirtualLikeMinValue)
+}
+
+// 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 == "" {
+ // m3u8_url 缺失时回退到 urls,兼容不同资源格式。
+ 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 {
+ // 媒资中心章节ID是最稳定的来源章节标识,优先用它做幂等去重。
+ 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 {
+ // 老数据如果没有媒资章节ID,则按集数去重,避免同一集重复入库。
+ 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 != "" {
+ // 集数缺失时使用资源hash兜底。
+ if _, ok := seenHashIds[hashId]; ok {
+ log.Info("跳过重复短剧媒资章节", log.Any("hashId", hashId))
+ return true
+ }
+ seenHashIds[hashId] = struct{}{}
+ return false
+ }
+ // 最后用播放地址兜底,避免无ID、无集数、无hash的重复资源写入。
+ 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 从后台登录态解析当前操作人,避免与 srv_playlet 循环依赖。
+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
+}
diff --git a/web/service/srv_mediaresource/serial_sync.go b/web/service/srv_mediaresource/serial_sync.go
new file mode 100644
index 0000000..9c895fe
--- /dev/null
+++ b/web/service/srv_mediaresource/serial_sync.go
@@ -0,0 +1,525 @@
+package srv_mediaresource
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/redis"
+ "videoweb/web/api/proto"
+
+ goredis "github.com/go-redis/redis"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ // PlayletSerialSyncCronSpec 连载中短剧同步扫描频率:每1小时扫描一次,扫描阶段只投递 Redis 任务
+ PlayletSerialSyncCronSpec = "0 0 * * * ?"
+
+ // playletSerialSyncQueueKeyFmt 解耦定时扫描和三方同步调用,避免 cron 扫描过程直接阻塞在外部接口上
+ playletSerialSyncQueueKeyFmt = "playlet:serial:sync:queue:%d"
+ playletSerialSyncLockKeyFmt = "playlet:serial:sync:lock:%d:%d"
+ playletSerialSyncJHLockKeyFmt = "playlet:serial:sync:jh:lock:%d:%d"
+ playletSerialSyncLockTTL = 55 * time.Minute
+ playletSerialSyncPopWait = 5 * time.Second
+ playletSerialSyncRetryWait = time.Second
+ playletSerialSyncBatchSize = int64(100)
+ playletManualSyncMaxCount = 100
+)
+
+func playletSerialSyncQueueKey() string {
+ return fmt.Sprintf(playletSerialSyncQueueKeyFmt, pubdef.AppId)
+}
+
+func playletSerialSyncLockKey(mediaId uint) string {
+ return fmt.Sprintf(playletSerialSyncLockKeyFmt, pubdef.AppId, mediaId)
+}
+
+func playletSerialSyncJHLockKey(mediaId uint) string {
+ return fmt.Sprintf(playletSerialSyncJHLockKeyFmt, pubdef.AppId, mediaId)
+}
+
+type playletSerialSyncJob struct {
+ PlayletId int64 `json:"playletId"`
+ MediaCenterId uint `json:"mediaCenterId"`
+ MediaSource string `json:"mediaSource"`
+ MediaSyncTime int64 `json:"mediaSyncTime"`
+ CreateTime int64 `json:"createTime"`
+}
+
+// EnqueueSerialPlayletSyncJobs 扫描连载中短剧并投递 Redis 同步任务
+//
+// 这里不直接请求三方媒资中心:扫描只负责发现任务,实际同步由后台消费者逐条处理
+// 可以降低定时任务执行时间,也方便 Redis 队列在三方接口短暂异常时继续缓冲任务
+func EnqueueSerialPlayletSyncJobs() {
+ ctx := context.Background()
+ filter := bson.M{
+ playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
+ playletmod.FN.Playlet.UpdateStatus: 1,
+ // 原创短剧由后台维护剧集,自动同步只处理媒资导入剧
+ playletmod.FN.Playlet.IsOriginal: bson.M{"$ne": true},
+ }
+ opts := options.Find().
+ SetSort(bson.D{{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1}, {Key: playletmod.FN.Playlet.Id, Value: 1}}).
+ SetLimit(playletSerialSyncBatchSize)
+ list, err := playletmod.FindManyPlaylet(ctx, filter, opts)
+ if err != nil {
+ log.Error("扫描连载中短剧失败", log.Any("err", err))
+ return
+ }
+ enqueued := 0
+ for _, item := range list {
+ if item == nil || item.Id == 0 || item.MediaCenterId == 0 || item.IsOriginal {
+ continue
+ }
+ if err := pushPlayletSerialSyncJob(ctx, item); err != nil {
+ log.Error("投递连载短剧同步任务失败", log.Any("playletId", item.Id), log.Any("mediaCenterId", item.MediaCenterId), log.E(err))
+ continue
+ }
+ enqueued++
+ }
+ log.Info("连载中短剧同步任务扫描完成", log.Any("scanCount", len(list)), log.Any("enqueued", enqueued))
+}
+
+// pushPlayletSerialSyncJob 将单部连载短剧投递到 Redis 队列
+func pushPlayletSerialSyncJob(ctx context.Context, playlet *playletmod.Playlet) error {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+ job := playletSerialSyncJob{
+ PlayletId: playlet.Id,
+ MediaCenterId: playlet.MediaCenterId,
+ MediaSource: playlet.MediaSource,
+ MediaSyncTime: playlet.MediaSyncTime,
+ CreateTime: time.Now().Unix(),
+ }
+ payload, err := json.Marshal(job)
+ if err != nil {
+ return err
+ }
+ return redis.RPush(playletSerialSyncQueueKey(), string(payload))
+}
+
+// runPlayletSerialSyncWorker 常驻消费连载短剧同步队列
+func runPlayletSerialSyncWorker(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ result, err := redis.Client().BLPop(playletSerialSyncPopWait, playletSerialSyncQueueKey()).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ continue
+ }
+ log.Error("消费连载短剧同步任务失败", log.E(err))
+ time.Sleep(playletSerialSyncRetryWait)
+ continue
+ }
+ if len(result) < 2 {
+ log.Error("连载短剧同步任务格式异常", log.Any("result", result))
+ continue
+ }
+ handlePlayletSerialSyncPayload(ctx, result[1])
+ }
+}
+
+// handlePlayletSerialSyncPayload 解析单个 Redis 任务并执行同步,panic 会被捕获避免 worker 退出
+func handlePlayletSerialSyncPayload(ctx context.Context, payload string) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Error("处理连载短剧同步任务异常", log.Any("err", err), log.Any("payload", payload))
+ }
+ }()
+ var job playletSerialSyncJob
+ if err := json.Unmarshal([]byte(payload), &job); err != nil {
+ log.Error("解析连载短剧同步任务失败", log.Any("payload", payload), log.E(err))
+ return
+ }
+ if job.PlayletId <= 0 || job.MediaCenterId == 0 {
+ log.Error("连载短剧同步任务参数异常", log.Any("job", job))
+ return
+ }
+ if _, reason := syncSerialPlaylet(ctx, job, false, false); reason != "" {
+ log.Error("连载短剧同步任务失败", log.Any("job", job), log.Any("reason", reason))
+ return
+ }
+ log.Info("连载短剧同步任务完成", log.Any("job", job))
+}
+
+// SyncPlayletChapters 手动同步指定短剧的媒资剧集,支持单/批量,允许已完结短剧补集或修正状态。
+func SyncPlayletChapters(ctx context.Context, req *proto.PlayletChapterSyncReq) (*proto.PlayletChapterSyncResp, *srvdef.SrvErr) {
+ ids := normalizePlayletChapterSyncIds(req)
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ if len(ids) > playletManualSyncMaxCount {
+ return nil, srvdef.New(srvdef.ErrParamError, "单次最多同步100部短剧", "")
+ }
+ resp := &proto.PlayletChapterSyncResp{
+ Total: len(ids),
+ List: make([]*proto.PlayletChapterSyncResult, 0, len(ids)),
+ }
+ for _, playletId := range ids {
+ item := syncOnePlayletChapters(ctx, playletId)
+ if item.Success {
+ resp.SuccessCount++
+ } else {
+ resp.FailedCount++
+ }
+ resp.List = append(resp.List, item)
+ }
+ return resp, nil
+}
+
+// normalizePlayletChapterSyncIds 过滤重复或无效短剧ID。
+func normalizePlayletChapterSyncIds(req *proto.PlayletChapterSyncReq) []int64 {
+ if req == nil {
+ return nil
+ }
+ result := make([]int64, 0, len(req.PlayletIds))
+ seen := make(map[int64]struct{}, len(req.PlayletIds))
+ for _, id := range req.PlayletIds {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
+
+// syncOnePlayletChapters 同步单部短剧的媒资剧集,单部失败不影响批量里其他短剧。
+func syncOnePlayletChapters(ctx context.Context, playletId int64) *proto.PlayletChapterSyncResult {
+ result := &proto.PlayletChapterSyncResult{
+ PlayletId: playletId,
+ }
+ playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: playletId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaSource, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaSyncTime, Value: 1},
+ }))
+ if err != nil {
+ result.Reason = playletChapterSyncErrReason(err)
+ return result
+ }
+ if playlet.MediaCenterId == 0 {
+ result.Reason = "非媒资导入短剧不能同步剧集"
+ return result
+ }
+ newChapterCount, reason := syncSerialPlaylet(ctx, playletSerialSyncJob{
+ PlayletId: playlet.Id,
+ MediaCenterId: playlet.MediaCenterId,
+ MediaSource: playlet.MediaSource,
+ MediaSyncTime: playlet.MediaSyncTime,
+ CreateTime: time.Now().Unix(),
+ }, true, true)
+ if reason != "" {
+ result.Reason = reason
+ return result
+ }
+ result.Success = true
+ result.NewChapterCount = newChapterCount
+ return result
+}
+
+// playletChapterSyncErrReason 转换单部短剧同步失败原因,避免批量结果里 reason 为空。
+func playletChapterSyncErrReason(err *srvdef.SrvErr) string {
+ if err == nil {
+ return ""
+ }
+ if err.Code == srvdef.ErrNoData {
+ return "短剧不存在"
+ }
+ if reason := err.GetTip(); reason != "" {
+ return reason
+ }
+ if reason := err.GetMsg(); reason != "" {
+ return reason
+ }
+ return err.Error()
+}
+
+// syncSerialPlaylet 更新主表集数/状态,只追加新增章节,不覆盖已有章节。
+// 手动同步使用 forceFullSync=true 全量拉取,便于恢复本地误删的旧章节;定时任务继续使用增量同步。
+func syncSerialPlaylet(ctx context.Context, job playletSerialSyncJob, allowCompleted bool, forceFullSync bool) (int, string) {
+ lockSource := job.MediaSource
+ locked, err := lockPlayletSerialSyncBySource(lockSource, job.MediaCenterId)
+ if err != nil {
+ log.Error("获取连载短剧同步锁失败", log.Any("job", job), log.E(err))
+ return 0, "获取同步锁失败"
+ }
+ if !locked {
+ return 0, "该媒资正在同步"
+ }
+ defer unlockPlayletSerialSyncBySource(lockSource, job.MediaCenterId)
+
+ playlet, serr := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: job.PlayletId,
+ playletmod.FN.Playlet.MediaCenterId: job.MediaCenterId,
+ })
+ if serr != nil {
+ if serr.Code == srvdef.ErrNoData {
+ return 0, "短剧不存在"
+ }
+ log.Error("查询连载短剧失败", log.Any("job", job), log.Any("err", serr))
+ return 0, "查询短剧失败"
+ }
+ if !allowCompleted && playlet.IsOriginal {
+ return 0, ""
+ }
+ if !allowCompleted && playlet.UpdateStatus != 1 {
+ return 0, ""
+ }
+
+ mediaSource := playlet.MediaSource
+ if mediaSource == "" {
+ mediaSource = job.MediaSource
+ }
+ ms := newPlayletMediaCenterBySource(mediaSource)
+ syncTime := playlet.MediaSyncTime
+ if forceFullSync {
+ syncTime = 0
+ }
+ result, err := ms.mediaSync(&playletMediaSyncReq{
+ Id: playlet.MediaCenterId,
+ SyncTime: syncTime,
+ })
+ if err != nil {
+ log.Error("同步连载短剧媒资失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.E(err))
+ return 0, "同步媒资失败"
+ }
+ if result.IsDelete {
+ return 0, "媒资不存在或已删除"
+ }
+ if result.Media == nil {
+ return 0, "媒资主资源为空"
+ }
+
+ newSyncTime := maxPlayletMediaContentUpdateTime(playlet.MediaSyncTime, result.MediaContentList)
+ if reason := updateSerialPlayletInfo(ctx, playlet, result.Media, newSyncTime); reason != "" {
+ return 0, reason
+ }
+ if len(result.MediaContentList) == 0 {
+ return 0, ""
+ }
+ chapters, reason := buildNewSerialPlayletChapters(ctx, playlet.Id, 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 playName := strings.TrimSpace(playlet.PlayName); playName != "" {
+ description := playletmod.BuildPlayNameDescription(playlet.Desc, playName, updateStatus, int64(media.TotalEpisode))
+ if description != "" {
+ setDoc[playletmod.FN.Playlet.Description] = description
+ setDoc[playletmod.FN.Playlet.SeoInfo.Description] = description
+ }
+ }
+ if err := playletmod.UpdateOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: playlet.Id,
+ }, bson.M{"$set": setDoc}); err != nil {
+ log.Error("更新连载短剧主表失败", log.Any("playletId", playlet.Id), log.Any("mediaCenterId", playlet.MediaCenterId), log.Any("err", err))
+ return "更新主表失败"
+ }
+ return ""
+}
+
+// buildNewSerialPlayletChapters 根据本地已存在章节做幂等过滤,只返回需要追加的新章节
+func buildNewSerialPlayletChapters(ctx context.Context, playletId int64, 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{}{}
+ }
+}
+
+// maxPlayletMediaContentUpdateTime 记录本次同步返回章节中的最大更新时间,作为下次三方增量同步点
+func maxPlayletMediaContentUpdateTime(syncTime int64, mediaContentList []playletMediaContent) int64 {
+ result := syncTime
+ for _, item := range mediaContentList {
+ if item.UpdateTime > result {
+ result = item.UpdateTime
+ }
+ }
+ return result
+}
+
+// normalizePlayletUpdateStatus 三方 update_status=0 在本项目按 2-已完结 保存
+func normalizePlayletUpdateStatus(updateStatus int) int {
+ if updateStatus == 0 {
+ return 2
+ }
+ return updateStatus
+}
+
+// applyPlayletDefaultPayToChapters 上架且连载中时,新增章节默认沿用剧级支付设置。
+func applyPlayletDefaultPayToChapters(playlet *playletmod.Playlet, chapters playletchaptermod.PlayletChapterList) {
+ if playlet == nil || len(chapters) == 0 {
+ return
+ }
+ if playlet.Status != playletmod.StatusUp || playlet.UpdateStatus != 1 {
+ return
+ }
+ payType := playletchaptermod.PayType(playlet.PayType)
+ singleCoins := playlet.SingleCoins
+ if payType != playletchaptermod.PayTypeCoin {
+ singleCoins = 0
+ }
+ for _, chapter := range chapters {
+ if chapter == nil {
+ continue
+ }
+ chapter.PayType = payType
+ chapter.SingleCoins = singleCoins
+ }
+}
+
+// lockPlayletSerialSync 锁定单个媒资ID,避免同一部连载短剧被多个 worker 同时同步
+func lockPlayletSerialSync(mediaId uint) (bool, error) {
+ return redis.Setnx_NewOK(playletSerialSyncLockKey(mediaId), true, playletSerialSyncLockTTL)
+}
+
+// unlockPlayletSerialSync 释放单个媒资ID的同步锁
+func unlockPlayletSerialSync(mediaId uint) {
+ if err := redis.Del(playletSerialSyncLockKey(mediaId)); err != nil {
+ log.Error("释放连载短剧同步锁失败", log.Any("mediaId", mediaId), log.E(err))
+ }
+}
+
+// lockPlayletSerialSyncBySource 按媒资来源锁定单个媒资ID
+func lockPlayletSerialSyncBySource(mediaSource string, mediaId uint) (bool, error) {
+ if mediaSource == playletmod.MediaSourceHG {
+ return lockPlayletSerialSync(mediaId)
+ }
+ return redis.Setnx_NewOK(playletSerialSyncJHLockKey(mediaId), true, playletSerialSyncLockTTL)
+}
+
+// unlockPlayletSerialSyncBySource 按媒资来源释放单个媒资ID的同步锁
+func unlockPlayletSerialSyncBySource(mediaSource string, mediaId uint) {
+ if mediaSource == playletmod.MediaSourceHG {
+ unlockPlayletSerialSync(mediaId)
+ return
+ }
+ if err := redis.Del(playletSerialSyncJHLockKey(mediaId)); err != nil {
+ log.Error("释放嘉华连载短剧同步锁失败", log.Any("mediaId", mediaId), log.E(err))
+ }
+}
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/cache.go b/web/service/srv_playlet/cache.go
new file mode 100644
index 0000000..9050238
--- /dev/null
+++ b/web/service/srv_playlet/cache.go
@@ -0,0 +1,8 @@
+package srv_playlet
+
+import "videoweb/pub/common/playletcache"
+
+// clearAppPlayletHomeCache 清除app首页短剧缓存
+func clearAppPlayletHomeCache() {
+ playletcache.ClearHomeCache()
+}
diff --git a/web/service/srv_playlet/operator.go b/web/service/srv_playlet/operator.go
new file mode 100644
index 0000000..5b832b2
--- /dev/null
+++ b/web/service/srv_playlet/operator.go
@@ -0,0 +1,57 @@
+package srv_playlet
+
+import (
+ "context"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/playletmod"
+ "videoweb/web/middleware/auth"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// ResolveOperator 解析当前后台登录操作人,供短剧写操作与媒资入库共用。
+func ResolveOperator(ctx context.Context) (int64, string) {
+ return resolvePlayletOperator(ctx)
+}
+
+// resolvePlayletOperator 从后台登录态解析当前操作人。
+func resolvePlayletOperator(ctx context.Context) (int64, string) {
+ c, ok := ctx.(*gin.Context)
+ if !ok || c == nil {
+ return 0, ""
+ }
+ accId := auth.GetAccount(c)
+ if accId <= 0 {
+ return 0, ""
+ }
+ acc, err := mwacc.FindOneAccByFilter(c, bson.M{
+ mwacc.FN.MwAcc.Id: accId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: mwacc.FN.MwAcc.Id, Value: 1},
+ {Key: mwacc.FN.MwAcc.UserName, Value: 1},
+ }))
+ if err != nil || acc == nil {
+ return accId, ""
+ }
+ return acc.Id, acc.UserName
+}
+
+// applyPlayletOperatorToSetDoc 将当前操作人写入更新文档。
+func applyPlayletOperatorToSetDoc(ctx context.Context, setDoc bson.M) {
+ if setDoc == nil {
+ return
+ }
+ operatorId, operatorName := resolvePlayletOperator(ctx)
+ setDoc[playletmod.FN.Playlet.OperatorId] = operatorId
+ setDoc[playletmod.FN.Playlet.OperatorName] = operatorName
+}
+
+// applyPlayletOperatorToDoc 将当前操作人写入新增文档。
+func applyPlayletOperatorToDoc(ctx context.Context, doc *playletmod.Playlet) {
+ if doc == nil {
+ return
+ }
+ doc.OperatorId, doc.OperatorName = resolvePlayletOperator(ctx)
+}
diff --git a/web/service/srv_playlet/playlet.go b/web/service/srv_playlet/playlet.go
new file mode 100644
index 0000000..17dbb3a
--- /dev/null
+++ b/web/service/srv_playlet/playlet.go
@@ -0,0 +1,1776 @@
+package srv_playlet
+
+import (
+ "context"
+ "math/rand"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/app/service/reportser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletchaptercollectmod"
+ "videoweb/pub/models/playletchapterlikemod"
+ "videoweb/pub/models/playletchaptermod"
+ "videoweb/pub/models/playletchapterunlockmod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/pub/pubdef"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_mediaresource"
+ "videoweb/web/service/srv_search"
+ "videoweb/web/skd/playletwatermark"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultPageSize int64 = 10
+ maxPageSize int64 = 100
+)
+
+// 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 && req.SortType == proto.PlayletListSortRecommendRank {
+ list, err = findPlayletsSortedByCategoryRank(ctx, filter, *req.CategoryId, (pageNum-1)*pageSize, pageSize, playletmod.CategoryRankAdminSort())
+ } else {
+ list, err = playletmod.FindManyPlaylet(ctx, filter, options.Find().
+ SetSort(buildPlayletListSort(req.SortType)).
+ 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
+ }
+ items = append(items, playletToWebResp(item, categoryNameMap, themeNameMap, tagNameMap, req.CategoryId))
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlaylet 新增短剧
+func AddPlaylet(ctx context.Context, req *proto.PlayletAddReq) (*proto.PlayletAddResp, *srvdef.SrvErr) {
+ if req == nil || strings.TrimSpace(req.Title) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "标题不能为空", "")
+ }
+ categoryBinds, err := resolveAddPlayletCategoryBinds(ctx, req.CategoryBinds, req.CategoryIds)
+ if 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
+ }
+ banner := strings.TrimSpace(req.Banner)
+ if req.ShowOnBanner && banner == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "显示在banner时,banner图不能为空", "")
+ }
+ if err := validatePlayletTagIds(ctx, req.TagIds); err != nil {
+ return nil, err
+ }
+ payType, singleCoins, srvErr := normalizePlayletPaySetting(req.PayType, &req.SingleCoins, true)
+ if srvErr != nil {
+ return nil, srvErr
+ }
+ operateType, srvErr := normalizePlayletOperateType(req.OperateType, true)
+ if srvErr != nil {
+ return nil, srvErr
+ }
+ doc := &playletmod.Playlet{
+ CategoryIds: playletmod.CategoryIdsFromBinds(categoryBinds),
+ CategoryBinds: categoryBinds,
+ ThemeIds: themeIds,
+ Title: strings.TrimSpace(req.Title),
+ PlayName: strings.TrimSpace(req.PlayName),
+ Slug: strings.TrimSpace(req.Slug),
+ Desc: strings.TrimSpace(req.Desc),
+ Anchor: strings.TrimSpace(req.Anchor),
+ Cover: req.Cover,
+ Preview: req.Preview,
+ TagIds: req.TagIds,
+ ContentType: req.ContentType,
+ OperateType: operateType,
+ PayType: payType,
+ SingleCoins: singleCoins,
+ TotalEpisode: req.TotalEpisode,
+ TopRank: req.TopRank,
+ RecommendRank: req.RecommendRank,
+ Status: req.Status,
+ IsFeatured: req.IsFeatured,
+ IsOriginal: true, // 管理后台新增的短剧都是原创
+ IsRecommend: req.IsRecommend,
+ ShowOnBanner: req.ShowOnBanner,
+ Banner: banner,
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ applyPlayletDescription(doc)
+ if doc.Status == playletmod.StatusUp {
+ doc.OnlineAt = resolvePlayletOnlineAt(req.OnlineAt, time.Now())
+ }
+ if doc.TagIds == nil {
+ doc.TagIds = make([]int64, 0)
+ }
+ applyPlayletOperatorToDoc(ctx, doc)
+ if err := ensurePlayletSlug(ctx, doc, 0); err != nil {
+ return nil, err
+ }
+ if err := playletmod.InsertOnePlaylet(ctx, doc); err != nil {
+ return nil, err
+ }
+ go srv_mediaresource.FillPlayletCompressedCover(context.Background(), doc)
+ clearAppPlayletHomeCache()
+ // 上架短剧异步写入 ES(含 SEO),避免阻塞后台接口。
+ if doc.Status == playletmod.StatusUp {
+ syncId := doc.Id
+ go func() {
+ if err := srv_search.SyncPlayletStatusToES(context.Background(), []int64{syncId}, playletmod.StatusUp); err != nil {
+ log.Error("短剧新增同步ES失败", log.Any("短剧ID", syncId), log.E(err))
+ }
+ }()
+ }
+ return &proto.PlayletAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlaylet 更新短剧
+func UpdatePlaylet(ctx context.Context, req *proto.PlayletUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ if req.ShowOnBanner != nil || req.Banner != nil {
+ exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ showOnBanner := exist.ShowOnBanner
+ banner := exist.Banner
+ if req.ShowOnBanner != nil {
+ showOnBanner = *req.ShowOnBanner
+ }
+ if req.Banner != nil {
+ banner = strings.TrimSpace(*req.Banner)
+ }
+ if showOnBanner && banner == "" {
+ return srvdef.New(srvdef.ErrParamError, "显示在banner时,banner图不能为空", "")
+ }
+ }
+ setDoc := bson.M{playletmod.FN.Playlet.UpdatedAt: time.Now()}
+ if req.Status != nil {
+ setDoc[playletmod.FN.Playlet.Status] = *req.Status
+ setDoc[playletmod.FN.Playlet.ReviewAt] = time.Now()
+ }
+ if req.IsFeatured != nil {
+ setDoc[playletmod.FN.Playlet.IsFeatured] = *req.IsFeatured
+ }
+ 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.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 {
+ setDoc[playletmod.FN.Playlet.Title] = strings.TrimSpace(*req.Title)
+ }
+ if req.PlayName != nil {
+ setDoc[playletmod.FN.Playlet.PlayName] = strings.TrimSpace(*req.PlayName)
+ }
+ 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
+ exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ if exist == nil || strings.TrimSpace(exist.Cover) != newCover {
+ // 封面变更后清空压缩封面,由后台任务按新原图重新压缩。
+ setDoc[playletmod.FN.Playlet.CompressedCover] = ""
+ coverChanged = true
+ }
+ }
+ if req.Preview != nil {
+ setDoc[playletmod.FN.Playlet.Preview] = *req.Preview
+ }
+ if req.TagIds != nil {
+ if err := validatePlayletTagIds(ctx, *req.TagIds); err != nil {
+ return err
+ }
+ setDoc[playletmod.FN.Playlet.TagIds] = *req.TagIds
+ }
+ if req.ContentType != nil {
+ setDoc[playletmod.FN.Playlet.ContentType] = *req.ContentType
+ }
+ if req.OperateType != nil {
+ operateType, srvErr := normalizePlayletOperateType(*req.OperateType, false)
+ if srvErr != nil {
+ return srvErr
+ }
+ setDoc[playletmod.FN.Playlet.OperateType] = operateType
+ }
+ if req.PayType != nil || req.SingleCoins != nil {
+ existPayType := playletmod.PayTypeFree
+ existCoins := int64(0)
+ if req.PayType == nil || req.SingleCoins == nil {
+ exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ existPayType = exist.PayType
+ existCoins = exist.SingleCoins
+ }
+ payType := existPayType
+ if req.PayType != nil {
+ payType = *req.PayType
+ }
+ coins := &existCoins
+ if req.SingleCoins != nil {
+ coins = req.SingleCoins
+ }
+ normalizedPayType, normalizedCoins, srvErr := normalizePlayletPaySetting(payType, coins, true)
+ if srvErr != nil {
+ return srvErr
+ }
+ setDoc[playletmod.FN.Playlet.PayType] = normalizedPayType
+ setDoc[playletmod.FN.Playlet.SingleCoins] = normalizedCoins
+ }
+ 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)
+ }
+ exist, err := playletmod.FindOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ if req.CategoryBinds != nil || req.CategoryIds != nil {
+ categoryBinds, serr := resolvePlayletCategoryBinds(ctx, req.CategoryBinds, req.CategoryIds, exist.CategoryBinds)
+ if serr != nil {
+ return serr
+ }
+ setDoc[playletmod.FN.Playlet.CategoryBinds] = categoryBinds
+ setDoc[playletmod.FN.Playlet.CategoryIds] = playletmod.CategoryIdsFromBinds(categoryBinds)
+ }
+ applyPlayletOnlineAtOnUpdate(exist, req, setDoc, time.Now())
+ if err := preparePlayletSlugOnUpdate(ctx, req.Id, exist, req, setDoc); err != nil {
+ return err
+ }
+ applyPlayletDescriptionOnUpdate(exist, req, setDoc)
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ if err := playletmod.UpdateOnePlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); 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()
+ // 编辑后按当前上下架状态异步同步 ES(含 SEO、题材名等搜索字段)。
+ syncId := req.Id
+ go func() {
+ exist, err := playletmod.FindOnePlaylet(context.Background(), bson.M{
+ playletmod.FN.Playlet.Id: syncId,
+ }, options.FindOne().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Status, Value: 1},
+ }))
+ if err != nil || exist == nil {
+ if err != nil {
+ log.Error("短剧更新后查询状态失败,跳过ES同步", log.Any("短剧ID", syncId), log.E(err))
+ }
+ return
+ }
+ if err := srv_search.SyncPlayletStatusToES(context.Background(), []int64{syncId}, exist.Status); err != nil {
+ log.Error("短剧更新同步ES失败", log.Any("短剧ID", syncId), log.Any("状态", exist.Status), log.E(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] = resolvePlayletOnlineAt(req.OnlineAt, now)
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ // 单条和批量都走 Mongo 批量更新,避免批量操作时逐条访问数据库。
+ if err := playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ // 上架会同步写入 ES,ES Bulk 带 refresh 且无超时,同步等待会把接口卡住。
+ // Mongo 已更新成功后异步同步 ES,避免 status=1 时接口无响应。
+ syncIds := append([]int64(nil), ids...)
+ syncStatus := req.Status
+ go func() {
+ if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, syncStatus); err != nil {
+ log.Error("短剧状态同步ES失败", log.Any("短剧ID", syncIds), log.Any("状态", syncStatus), log.E(err))
+ }
+ }()
+ if flag := reportser.PlayletStatusFlag(req.Status); flag != 0 {
+ reportPlayletStatusEvents(ctx, ids, flag)
+ }
+ return nil
+}
+
+func reportPlayletStatusEvents(ctx context.Context, ids []int64, flag int64) {
+ if len(ids) == 0 {
+ return
+ }
+ 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.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.CategoryIds, Value: 1},
+ {Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
+ }))
+ if err != nil {
+ log.Error("短剧状态变更上报查询失败", log.Any("短剧ID", ids), log.E(err))
+ return
+ }
+ for _, item := range list {
+ reportser.ReportPlayletStatusChange(item, flag)
+ }
+}
+
+// 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不能为空", "")
+ }
+ // isRecommend 是明确的 bool 开关,false 表示取消推荐。
+ setDoc := bson.M{
+ playletmod.FN.Playlet.IsRecommend: req.IsRecommend,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// 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不能为空", "")
+ }
+ // isFeatured 是爆剧开关,直接批量写入请求值,false 表示取消爆剧。
+ setDoc := bson.M{
+ playletmod.FN.Playlet.IsFeatured: req.IsFeatured,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// SetPlayletOperateType 单/批量设置短剧运营类型
+func SetPlayletOperateType(ctx context.Context, req *proto.PlayletOperateTypeSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "")
+ }
+ operateType, srvErr := normalizePlayletOperateType(req.OperateType, false)
+ if srvErr != nil {
+ return srvErr
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.OperateType: operateType,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// 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不能为空", "")
+ }
+ // showOnBanner 是首页banner展示开关,只负责批量设置字段值。
+ setDoc := bson.M{
+ playletmod.FN.Playlet.ShowOnBanner: req.ShowOnBanner,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// 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
+ }
+ // themeIds 是短剧多题材字段,按请求覆盖原题材集合。
+ setDoc := bson.M{
+ playletmod.FN.Playlet.ThemeIds: themeIds,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// 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.CategoryBinds == nil && req.CategoryIds == nil {
+ return srvdef.New(srvdef.ErrParamError, "分类不能为空", "")
+ }
+ categoryBinds, err := resolvePlayletCategoryBinds(ctx, req.CategoryBinds, req.CategoryIds, nil)
+ if err != nil {
+ return err
+ }
+ // 按请求覆盖原分类集合;空数组表示清空分类。
+ setDoc := bson.M{
+ playletmod.FN.Playlet.CategoryBinds: categoryBinds,
+ playletmod.FN.Playlet.CategoryIds: playletmod.CategoryIdsFromBinds(categoryBinds),
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// 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 := normalizePlayletTagIds(*req.TagIds)
+ if err != nil {
+ return err
+ }
+ if err := validatePlayletTagIds(ctx, tagIds); err != nil {
+ return err
+ }
+ // tagIds 是短剧多标签字段,按请求覆盖原标签集合;空数组表示清空标签。
+ setDoc := bson.M{
+ playletmod.FN.Playlet.TagIds: tagIds,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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
+}
+
+// SetPlayletVirtualLike 单/批量设置短剧虚拟点赞数
+func SetPlayletVirtualLike(ctx context.Context, req *proto.PlayletVirtualLikeSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ // 去重并过滤无效ID,避免同一短剧在一次批量请求中被重复赋值。
+ 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,
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ 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()
+ syncIds := append([]int64(nil), ids...)
+ go func() {
+ // 虚拟点赞数会写入 ES 的展示点赞数字段,后台保存成功后异步刷新 ES。
+ if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, playletmod.StatusUp); err != nil {
+ log.Error("短剧虚拟点赞数同步ES失败", log.Any("短剧ID", syncIds), log.E(err))
+ }
+ }()
+ return nil
+}
+
+// randomUniqueVirtualLikeValues 从[min,max]范围内随机取count个不重复的虚拟点赞数
+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)
+ // 使用Floyd采样算法,避免在大范围随机时为了去重反复重试。
+ if _, ok := selected[picked]; ok {
+ selected[current] = struct{}{}
+ result = append(result, current)
+ continue
+ }
+ selected[picked] = struct{}{}
+ result = append(result, picked)
+ }
+ rng.Shuffle(len(result), func(i, j int) {
+ result[i], result[j] = result[j], result[i]
+ })
+ return result
+}
+
+// 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无效", "")
+ }
+ 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: req.Md5,
+ Hash: 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: req.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
+}
+
+// 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 {
+ 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] = *req.Md5
+ }
+ if req.Hash != nil {
+ setDoc[playletchaptermod.FN.PlayletChapter.Hash] = *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] = *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 {
+ setDoc[playletchaptermod.FN.PlayletChapter.PayType] = *req.PayType
+ }
+ if req.SingleCoins != nil {
+ 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},
+ )
+}
+
+// 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, "", "")
+ }
+ if _, serr := playletchapterlikemod.DeleteManyPlayletChapterLike(ctx, bson.M{
+ playletchapterlikemod.FN.PlayletChapterLike.ChapterId: bson.M{"$in": ids},
+ }); serr != nil {
+ log.Warn("删除剧集后清理点赞关系失败", log.Any("chapterIds", ids), log.E(serr))
+ }
+ if _, serr := playletchaptercollectmod.DeleteManyPlayletChapterCollect(ctx, bson.M{
+ playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: bson.M{"$in": ids},
+ }); serr != nil {
+ log.Warn("删除剧集后清理收藏关系失败", log.Any("chapterIds", ids), log.E(serr))
+ }
+ if _, serr := playletchapterunlockmod.DeleteManyPlayletChapterUnlock(ctx, bson.M{
+ playletchapterunlockmod.FN.PlayletChapterUnlock.ChapterId: bson.M{"$in": ids},
+ }); serr != nil {
+ log.Warn("删除剧集后清理解锁记录失败", log.Any("chapterIds", ids), log.E(serr))
+ }
+ return nil
+}
+
+// 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},
+ )
+}
+
+// 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, srvErr := normalizePlayletPaySetting(req.PayType, req.SingleCoins, true)
+ if srvErr != nil {
+ return srvErr
+ }
+ setDoc := bson.M{
+ playletmod.FN.Playlet.PayType: payType,
+ playletmod.FN.Playlet.SingleCoins: singleCoins,
+ playletmod.FN.Playlet.UpdatedAt: time.Now(),
+ }
+ applyPlayletOperatorToSetDoc(ctx, setDoc)
+ return playletmod.UpdateManyPlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: bson.M{"$in": ids}},
+ bson.M{"$set": setDoc},
+ )
+}
+
+// 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
+}
+
+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 {
+ filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{*req.CategoryId}}
+ }
+ if req.ThemeId != nil {
+ filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{*req.ThemeId}}
+ }
+ if req.TagId != nil {
+ filter[playletmod.FN.Playlet.TagIds] = bson.M{"$in": []int64{*req.TagId}}
+ }
+ if req.ContentType != nil {
+ filter[playletmod.FN.Playlet.ContentType] = *req.ContentType
+ }
+ if req.OperateType != nil {
+ if *req.OperateType == playletmod.OperateTypeComic {
+ filter[playletmod.FN.Playlet.OperateType] = playletmod.OperateTypeComic
+ } else {
+ // 历史数据可能没有该字段,按短剧处理
+ filter[playletmod.FN.Playlet.OperateType] = bson.M{"$ne": playletmod.OperateTypeComic}
+ }
+ }
+ 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.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 slug := strings.TrimSpace(req.Slug); slug != "" {
+ filter[playletmod.FN.Playlet.Slug] = slug
+ }
+ if operatorName := strings.TrimSpace(req.OperatorName); operatorName != "" {
+ filter[playletmod.FN.Playlet.OperatorName] = bson.M{"$regex": regexp.QuoteMeta(operatorName)}
+ }
+ return filter
+}
+
+// dedupePositiveInt64Ids 过滤无效ID并去重
+func dedupePositiveInt64Ids(ids []int64) []int64 {
+ result := make([]int64, 0, len(ids))
+ seen := make(map[int64]struct{}, len(ids))
+ for _, id := range ids {
+ // 只有正数ID才是有效的 Mongo 业务自增ID。
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
+
+// isValidPlayletStatus 校验短剧上下架状态
+func isValidPlayletStatus(status playletmod.Status) bool {
+ return status == playletmod.StatusDown || status == playletmod.StatusUp
+}
+
+// resolvePlayletOnlineAt 上架时间未传或为零时使用当前时间
+func resolvePlayletOnlineAt(passed *time.Time, now time.Time) time.Time {
+ if passed != nil && !passed.IsZero() {
+ return *passed
+ }
+ return now
+}
+
+// applyPlayletOnlineAtOnUpdate 编辑短剧时写入上架时间。
+// 新上架未传时间则用当前时间;已上架仅在显式传入时更新。
+func applyPlayletOnlineAtOnUpdate(exist *playletmod.Playlet, req *proto.PlayletUpdateReq, setDoc bson.M, now time.Time) {
+ if req == nil || setDoc == nil {
+ return
+ }
+ goingUp := req.Status != nil && *req.Status == playletmod.StatusUp && (exist == nil || exist.Status != playletmod.StatusUp)
+ if goingUp {
+ setDoc[playletmod.FN.Playlet.OnlineAt] = resolvePlayletOnlineAt(req.OnlineAt, now)
+ return
+ }
+ if req.OnlineAt != nil && !req.OnlineAt.IsZero() {
+ setDoc[playletmod.FN.Playlet.OnlineAt] = *req.OnlineAt
+ }
+}
+
+// FillEmptyPlayletOnlineAt 将已上架但上架时间为空的短剧,回填为该条修改时间
+func FillEmptyPlayletOnlineAt(ctx context.Context) (*proto.PlayletOnlineAtFillResp, *srvdef.SrvErr) {
+ filter := emptyOnlineAtUpPlayletFilter()
+ list, err := playletmod.FindManyPlaylet(ctx, filter, options.Find().SetProjection(bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletOnlineAtFillResp{}
+ if len(list) == 0 {
+ return resp, nil
+ }
+ writes := make([]mongo.WriteModel, 0, len(list))
+ ids := make([]int64, 0, len(list))
+ for _, item := range list {
+ if item == nil || item.Id <= 0 {
+ continue
+ }
+ onlineAt := item.UpdatedAt
+ if onlineAt.IsZero() {
+ onlineAt = item.CreatedAt
+ }
+ if onlineAt.IsZero() {
+ continue
+ }
+ writes = append(writes, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{playletmod.FN.Playlet.Id: item.Id}).
+ SetUpdate(bson.M{"$set": bson.M{
+ playletmod.FN.Playlet.OnlineAt: onlineAt,
+ }}))
+ ids = append(ids, item.Id)
+ }
+ if len(writes) == 0 {
+ return resp, nil
+ }
+ if err := playletmod.BulkWritePlaylet(ctx, writes); err != nil {
+ return nil, err
+ }
+ resp.UpdatedCount = int64(len(ids))
+ clearAppPlayletHomeCache()
+ syncIds := append([]int64(nil), ids...)
+ go func() {
+ if err := srv_search.SyncPlayletStatusToES(context.Background(), syncIds, playletmod.StatusUp); err != nil {
+ log.Error("回填上架时间同步ES失败", log.Any("短剧ID", syncIds), log.E(err))
+ }
+ }()
+ return resp, nil
+}
+
+func emptyOnlineAtUpPlayletFilter() bson.M {
+ return bson.M{
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ "$or": []bson.M{
+ {playletmod.FN.Playlet.OnlineAt: bson.M{"$exists": false}},
+ {playletmod.FN.Playlet.OnlineAt: nil},
+ {playletmod.FN.Playlet.OnlineAt: time.Time{}},
+ },
+ }
+}
+
+// isValidPlayletChapterPayType 校验章节支付方式
+func isValidPlayletChapterPayType(payType playletchaptermod.PayType) bool {
+ return payType == playletchaptermod.PayTypeFree ||
+ payType == playletchaptermod.PayTypeCoin ||
+ payType == playletchaptermod.PayTypeVip
+}
+
+func normalizePlayletOperateType(operateType playletmod.OperateType, allowDefault bool) (playletmod.OperateType, *srvdef.SrvErr) {
+ if operateType == 0 && allowDefault {
+ return playletmod.OperateTypePlaylet, nil
+ }
+ if !playletmod.IsValidOperateType(operateType) {
+ return 0, srvdef.New(srvdef.ErrParamError, "运营类型无效", "")
+ }
+ return operateType, nil
+}
+
+// isValidPlayletPayType 校验短剧默认支付方式
+func isValidPlayletPayType(payType playletmod.PayType) bool {
+ return payType == playletmod.PayTypeFree ||
+ payType == playletmod.PayTypeCoin ||
+ payType == playletmod.PayTypeVip
+}
+
+// normalizePlayletPaySetting 规范化短剧默认支付方式;非金币时单集金币清零。
+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
+}
+
+// buildPlayletListSort 后台列表排序,均为降序;未传或非法值按创建时间。
+func buildPlayletListSort(sortType proto.PlayletListSortType) bson.D {
+ sortKey := playletmod.FN.Playlet.CreatedAt
+ switch sortType {
+ case proto.PlayletListSortRecommendRank:
+ sortKey = playletmod.FN.Playlet.RecommendRank
+ case proto.PlayletListSortUpdatedAt:
+ sortKey = playletmod.FN.Playlet.UpdatedAt
+ case proto.PlayletListSortCountLike:
+ sortKey = playletmod.FN.Playlet.CountLike
+ case proto.PlayletListSortCountBrowse:
+ sortKey = playletmod.FN.Playlet.CountBrowse
+ case proto.PlayletListSortCountCollect:
+ sortKey = playletmod.FN.Playlet.CountCollect
+ case proto.PlayletListSortOnlineAt:
+ sortKey = playletmod.FN.Playlet.OnlineAt
+ }
+ return bson.D{
+ {Key: sortKey, Value: -1},
+ {Key: playletmod.FN.Playlet.Id, Value: -1},
+ }
+}
+
+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 applyPlayletDescription(doc *playletmod.Playlet) {
+ if doc == nil {
+ return
+ }
+ description := playletmod.BuildPlayNameDescription(doc.Desc, doc.PlayName, doc.UpdateStatus, doc.TotalEpisode)
+ if description == "" {
+ return
+ }
+ doc.Description = description
+ doc.SeoInfo.Description = description
+}
+
+func applyPlayletDescriptionOnUpdate(exist *playletmod.Playlet, req *proto.PlayletUpdateReq, setDoc bson.M) {
+ if exist == nil || req == nil || setDoc == nil {
+ return
+ }
+ playName := strings.TrimSpace(exist.PlayName)
+ if req.PlayName != nil {
+ playName = strings.TrimSpace(*req.PlayName)
+ }
+ if playName == "" {
+ return
+ }
+ desc := exist.Desc
+ if req.Desc != nil {
+ desc = strings.TrimSpace(*req.Desc)
+ }
+ totalEpisode := exist.TotalEpisode
+ if req.TotalEpisode != nil {
+ totalEpisode = *req.TotalEpisode
+ }
+ description := playletmod.BuildPlayNameDescription(desc, playName, exist.UpdateStatus, totalEpisode)
+ if description == "" {
+ return
+ }
+ setDoc[playletmod.FN.Playlet.Description] = description
+ seo := exist.SeoInfo
+ if req.SeoInfo != nil {
+ if current, ok := setDoc["seoInfo"].(pubdef.SeoValue); ok {
+ seo = current
+ } else {
+ seo = normalizeSeoValue(*req.SeoInfo)
+ }
+ }
+ seo = normalizeSeoValue(seo)
+ seo.Description = description
+ setDoc["seoInfo"] = seo
+}
+
+func playletToWebResp(item *playletmod.Playlet, categoryNameMap map[int64]string, themeNameMap map[int64]string, tagNameMap map[int64]string, categoryId *int64) *proto.PlayletListResp {
+ binds := playletmod.EffectiveCategoryBinds(item)
+ var filterCategoryId int64
+ if categoryId != nil {
+ filterCategoryId = *categoryId
+ }
+ return &proto.PlayletListResp{
+ Id: item.Id,
+ Status: item.Status,
+ IsFeatured: item.IsFeatured,
+ IsOriginal: item.IsOriginal,
+ IsRecommend: item.IsRecommend,
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: item.Banner,
+ ReviewAt: item.ReviewAt,
+ OnlineAt: item.OnlineAt,
+ CategoryIds: playletmod.CategoryIdsFromBinds(binds),
+ CategoryNames: categoryNamesFromMap(playletmod.CategoryIdsFromBinds(binds), categoryNameMap),
+ CategoryBinds: toProtoCategoryBinds(binds),
+ CategoryRank: playletmod.CategoryRankOf(binds, filterCategoryId),
+ ThemeIds: item.ThemeIds,
+ ThemeNames: themeNamesFromMap(item.ThemeIds, themeNameMap),
+ Title: item.Title,
+ PlayName: item.PlayName,
+ Description: item.Description,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: item.Cover,
+ Preview: item.Preview,
+ TagIds: item.TagIds,
+ TagNames: tagNamesFromMap(item.TagIds, tagNameMap),
+ ContentType: item.ContentType,
+ OperateType: playletmod.NormalizeOperateType(item.OperateType),
+ PayType: item.PayType,
+ SingleCoins: item.SingleCoins,
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ TopRank: item.TopRank,
+ RecommendRank: item.RecommendRank,
+ CountComment: item.CountComment,
+ CountBuy: item.CountBuy,
+ CountBrowse: item.CountBrowse,
+ HeatValue: item.HeatValue,
+ 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,
+ CompressedCover: item.CompressedCover,
+ }
+}
+
+func playletDisplayCountLike(item *playletmod.Playlet) int64 {
+ if item == nil {
+ return 0
+ }
+ return item.CountLike + item.VirtualCountLike
+}
+
+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 playletmod.CategoryIdsFromBinds(playletmod.EffectiveCategoryBinds(item)) {
+ if categoryId <= 0 {
+ continue
+ }
+ if _, ok := seen[categoryId]; ok {
+ continue
+ }
+ seen[categoryId] = struct{}{}
+ categoryIds = append(categoryIds, categoryId)
+ }
+ }
+ result := make(map[int64]string)
+ if len(categoryIds) == 0 {
+ return result, nil
+ }
+ categories, err := playletcategorymod.FindManyPlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$in": categoryIds},
+ })
+ if err != nil {
+ return nil, err
+ }
+ for _, category := range categories {
+ if category == nil {
+ continue
+ }
+ result[category.Id] = category.Name
+ }
+ return result, nil
+}
+
+// categoryNamesFromMap 按分类ID顺序返回分类名称。
+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)
+ if len(themeIds) == 0 {
+ return result, nil
+ }
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIds},
+ })
+ if err != nil {
+ return nil, err
+ }
+ for _, theme := range themes {
+ if theme == nil {
+ continue
+ }
+ result[theme.Id] = theme.Name
+ }
+ return result, nil
+}
+
+// themeNamesFromMap 按题材ID顺序返回题材名称
+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)
+ if len(tagIds) == 0 {
+ return result, nil
+ }
+ tags, err := playlettagmod.FindManyPlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Id: bson.M{"$in": tagIds},
+ })
+ if err != nil {
+ return nil, err
+ }
+ for _, tag := range tags {
+ if tag == nil {
+ continue
+ }
+ result[tag.Id] = tag.Name
+ }
+ return result, nil
+}
+
+// tagNamesFromMap 按标签ID顺序返回标签名称
+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 resolveAddPlayletCategoryBinds(ctx context.Context, binds []proto.PlayletCategoryBind, ids []int64) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) {
+ if len(binds) > 0 {
+ return resolvePlayletCategoryBinds(ctx, &binds, nil, nil)
+ }
+ return resolvePlayletCategoryBinds(ctx, nil, &ids, nil)
+}
+
+func resolvePlayletCategoryBinds(ctx context.Context, binds *[]proto.PlayletCategoryBind, ids *[]int64, exist []playletmod.PlayletCategoryBind) ([]playletmod.PlayletCategoryBind, *srvdef.SrvErr) {
+ if binds != nil {
+ result := playletmod.NormalizeCategoryBinds(toModCategoryBinds(*binds))
+ if err := validatePlayletCategoryIds(ctx, playletmod.CategoryIdsFromBinds(result)); err != nil {
+ return nil, err
+ }
+ return result, nil
+ }
+ if ids != nil {
+ categoryIds, err := normalizePlayletCategoryIds(*ids)
+ if err != nil {
+ return nil, err
+ }
+ if err := validatePlayletCategoryIds(ctx, categoryIds); err != nil {
+ return nil, err
+ }
+ if len(exist) > 0 {
+ return playletmod.MergeCategoryBindsByIds(exist, categoryIds), nil
+ }
+ return playletmod.CategoryBindsFromIds(categoryIds), nil
+ }
+ return make([]playletmod.PlayletCategoryBind, 0), nil
+}
+
+func toModCategoryBinds(binds []proto.PlayletCategoryBind) []playletmod.PlayletCategoryBind {
+ result := make([]playletmod.PlayletCategoryBind, 0, len(binds))
+ for _, bind := range binds {
+ result = append(result, playletmod.PlayletCategoryBind{Id: bind.Id, Rank: bind.Rank})
+ }
+ return result
+}
+
+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 findPlayletsSortedByCategoryRank(ctx context.Context, filter bson.M, categoryId int64, skip, limit int64, sort bson.D) (playletmod.PlayletList, *srvdef.SrvErr) {
+ return playletmod.FindManyPlayletByCategoryRank(ctx, filter, categoryId, skip, limit, sort, nil)
+}
+
+// normalizePlayletCategoryIds 校验并去重分类ID;空数组表示不绑定任何分类。
+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
+}
+
+// normalizePlayletTagIds 校验并去重标签ID
+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 chapterToWebResp(item *playletchaptermod.PlayletChapter) *proto.PlayletChapterListResp {
+ 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,
+ }
+}
+
+// validatePlayletCategoryIds 校验分类ID是否存在;空数组允许表示未绑定分类。
+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 validatePlayletTagIds(ctx context.Context, tagIds []int64) *srvdef.SrvErr {
+ if len(tagIds) == 0 {
+ return nil
+ }
+ uniqueIds := make([]int64, 0, len(tagIds))
+ seen := make(map[int64]struct{}, len(tagIds))
+ for _, tagId := range tagIds {
+ if tagId <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "标签ID无效", "")
+ }
+ if _, ok := seen[tagId]; ok {
+ continue
+ }
+ seen[tagId] = struct{}{}
+ uniqueIds = append(uniqueIds, tagId)
+ }
+ count, err := playlettagmod.CountPlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Id: bson.M{"$in": uniqueIds},
+ })
+ if err != nil {
+ return err
+ }
+ if count != int64(len(uniqueIds)) {
+ return srvdef.New(srvdef.ErrParamError, "标签不存在", "")
+ }
+ return nil
+}
+
+// validatePlayletThemeIds 校验题材ID是否存在
+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
+}
diff --git a/web/service/srv_playlet/slug.go b/web/service/srv_playlet/slug.go
new file mode 100644
index 0000000..da16dc4
--- /dev/null
+++ b/web/service/srv_playlet/slug.go
@@ -0,0 +1,454 @@
+package srv_playlet
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/web/api/proto"
+ "videoweb/web/service/srv_playletcategory"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ syncSlugDefaultSize = int64(500)
+ syncSlugMaxSize = int64(2000)
+)
+
+func emptySlugFilter() bson.M {
+ return bson.M{
+ "$or": []bson.M{
+ {"slug": ""},
+ {"slug": bson.M{"$exists": false}},
+ },
+ }
+}
+
+func ensureThemeSlug(ctx context.Context, ac *playletthememod.PlayletTheme, excludeId int64) *srvdef.SrvErr {
+ ac.Slug = utils.SanitizeSlug(ac.Slug)
+ if ac.Slug != "" {
+ if utils.IsSlugAllDigits(ac.Slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ filter := bson.M{playletthememod.FN.PlayletTheme.Slug: ac.Slug}
+ if excludeId > 0 {
+ filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playletthememod.FindOnePlayletTheme(ctx, filter)
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ return nil
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name)
+ ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
+ filter := bson.M{playletthememod.FN.PlayletTheme.Slug: slug}
+ if excludeId > 0 {
+ filter[playletthememod.FN.PlayletTheme.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playletthememod.FindOnePlayletTheme(ctx, filter)
+ return exist != nil
+ })
+ return nil
+}
+
+func prepareThemeSlugOnUpdate(ctx context.Context, id int64, info *playletthememod.PlayletTheme, req *proto.PlayletThemeUpdateReq, setDoc bson.M) *srvdef.SrvErr {
+ slug := info.Slug
+ if req.Slug != nil {
+ slug = utils.SanitizeSlug(*req.Slug)
+ }
+ if slug != "" {
+ if req.Slug != nil {
+ if utils.IsSlugAllDigits(slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ exist, _ := playletthememod.FindOnePlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Slug: slug,
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$ne": id},
+ })
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ setDoc[playletthememod.FN.PlayletTheme.Slug] = slug
+ }
+ return nil
+ }
+ name := info.Name
+ if req.Name != nil {
+ name = strings.TrimSpace(*req.Name)
+ }
+ keywords := info.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(keywords, name)
+ newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
+ exist, _ := playletthememod.FindOnePlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Slug: s,
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$ne": id},
+ })
+ return exist != nil
+ })
+ setDoc[playletthememod.FN.PlayletTheme.Slug] = newSlug
+ return nil
+}
+
+func ensureTagSlug(ctx context.Context, ac *playlettagmod.PlayletTag, excludeId int64) *srvdef.SrvErr {
+ ac.Slug = utils.SanitizeSlug(ac.Slug)
+ if ac.Slug != "" {
+ if utils.IsSlugAllDigits(ac.Slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ filter := bson.M{playlettagmod.FN.PlayletTag.Slug: ac.Slug}
+ if excludeId > 0 {
+ filter[playlettagmod.FN.PlayletTag.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playlettagmod.FindOnePlayletTag(ctx, filter)
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ return nil
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name)
+ ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
+ filter := bson.M{playlettagmod.FN.PlayletTag.Slug: slug}
+ if excludeId > 0 {
+ filter[playlettagmod.FN.PlayletTag.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playlettagmod.FindOnePlayletTag(ctx, filter)
+ return exist != nil
+ })
+ return nil
+}
+
+func prepareTagSlugOnUpdate(ctx context.Context, id int64, info *playlettagmod.PlayletTag, req *proto.PlayletTagUpdateReq, setDoc bson.M) *srvdef.SrvErr {
+ slug := info.Slug
+ if req.Slug != nil {
+ slug = utils.SanitizeSlug(*req.Slug)
+ }
+ if slug != "" {
+ if req.Slug != nil {
+ if utils.IsSlugAllDigits(slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ exist, _ := playlettagmod.FindOnePlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Slug: slug,
+ playlettagmod.FN.PlayletTag.Id: bson.M{"$ne": id},
+ })
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ setDoc[playlettagmod.FN.PlayletTag.Slug] = slug
+ }
+ return nil
+ }
+ name := info.Name
+ if req.Name != nil {
+ name = strings.TrimSpace(*req.Name)
+ }
+ keywords := info.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(keywords, name)
+ newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
+ exist, _ := playlettagmod.FindOnePlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Slug: s,
+ playlettagmod.FN.PlayletTag.Id: bson.M{"$ne": id},
+ })
+ return exist != nil
+ })
+ setDoc[playlettagmod.FN.PlayletTag.Slug] = newSlug
+ return nil
+}
+
+func ensurePlayletSlug(ctx context.Context, ac *playletmod.Playlet, excludeId int64) *srvdef.SrvErr {
+ ac.Slug = utils.SanitizeSlug(ac.Slug)
+ if ac.Slug != "" {
+ if utils.IsSlugAllDigits(ac.Slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ filter := bson.M{playletmod.FN.Playlet.Slug: ac.Slug}
+ if excludeId > 0 {
+ filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playletmod.FindOnePlaylet(ctx, filter)
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ return nil
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Title)
+ ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
+ filter := bson.M{playletmod.FN.Playlet.Slug: slug}
+ if excludeId > 0 {
+ filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playletmod.FindOnePlaylet(ctx, filter)
+ return exist != nil
+ })
+ return nil
+}
+
+func preparePlayletSlugOnUpdate(ctx context.Context, id int64, info *playletmod.Playlet, req *proto.PlayletUpdateReq, setDoc bson.M) *srvdef.SrvErr {
+ slug := info.Slug
+ if req.Slug != nil {
+ slug = utils.SanitizeSlug(*req.Slug)
+ }
+ if slug != "" {
+ if req.Slug != nil {
+ if utils.IsSlugAllDigits(slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ exist, _ := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Slug: slug,
+ playletmod.FN.Playlet.Id: bson.M{"$ne": id},
+ })
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ setDoc[playletmod.FN.Playlet.Slug] = slug
+ }
+ return nil
+ }
+ title := info.Title
+ if req.Title != nil {
+ title = strings.TrimSpace(*req.Title)
+ }
+ keywords := info.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(keywords, title)
+ newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
+ exist, _ := playletmod.FindOnePlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Slug: s,
+ playletmod.FN.Playlet.Id: bson.M{"$ne": id},
+ })
+ return exist != nil
+ })
+ setDoc[playletmod.FN.Playlet.Slug] = newSlug
+ return nil
+}
+
+// SyncEmptySlugs 为题材、标签、分类、短剧中空 slug 自动生成。可重复调用。
+func SyncEmptySlugs(ctx context.Context, req *proto.PlayletSlugSyncReq) (*proto.PlayletSlugSyncResp, *srvdef.SrvErr) {
+ maxSize := syncSlugDefaultSize
+ if req != nil && req.MaxSize > 0 {
+ maxSize = req.MaxSize
+ }
+ if maxSize > syncSlugMaxSize {
+ maxSize = syncSlugMaxSize
+ }
+ resp := &proto.PlayletSlugSyncResp{}
+ remain := maxSize
+
+ themeUpdated, themeMore, err := syncThemeEmptySlugs(ctx, remain)
+ if err != nil {
+ return nil, err
+ }
+ resp.ThemeUpdated = themeUpdated
+ remain -= themeUpdated
+ if remain <= 0 {
+ resp.HasMore = themeMore || hasEmptySlug(ctx, "tag") || hasEmptySlug(ctx, "category") || hasEmptySlug(ctx, "playlet")
+ playletcache.ClearHomeCache()
+ return resp, nil
+ }
+
+ tagUpdated, tagMore, err := syncTagEmptySlugs(ctx, remain)
+ if err != nil {
+ return nil, err
+ }
+ resp.TagUpdated = tagUpdated
+ remain -= tagUpdated
+ if remain <= 0 {
+ resp.HasMore = tagMore || hasEmptySlug(ctx, "category") || hasEmptySlug(ctx, "playlet")
+ playletcache.ClearHomeCache()
+ return resp, nil
+ }
+
+ categoryUpdated, categoryMore, err := syncCategoryEmptySlugs(ctx, remain)
+ if err != nil {
+ return nil, err
+ }
+ resp.CategoryUpdated = categoryUpdated
+ remain -= categoryUpdated
+ if remain <= 0 {
+ resp.HasMore = categoryMore || hasEmptySlug(ctx, "playlet")
+ playletcache.ClearHomeCache()
+ return resp, nil
+ }
+
+ playletUpdated, playletMore, err := syncPlayletEmptySlugs(ctx, remain)
+ if err != nil {
+ return nil, err
+ }
+ resp.PlayletUpdated = playletUpdated
+ resp.HasMore = playletMore
+ playletcache.ClearHomeCache()
+ return resp, nil
+}
+
+func hasEmptySlug(ctx context.Context, kind string) bool {
+ filter := emptySlugFilter()
+ switch kind {
+ case "theme":
+ count, _ := playletthememod.CountPlayletTheme(ctx, filter)
+ return count > 0
+ case "tag":
+ count, _ := playlettagmod.CountPlayletTag(ctx, filter)
+ return count > 0
+ case "category":
+ count, _ := playletcategorymod.CountPlayletCategory(ctx, filter)
+ return count > 0
+ case "playlet":
+ count, _ := playletmod.CountPlaylet(ctx, filter)
+ return count > 0
+ default:
+ return false
+ }
+}
+
+func syncThemeEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playletthememod.FindManyPlayletTheme(ctx, emptySlugFilter(), options.Find().
+ SetSort(bson.D{{Key: playletthememod.FN.PlayletTheme.Id, Value: 1}}).
+ SetLimit(maxSize+1))
+ if err != nil {
+ return 0, false, err
+ }
+ hasMore := int64(len(list)) > maxSize
+ if hasMore {
+ list = list[:maxSize]
+ }
+ var updated int64
+ now := time.Now()
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if serr := ensureThemeSlug(ctx, item, item.Id); serr != nil {
+ return updated, hasMore, serr
+ }
+ if err := playletthememod.UpdateOnePlayletTheme(ctx,
+ bson.M{playletthememod.FN.PlayletTheme.Id: item.Id},
+ bson.M{"$set": bson.M{
+ playletthememod.FN.PlayletTheme.Slug: item.Slug,
+ playletthememod.FN.PlayletTheme.UpdatedAt: now,
+ }},
+ ); err != nil {
+ return updated, hasMore, err
+ }
+ updated++
+ }
+ return updated, hasMore, nil
+}
+
+func syncTagEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playlettagmod.FindManyPlayletTag(ctx, emptySlugFilter(), options.Find().
+ SetSort(bson.D{{Key: playlettagmod.FN.PlayletTag.Id, Value: 1}}).
+ SetLimit(maxSize+1))
+ if err != nil {
+ return 0, false, err
+ }
+ hasMore := int64(len(list)) > maxSize
+ if hasMore {
+ list = list[:maxSize]
+ }
+ var updated int64
+ now := time.Now()
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if serr := ensureTagSlug(ctx, item, item.Id); serr != nil {
+ return updated, hasMore, serr
+ }
+ if err := playlettagmod.UpdateOnePlayletTag(ctx,
+ bson.M{playlettagmod.FN.PlayletTag.Id: item.Id},
+ bson.M{"$set": bson.M{
+ playlettagmod.FN.PlayletTag.Slug: item.Slug,
+ playlettagmod.FN.PlayletTag.UpdatedAt: now,
+ }},
+ ); err != nil {
+ return updated, hasMore, err
+ }
+ updated++
+ }
+ return updated, hasMore, nil
+}
+
+func syncCategoryEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playletcategorymod.FindManyPlayletCategory(ctx, emptySlugFilter(), options.Find().
+ SetSort(bson.D{{Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1}}).
+ SetLimit(maxSize+1))
+ if err != nil {
+ return 0, false, err
+ }
+ hasMore := int64(len(list)) > maxSize
+ if hasMore {
+ list = list[:maxSize]
+ }
+ var updated int64
+ now := time.Now()
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if serr := srv_playletcategory.EnsureCategorySlug(ctx, item, item.Id); serr != nil {
+ return updated, hasMore, serr
+ }
+ if err := playletcategorymod.UpdateOnePlayletCategory(ctx,
+ bson.M{playletcategorymod.FN.PlayletCategory.Id: item.Id},
+ bson.M{"$set": bson.M{
+ playletcategorymod.FN.PlayletCategory.Slug: item.Slug,
+ playletcategorymod.FN.PlayletCategory.UpdatedAt: now,
+ }},
+ ); err != nil {
+ return updated, hasMore, err
+ }
+ updated++
+ }
+ return updated, hasMore, nil
+}
+
+func syncPlayletEmptySlugs(ctx context.Context, maxSize int64) (int64, bool, *srvdef.SrvErr) {
+ list, err := playletmod.FindManyPlaylet(ctx, emptySlugFilter(), options.Find().
+ SetSort(bson.D{{Key: playletmod.FN.Playlet.Id, Value: 1}}).
+ SetLimit(maxSize+1))
+ if err != nil {
+ return 0, false, err
+ }
+ hasMore := int64(len(list)) > maxSize
+ if hasMore {
+ list = list[:maxSize]
+ }
+ var updated int64
+ now := time.Now()
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ if serr := ensurePlayletSlug(ctx, item, item.Id); serr != nil {
+ return updated, hasMore, serr
+ }
+ if err := playletmod.UpdateOnePlaylet(ctx,
+ bson.M{playletmod.FN.Playlet.Id: item.Id},
+ bson.M{"$set": bson.M{
+ playletmod.FN.Playlet.Slug: item.Slug,
+ playletmod.FN.Playlet.UpdatedAt: now,
+ }},
+ ); err != nil {
+ return updated, hasMore, err
+ }
+ updated++
+ }
+ return updated, hasMore, nil
+}
diff --git a/web/service/srv_playlet/tag.go b/web/service/srv_playlet/tag.go
new file mode 100644
index 0000000..5eb6540
--- /dev/null
+++ b/web/service/srv_playlet/tag.go
@@ -0,0 +1,167 @@
+package srv_playlet
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// ListPlayletTag 分页查询标签列表
+func ListPlayletTag(ctx context.Context, req *proto.PlayletTagListReq) (*proto.PlayletTagListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playlettagmod.FN.PlayletTag.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[playlettagmod.FN.PlayletTag.Status] = *req.Status
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[playlettagmod.FN.PlayletTag.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ if slug := strings.TrimSpace(req.Slug); slug != "" {
+ filter[playlettagmod.FN.PlayletTag.Slug] = slug
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playlettagmod.CountPlayletTag(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletTagListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletTagListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playlettagmod.FindManyPlayletTag(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: playlettagmod.FN.PlayletTag.Rank, Value: -1},
+ {Key: playlettagmod.FN.PlayletTag.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletTagListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.PlayletTagListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Status: item.Status,
+ Rank: item.Rank,
+ SeoInfo: normalizeSeoValue(item.SeoInfo),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlayletTag 新增标签
+func AddPlayletTag(ctx context.Context, req *proto.PlayletTagAddReq) (*proto.PlayletTagAddResp, *srvdef.SrvErr) {
+ if req == nil || strings.TrimSpace(req.Name) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
+ }
+ doc := &playlettagmod.PlayletTag{
+ Name: strings.TrimSpace(req.Name),
+ Slug: strings.TrimSpace(req.Slug),
+ Status: req.Status,
+ Rank: req.Rank,
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ if err := ensureTagSlug(ctx, doc, 0); err != nil {
+ return nil, err
+ }
+ if err := playlettagmod.InsertOnePlayletTag(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearAppPlayletHomeCache()
+ return &proto.PlayletTagAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlayletTag 更新标签
+func UpdatePlayletTag(ctx context.Context, req *proto.PlayletTagUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ info, err := playlettagmod.FindOnePlayletTag(ctx, bson.M{playlettagmod.FN.PlayletTag.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ setDoc := bson.M{playlettagmod.FN.PlayletTag.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "标签名称不能为空", "")
+ }
+ setDoc[playlettagmod.FN.PlayletTag.Name] = name
+ }
+ if req.Status != nil {
+ setDoc[playlettagmod.FN.PlayletTag.Status] = *req.Status
+ }
+ if req.Rank != nil {
+ setDoc[playlettagmod.FN.PlayletTag.Rank] = *req.Rank
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
+ }
+ if err := prepareTagSlugOnUpdate(ctx, req.Id, info, req, setDoc); err != nil {
+ return err
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ if err := playlettagmod.UpdateOnePlayletTag(ctx,
+ bson.M{playlettagmod.FN.PlayletTag.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ 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..b978ae7
--- /dev/null
+++ b/web/service/srv_playlet/theme.go
@@ -0,0 +1,204 @@
+package srv_playlet
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playletthememod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// ListPlayletTheme 分页查询题材列表
+func ListPlayletTheme(ctx context.Context, req *proto.PlayletThemeListReq) (*proto.PlayletThemeListPageResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "", "")
+ }
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[playletthememod.FN.PlayletTheme.Id] = *req.Id
+ }
+ if req.Status != nil {
+ filter[playletthememod.FN.PlayletTheme.Status] = *req.Status
+ }
+ if req.IsRecommend != nil {
+ filter[playletthememod.FN.PlayletTheme.IsRecommend] = *req.IsRecommend
+ }
+ if name := strings.TrimSpace(req.Name); name != "" {
+ filter[playletthememod.FN.PlayletTheme.Name] = bson.M{"$regex": regexp.QuoteMeta(name)}
+ }
+ if slug := strings.TrimSpace(req.Slug); slug != "" {
+ filter[playletthememod.FN.PlayletTheme.Slug] = slug
+ }
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ totalCount, err := playletthememod.CountPlayletTheme(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &proto.PlayletThemeListPageResp{
+ BasePageResp: response.BasePageResp{
+ TotalCount: totalCount,
+ PageSize: int(pageSize),
+ PageNum: int(pageNum),
+ List: make([]*proto.PlayletThemeListResp, 0),
+ },
+ }
+ if totalCount == 0 {
+ return resp, nil
+ }
+ list, err := playletthememod.FindManyPlayletTheme(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Rank, Value: -1},
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ items := make([]*proto.PlayletThemeListResp, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ items = append(items, &proto.PlayletThemeListResp{
+ Id: item.Id,
+ Name: item.Name,
+ Slug: item.Slug,
+ Icon: item.Icon,
+ Status: item.Status,
+ IsRecommend: item.IsRecommend,
+ Rank: item.Rank,
+ SeoInfo: normalizeSeoValue(item.SeoInfo),
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ })
+ }
+ resp.List = items
+ return resp, nil
+}
+
+// AddPlayletTheme 新增题材
+func AddPlayletTheme(ctx context.Context, req *proto.PlayletThemeAddReq) (*proto.PlayletThemeAddResp, *srvdef.SrvErr) {
+ if req == nil || strings.TrimSpace(req.Name) == "" {
+ return nil, srvdef.New(srvdef.ErrParamError, "题材名称不能为空", "")
+ }
+ doc := &playletthememod.PlayletTheme{
+ Name: strings.TrimSpace(req.Name),
+ Slug: strings.TrimSpace(req.Slug),
+ Icon: req.Icon,
+ Status: req.Status,
+ IsRecommend: req.IsRecommend,
+ Rank: req.Rank,
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ if err := ensureThemeSlug(ctx, doc, 0); err != nil {
+ return nil, err
+ }
+ if err := playletthememod.InsertOnePlayletTheme(ctx, doc); err != nil {
+ return nil, err
+ }
+ clearAppPlayletHomeCache()
+ return &proto.PlayletThemeAddResp{Id: doc.Id}, nil
+}
+
+// UpdatePlayletTheme 更新题材
+func UpdatePlayletTheme(ctx context.Context, req *proto.PlayletThemeUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ info, err := playletthememod.FindOnePlayletTheme(ctx, bson.M{playletthememod.FN.PlayletTheme.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ setDoc := bson.M{playletthememod.FN.PlayletTheme.UpdatedAt: time.Now()}
+ if req.Name != nil {
+ name := strings.TrimSpace(*req.Name)
+ if name == "" {
+ return srvdef.New(srvdef.ErrParamError, "题材名称不能为空", "")
+ }
+ setDoc[playletthememod.FN.PlayletTheme.Name] = name
+ }
+ if req.Icon != nil {
+ setDoc[playletthememod.FN.PlayletTheme.Icon] = *req.Icon
+ }
+ if req.Status != nil {
+ setDoc[playletthememod.FN.PlayletTheme.Status] = *req.Status
+ }
+ if req.IsRecommend != nil {
+ setDoc[playletthememod.FN.PlayletTheme.IsRecommend] = *req.IsRecommend
+ }
+ if req.Rank != nil {
+ setDoc[playletthememod.FN.PlayletTheme.Rank] = *req.Rank
+ }
+ if req.SeoInfo != nil {
+ setDoc["seoInfo"] = normalizeSeoValue(*req.SeoInfo)
+ }
+ if err := prepareThemeSlugOnUpdate(ctx, req.Id, info, req, setDoc); err != nil {
+ return err
+ }
+ if len(setDoc) <= 1 {
+ return srvdef.New(srvdef.ErrParamError, "没有可更新字段", "")
+ }
+ if err := playletthememod.UpdateOnePlayletTheme(ctx,
+ bson.M{playletthememod.FN.PlayletTheme.Id: req.Id},
+ bson.M{"$set": setDoc},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// SetPlayletThemeRecommend 单/批量设置题材是否推荐
+func SetPlayletThemeRecommend(ctx context.Context, req *proto.PlayletThemeRecommendSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材ID不能为空", "")
+ }
+ if err := playletthememod.UpdateManyPlayletTheme(ctx,
+ bson.M{playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids}},
+ bson.M{"$set": bson.M{
+ playletthememod.FN.PlayletTheme.IsRecommend: req.IsRecommend,
+ playletthememod.FN.PlayletTheme.UpdatedAt: time.Now(),
+ }},
+ ); err != nil {
+ return err
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
+
+// DelPlayletTheme 删除题材(被短剧引用时禁止删除)
+func DelPlayletTheme(ctx context.Context, id int64) *srvdef.SrvErr {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "id无效", "")
+ }
+ usedCount, err := playletmod.CountPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.ThemeIds: bson.M{"$in": []int64{id}},
+ })
+ if err != nil {
+ return err
+ }
+ if usedCount > 0 {
+ return srvdef.New(srvdef.ErrParamError, "题材已被短剧引用,无法删除", "")
+ }
+ count, err := playletthememod.DelOnePlayletTheme(ctx, bson.M{playletthememod.FN.PlayletTheme.Id: id})
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ clearAppPlayletHomeCache()
+ return nil
+}
diff --git a/web/service/srv_playletcategory/category.go b/web/service/srv_playletcategory/category.go
new file mode 100644
index 0000000..36fc7a3
--- /dev/null
+++ b/web/service/srv_playletcategory/category.go
@@ -0,0 +1,224 @@
+package srv_playletcategory
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "time"
+ "videoweb/pub/common/playletcache"
+ "videoweb/pub/common/response"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/pubdef"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultCategoryPageSize int64 = 10
+ maxCategoryPageSize int64 = 100
+)
+
+// 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)}
+ }
+ if slug := strings.TrimSpace(req.Slug); slug != "" {
+ filter[playletcategorymod.FN.PlayletCategory.Slug] = slug
+ }
+ pageNum, pageSize := normalizeCategoryPage(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().
+ 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,
+ Desc: item.Desc,
+ 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: strings.TrimSpace(req.Slug),
+ Desc: strings.TrimSpace(req.Desc),
+ Icon: strings.TrimSpace(req.Icon),
+ Status: req.Status,
+ Rank: req.Rank,
+ Remark: strings.TrimSpace(req.Remark),
+ SeoInfo: normalizeSeoValue(req.SeoInfo),
+ }
+ if err := EnsureCategorySlug(ctx, doc, 0); err != nil {
+ return nil, err
+ }
+ if err := playletcategorymod.InsertOnePlayletCategory(ctx, doc); err != nil {
+ return nil, err
+ }
+ playletcache.ClearHomeCache()
+ 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无效", "")
+ }
+ info, 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.Desc != nil {
+ setDoc[playletcategorymod.FN.PlayletCategory.Desc] = strings.TrimSpace(*req.Desc)
+ }
+ 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)
+ }
+ if err := prepareCategorySlugOnUpdate(ctx, req.Id, info, req, setDoc); err != nil {
+ return err
+ }
+ 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
+ }
+ playletcache.ClearHomeCache()
+ 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, "", "")
+ }
+ playletcache.ClearHomeCache()
+ return nil
+}
+
+// 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 normalizeCategoryPage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = defaultCategoryPageSize
+ }
+ if pageSize > maxCategoryPageSize {
+ pageSize = maxCategoryPageSize
+ }
+ return pageNum, pageSize
+}
diff --git a/web/service/srv_playletcategory/slug.go b/web/service/srv_playletcategory/slug.go
new file mode 100644
index 0000000..2e45ec5
--- /dev/null
+++ b/web/service/srv_playletcategory/slug.go
@@ -0,0 +1,82 @@
+package srv_playletcategory
+
+import (
+ "context"
+ "strings"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/playletcategorymod"
+ "videoweb/web/api/proto"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// EnsureCategorySlug 新增时保证 slug:无值则生成,有值则校验唯一且不能全数字
+func EnsureCategorySlug(ctx context.Context, ac *playletcategorymod.PlayletCategory, excludeId int64) *srvdef.SrvErr {
+ ac.Slug = utils.SanitizeSlug(ac.Slug)
+ if ac.Slug != "" {
+ if utils.IsSlugAllDigits(ac.Slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ filter := bson.M{playletcategorymod.FN.PlayletCategory.Slug: ac.Slug}
+ if excludeId > 0 {
+ filter[playletcategorymod.FN.PlayletCategory.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playletcategorymod.FindOnePlayletCategory(ctx, filter)
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ return nil
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(ac.SeoInfo.Keyworld, ac.Name)
+ ac.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
+ filter := bson.M{playletcategorymod.FN.PlayletCategory.Slug: slug}
+ if excludeId > 0 {
+ filter[playletcategorymod.FN.PlayletCategory.Id] = bson.M{"$ne": excludeId}
+ }
+ exist, _ := playletcategorymod.FindOnePlayletCategory(ctx, filter)
+ return exist != nil
+ })
+ return nil
+}
+
+func prepareCategorySlugOnUpdate(ctx context.Context, id int64, info *playletcategorymod.PlayletCategory, req *proto.PlayletCategoryUpdateReq, setDoc bson.M) *srvdef.SrvErr {
+ slug := info.Slug
+ if req.Slug != nil {
+ slug = utils.SanitizeSlug(*req.Slug)
+ }
+ if slug != "" {
+ if req.Slug != nil {
+ if utils.IsSlugAllDigits(slug) {
+ return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
+ }
+ exist, _ := playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Slug: slug,
+ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$ne": id},
+ })
+ if exist != nil {
+ return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
+ }
+ setDoc[playletcategorymod.FN.PlayletCategory.Slug] = slug
+ }
+ return nil
+ }
+ name := info.Name
+ if req.Name != nil {
+ name = strings.TrimSpace(*req.Name)
+ }
+ keywords := info.SeoInfo.Keyworld
+ if req.SeoInfo != nil {
+ keywords = normalizeSeoValue(*req.SeoInfo).Keyworld
+ }
+ source := utils.SlugSourceFromKeywordsOrTitle(keywords, name)
+ newSlug := utils.MakeUniqueSlug(source, func(s string) bool {
+ exist, _ := playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
+ playletcategorymod.FN.PlayletCategory.Slug: s,
+ playletcategorymod.FN.PlayletCategory.Id: bson.M{"$ne": id},
+ })
+ return exist != nil
+ })
+ setDoc[playletcategorymod.FN.PlayletCategory.Slug] = newSlug
+ return nil
+}
diff --git a/web/service/srv_recharge/callback.go b/web/service/srv_recharge/callback.go
new file mode 100644
index 0000000..981928d
--- /dev/null
+++ b/web/service/srv_recharge/callback.go
@@ -0,0 +1,258 @@
+package srv_recharge
+
+import (
+ "context"
+ "crypto/md5"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/dataReport"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/models/eventmod"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/vipordermod"
+ "videoweb/pub/payment"
+ "videoweb/pub/pubdef"
+
+ "github.com/gin-gonic/gin"
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// BillCallbackParam 嘉华支付回调参数
+type BillCallbackParam struct {
+ TradeNo string `json:"transNo" form:"transNo"`
+ Oid string `json:"oid" form:"oid"`
+ RealAmount int64 `json:"payMoney" form:"payMoney"`
+ PaymentAt time.Time `json:"paymentAt" form:"paymentAt"`
+ SuccessAt time.Time `json:"successAt" form:"successAt"`
+}
+
+// OnPayCenterCallback 统一支付回调入口
+// 通过 vip_order 表查询订单,再按 OrderType 分支处理。
+func OnPayCenterCallback(ctx *gin.Context, param *BillCallbackParam) *srvdef.SrvErr {
+ log.Info("srv_recharge:OnPayCenterCallback",
+ log.Any("tradeNo", param.TradeNo),
+ log.Any("oid", param.Oid),
+ log.Any("realAmount", param.RealAmount),
+ )
+ if param.TradeNo == "" {
+ return srvdef.New(srvdef.ErrParamError, "tradeNo 不能为空", "")
+ }
+
+ order, serr := vipordermod.FindOneVipOrder(ctx, bson.M{
+ vipordermod.FN.VipOrder.TradeNo: param.TradeNo,
+ })
+ if serr != nil {
+ log.Error("OnPayCenterCallback FindOneVipOrder failed",
+ log.Any("tradeNo", param.TradeNo), log.E(serr))
+ return serr
+ }
+
+ if order.Status == pubdef.VipOrderSuccess {
+ log.Info("OnPayCenterCallback already success", log.Any("tradeNo", param.TradeNo))
+ return nil
+ }
+
+ if order.Oid != "" && order.Oid != param.Oid {
+ return srvdef.New(srvdef.ErrParamError,
+ fmt.Sprintf("oid 不匹配: %s vs %s", order.Oid, param.Oid), "")
+ }
+
+ paymentAt := resolvePaymentAt(param)
+
+ if serr = vipordermod.UpdateOrderSuccess(ctx, param.TradeNo, param.Oid, param.RealAmount, paymentAt); serr != nil {
+ log.Error("OnPayCenterCallback UpdateOrderSuccess failed",
+ log.Any("tradeNo", param.TradeNo), log.E(serr))
+ return serr
+ }
+
+ orderSnap := *order
+ realAmount := param.RealAmount
+ oid := param.Oid
+
+ switch order.OrderType {
+ case vipordermod.OrderTypeCoin:
+ go handleCoinOrderSuccess(orderSnap, oid, realAmount)
+ default:
+ go handleVipOrderSuccess(orderSnap, oid, realAmount)
+ }
+
+ return nil
+}
+
+func handleVipOrderSuccess(order vipordermod.VipOrder, oid string, realAmount int64) {
+ ctx := context.Background()
+
+ pkg, err := findVipPackage(ctx, order.CardId)
+ if err != nil {
+ log.Error("handleVipOrderSuccess find package failed",
+ log.Any("userId", order.UserId), log.Any("packageId", order.CardId), log.E(err))
+ return
+ }
+
+ expire, err := payment.ActivateVip(ctx, order.UserId, pkg)
+ if err != nil {
+ log.Error("handleVipOrderSuccess ActivateVip failed, retrying",
+ log.Any("userId", order.UserId), log.Any("packageId", order.CardId), log.E(err))
+ time.Sleep(3 * time.Second)
+ expire, err = payment.ActivateVip(ctx, order.UserId, pkg)
+ if err != nil {
+ log.Error("handleVipOrderSuccess ActivateVip retry failed",
+ log.Any("userId", order.UserId), log.Any("packageId", order.CardId), log.E(err))
+ }
+ }
+ log.Info("handleVipOrderSuccess done", log.Any("userId", order.UserId), log.Any("expire", expire))
+
+ if pkg.GiftCoin > 0 {
+ _ = payment.WriteCoinTransByUser(ctx, payment.WriteCoinTransOpt{
+ UserId: order.UserId,
+ FlowDir: cointransmod.FlowIn,
+ TranType: cointransmod.TranTypeVipGiftCoin,
+ Amount: pkg.GiftCoin,
+ OrderNo: order.TradeNo,
+ Remark: payment.CoinTransRemark(cointransmod.TranTypeVipGiftCoin, pkg.Name),
+ })
+ }
+
+ reportVipOrderPaid(ctx, &order, oid, realAmount, expire)
+}
+
+func handleCoinOrderSuccess(order vipordermod.VipOrder, oid string, realAmount int64) {
+ ctx := context.Background()
+
+ if err := addUserCoins(ctx, order.UserId, order.Coin); err != nil {
+ log.Error("handleCoinOrderSuccess addUserCoins failed, retrying",
+ log.Any("userId", order.UserId), log.Any("coin", order.Coin), log.E(err))
+ time.Sleep(3 * time.Second)
+ if err = addUserCoins(ctx, order.UserId, order.Coin); err != nil {
+ log.Error("handleCoinOrderSuccess addUserCoins retry failed",
+ log.Any("userId", order.UserId), log.Any("coin", order.Coin), log.E(err))
+ }
+ }
+ log.Info("handleCoinOrderSuccess done", log.Any("userId", order.UserId), log.Any("coin", order.Coin))
+
+ _ = payment.WriteCoinTransByUser(ctx, payment.WriteCoinTransOpt{
+ UserId: order.UserId,
+ FlowDir: cointransmod.FlowIn,
+ TranType: cointransmod.TranTypeCoinRecharge,
+ Amount: order.Coin,
+ OrderNo: order.TradeNo,
+ Remark: payment.CoinTransRemark(cointransmod.TranTypeCoinRecharge, ""),
+ })
+
+ reportCoinOrderPaid(ctx, &order, oid, realAmount)
+}
+
+func resolvePaymentAt(param *BillCallbackParam) time.Time {
+ if !param.PaymentAt.IsZero() {
+ return param.PaymentAt
+ }
+ if !param.SuccessAt.IsZero() {
+ return param.SuccessAt
+ }
+ return time.Now()
+}
+
+func findVipPackage(ctx context.Context, packageId int64) (*rechargepackagemod.RechargePackage, *srvdef.SrvErr) {
+ pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
+ rechargepackagemod.FN.RechargePackage.Id: packageId,
+ rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
+ })
+ if serr != nil || pkg == nil {
+ return nil, srvdef.New(srvdef.ErrNoData, "会员套餐不存在", "")
+ }
+ return pkg, nil
+}
+
+func addUserCoins(ctx context.Context, userId int64, coin int64) *srvdef.SrvErr {
+ return usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: userId},
+ bson.M{
+ "$inc": bson.M{usermod.FN.User.Recharge: coin},
+ "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
+ },
+ )
+}
+
+func reportVipOrderPaid(ctx context.Context, order *vipordermod.VipOrder, oid string, realAmount int64, vipExpire time.Time) {
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: order.UserId})
+ districtCode, ip := "", order.IPAddr
+ if user != nil {
+ districtCode = user.DistrictCode
+ }
+
+ ua := useragent.UA{SysType: order.SysType}
+ eventId := newEventId(pubdef.TrackerAppId(), order.UserId, order.TradeNo, order.CreatedAt.Unix())
+ cf := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderPaid, districtCode, ua, strconv.FormatInt(user.Id, 10), ip)
+
+ var expireTS int64
+ if !vipExpire.IsZero() {
+ expireTS = vipExpire.Unix()
+ }
+ cf.Payload = dataReport.JsonRawMessage(&dataReport.OrderPaidEvent{
+ OrderID: order.TradeNo,
+ OrderType: "vip_subscription",
+ ProductID: strconv.FormatInt(order.CardId, 10),
+ Amount: dataReport.JsonNumber(realAmount),
+ Currency: "CNY",
+ VIPExpirationTime: dataReport.JsonNumber(expireTS),
+ PayType: order.PayMode,
+ PayChannel: order.PayChannel,
+ TransactionID: oid,
+ CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
+ })
+ insertEvent(ctx, cf, order.TradeNo)
+}
+
+func reportCoinOrderPaid(ctx context.Context, order *vipordermod.VipOrder, oid string, realAmount int64) {
+ user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: order.UserId})
+ districtCode, ip := "", order.IPAddr
+ if user != nil {
+ districtCode = user.DistrictCode
+ }
+
+ ua := useragent.UA{SysType: order.SysType}
+ eventId := newEventId(pubdef.TrackerAppId(), order.UserId, order.TradeNo, order.CreatedAt.Unix())
+ cf := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderPaid, districtCode, ua, strconv.FormatInt(user.Id, 10), ip)
+
+ cf.Payload = dataReport.JsonRawMessage(&dataReport.OrderPaidEvent{
+ OrderID: order.TradeNo,
+ OrderType: "coin_purchase",
+ ProductID: strconv.FormatInt(order.CoinCfgId, 10),
+ Amount: dataReport.JsonNumber(realAmount),
+ Currency: "CNY",
+ CoinQuantity: dataReport.JsonNumber(order.Coin),
+ PayType: order.PayMode,
+ PayChannel: order.PayChannel,
+ TransactionID: oid,
+ CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
+ })
+ insertEvent(ctx, cf, order.TradeNo)
+}
+
+func insertEvent(ctx context.Context, cf dataReport.CommonField, tradeNo string) {
+ data, _ := json.Marshal(cf)
+ if err := eventmod.InsertOneEvent(ctx, &eventmod.Event{
+ EventId: cf.EventID,
+ EventType: cf.Event,
+ Data: string(data),
+ Status: eventmod.Default,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }); err != nil {
+ log.Error("insertEvent failed", log.Any("tradeNo", tradeNo), log.E(err))
+ }
+}
+
+func newEventId(appId string, uid int64, orderId interface{}, createTime int64) string {
+ input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
+ h := md5.New()
+ h.Write([]byte(input))
+ return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/web/service/srv_rechargepackage/define.go b/web/service/srv_rechargepackage/define.go
new file mode 100644
index 0000000..39753c4
--- /dev/null
+++ b/web/service/srv_rechargepackage/define.go
@@ -0,0 +1,88 @@
+package srv_rechargepackage
+
+import (
+ "time"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/pubdef"
+)
+
+// RechargePackageListReq Web后台购买套餐分页列表请求
+type RechargePackageListReq struct {
+ pubdef.Page
+ Id *int64 `json:"id"` // 套餐ID
+ PackageType *rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
+ Status *rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
+ Name string `json:"name"` // 展示标题,模糊搜索
+}
+
+// RechargePackageListResp Web后台购买套餐分页列表响应
+type RechargePackageListResp struct {
+ Total int64 `json:"total"` // 总数量
+ List []*RechargePackageItem `json:"list"` // 当前页列表
+}
+
+// VipPackageOption 会员畅享下拉列表项。
+type VipPackageOption struct {
+ Id int64 `json:"id"` // 套餐ID
+ Name string `json:"name"` // 套餐名称
+}
+
+// RechargePackageItem Web后台购买套餐列表项
+type RechargePackageItem struct {
+ Id int64 `json:"id"` // 套餐ID
+ PackageType rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
+ Name string `json:"name"` // 展示标题
+ SubTitle string `json:"subTitle"` // 展示副标题
+ Price int64 `json:"price"` // 售价,单位分
+ OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
+ DurationDays int64 `json:"durationDays"` // 会员有效天数
+ DurationHours int64 `json:"durationHours"` // 会员有效小时数,由durationDays计算
+ BaseCoin int64 `json:"baseCoin"` // 基础金币数
+ GiftCoin int64 `json:"giftCoin"` // 赠送金币数
+ TotalCoin int64 `json:"totalCoin"` // 到账总金币数,由baseCoin+giftCoin计算
+ Sort int32 `json:"sort"` // 排序,越小越靠前
+ Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
+ Remark string `json:"remark"` // 后台备注
+ CreatedAt time.Time `json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt"` // 更新时间
+}
+
+// RechargePackageAddReq Web后台新增购买套餐请求
+type RechargePackageAddReq struct {
+ PackageType rechargepackagemod.PackageType `json:"packageType" binding:"required"` // 套餐类型:1-会员畅享 2-金币充值
+ Name string `json:"name" binding:"required"` // 展示标题
+ Price int64 `json:"price" binding:"required"` // 售价,单位分
+ OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
+ DurationDays int64 `json:"durationDays"` // 会员有效天数,金币套餐传0
+ BaseCoin int64 `json:"baseCoin"` // 基础金币数,会员套餐传0
+ GiftCoin int64 `json:"giftCoin"` // 赠送金币数
+ Sort int32 `json:"sort"` // 排序,越小越靠前
+ Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用,默认1
+ Remark string `json:"remark"` // 后台备注
+}
+
+// RechargePackageUpdateReq Web后台更新购买套餐请求
+type RechargePackageUpdateReq struct {
+ Id int64 `json:"id" binding:"required"` // 套餐ID
+ PackageType *rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
+ Name *string `json:"name"` // 展示标题
+ Price *int64 `json:"price"` // 售价,单位分
+ OriginalPrice *int64 `json:"originalPrice"` // 原价,单位分
+ DurationDays *int64 `json:"durationDays"` // 会员有效天数
+ BaseCoin *int64 `json:"baseCoin"` // 基础金币数
+ GiftCoin *int64 `json:"giftCoin"` // 赠送金币数
+ Sort *int32 `json:"sort"` // 排序
+ Status *rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
+ Remark *string `json:"remark"` // 后台备注
+}
+
+// RechargePackageDelReq Web后台删除购买套餐请求。
+type RechargePackageDelReq struct {
+ Id int64 `json:"id" binding:"required"` // 套餐ID
+}
+
+// RechargePackageStatusSetReq Web后台单/批量设置购买套餐状态请求。
+type RechargePackageStatusSetReq struct {
+ Ids []int64 `json:"ids"` // 套餐ID集合
+ Status rechargepackagemod.Status `json:"status"` // 状态:1-启用 2-停用
+}
diff --git a/web/service/srv_rechargepackage/package.go b/web/service/srv_rechargepackage/package.go
new file mode 100644
index 0000000..c106f61
--- /dev/null
+++ b/web/service/srv_rechargepackage/package.go
@@ -0,0 +1,343 @@
+package srv_rechargepackage
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/rechargepackagemod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const (
+ defaultPageNum int64 = 1
+ defaultPageSize int64 = 10
+ maxPageSize int64 = 100
+)
+
+// ListRechargePackage 分页查询购买套餐配置。
+func ListRechargePackage(ctx context.Context, req *RechargePackageListReq) (*RechargePackageListResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ filter := buildListFilter(req)
+ pageNum, pageSize := normalizePage(req.PageNum, req.PageSize)
+ total, err := rechargepackagemod.CountRechargePackage(ctx, filter)
+ if err != nil {
+ return nil, err
+ }
+ resp := &RechargePackageListResp{
+ Total: total,
+ List: make([]*RechargePackageItem, 0),
+ }
+ if total == 0 {
+ return resp, nil
+ }
+ list, err := rechargepackagemod.FindManyRechargePackage(ctx, filter, options.Find().
+ SetSort(bson.D{
+ {Key: rechargepackagemod.FN.RechargePackage.PackageType, Value: 1},
+ {Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
+ {Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
+ }).
+ SetSkip((pageNum-1)*pageSize).
+ SetLimit(pageSize))
+ if err != nil {
+ return nil, err
+ }
+ for _, item := range list {
+ resp.List = append(resp.List, rechargePackageToWebItem(item))
+ }
+ return resp, nil
+}
+
+// ListVipPackageOptions 查询启用的会员畅享套餐选项,不分页。
+func ListVipPackageOptions(ctx context.Context) ([]*VipPackageOption, *srvdef.SrvErr) {
+ list, err := rechargepackagemod.FindManyRechargePackage(ctx, bson.M{
+ rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
+ rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
+ }, options.Find().
+ SetProjection(bson.D{
+ {Key: rechargepackagemod.FN.RechargePackage.Id, Value: 1},
+ {Key: rechargepackagemod.FN.RechargePackage.Name, Value: 1},
+ }).
+ SetSort(bson.D{
+ {Key: rechargepackagemod.FN.RechargePackage.Sort, Value: 1},
+ {Key: rechargepackagemod.FN.RechargePackage.Id, Value: -1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ result := make([]*VipPackageOption, 0, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ result = append(result, &VipPackageOption{
+ Id: item.Id,
+ Name: item.Name,
+ })
+ }
+ return result, nil
+}
+
+// AddRechargePackage 新增购买套餐配置。
+func AddRechargePackage(ctx context.Context, req *RechargePackageAddReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ doc := &rechargepackagemod.RechargePackage{
+ PackageType: req.PackageType,
+ Name: strings.TrimSpace(req.Name),
+ Price: req.Price,
+ OriginalPrice: req.OriginalPrice,
+ DurationDays: req.DurationDays,
+ BaseCoin: req.BaseCoin,
+ GiftCoin: req.GiftCoin,
+ Sort: req.Sort,
+ Status: req.Status,
+ Remark: strings.TrimSpace(req.Remark),
+ }
+ if doc.Status == 0 {
+ doc.Status = rechargepackagemod.StatusEnabled
+ }
+ normalizePackageFields(doc)
+ if err := validatePackage(doc); err != nil {
+ return err
+ }
+ doc.SubTitle = buildSubTitle(doc)
+ return rechargepackagemod.InsertOneRechargePackage(ctx, doc)
+}
+
+// UpdateRechargePackage 更新购买套餐配置。
+func UpdateRechargePackage(ctx context.Context, req *RechargePackageUpdateReq) *srvdef.SrvErr {
+ if req == nil || req.Id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
+ }
+ exist, err := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{rechargepackagemod.FN.RechargePackage.Id: req.Id})
+ if err != nil {
+ return err
+ }
+ doc := *exist
+ applyUpdate(&doc, req)
+ normalizePackageFields(&doc)
+ if err := validatePackage(&doc); err != nil {
+ return err
+ }
+ doc.SubTitle = buildSubTitle(&doc)
+ now := time.Now()
+ setDoc := bson.M{
+ rechargepackagemod.FN.RechargePackage.PackageType: doc.PackageType,
+ rechargepackagemod.FN.RechargePackage.Name: doc.Name,
+ rechargepackagemod.FN.RechargePackage.SubTitle: doc.SubTitle,
+ rechargepackagemod.FN.RechargePackage.Price: doc.Price,
+ rechargepackagemod.FN.RechargePackage.OriginalPrice: doc.OriginalPrice,
+ rechargepackagemod.FN.RechargePackage.DurationDays: doc.DurationDays,
+ rechargepackagemod.FN.RechargePackage.BaseCoin: doc.BaseCoin,
+ rechargepackagemod.FN.RechargePackage.GiftCoin: doc.GiftCoin,
+ rechargepackagemod.FN.RechargePackage.Sort: doc.Sort,
+ rechargepackagemod.FN.RechargePackage.Status: doc.Status,
+ rechargepackagemod.FN.RechargePackage.Remark: doc.Remark,
+ rechargepackagemod.FN.RechargePackage.UpdatedAt: now,
+ }
+ return rechargepackagemod.UpdateOneRechargePackage(ctx,
+ bson.M{rechargepackagemod.FN.RechargePackage.Id: req.Id},
+ bson.M{"$set": setDoc},
+ )
+}
+
+// DeleteRechargePackage 删除购买套餐配置。
+func DeleteRechargePackage(ctx context.Context, id int64) *srvdef.SrvErr {
+ if id <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
+ }
+ return rechargepackagemod.DeleteOneRechargePackage(ctx, bson.M{rechargepackagemod.FN.RechargePackage.Id: id})
+}
+
+// SetRechargePackageStatus 单/批量设置购买套餐启停状态。
+func SetRechargePackageStatus(ctx context.Context, req *RechargePackageStatusSetReq) *srvdef.SrvErr {
+ if req == nil {
+ return srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
+ }
+ ids := dedupePositiveInt64Ids(req.Ids)
+ if len(ids) == 0 {
+ return srvdef.New(srvdef.ErrParamError, "套餐ID不能为空", "")
+ }
+ if !isValidStatus(req.Status) {
+ return srvdef.New(srvdef.ErrParamError, "状态无效", "")
+ }
+ return rechargepackagemod.UpdateManyRechargePackage(ctx,
+ bson.M{rechargepackagemod.FN.RechargePackage.Id: bson.M{"$in": ids}},
+ bson.M{"$set": bson.M{
+ rechargepackagemod.FN.RechargePackage.Status: req.Status,
+ rechargepackagemod.FN.RechargePackage.UpdatedAt: time.Now(),
+ }},
+ )
+}
+
+func buildListFilter(req *RechargePackageListReq) bson.M {
+ filter := bson.M{}
+ if req.Id != nil {
+ filter[rechargepackagemod.FN.RechargePackage.Id] = *req.Id
+ }
+ if req.PackageType != nil {
+ filter[rechargepackagemod.FN.RechargePackage.PackageType] = *req.PackageType
+ }
+ if req.Status != nil {
+ filter[rechargepackagemod.FN.RechargePackage.Status] = *req.Status
+ }
+ if strings.TrimSpace(req.Name) != "" {
+ filter[rechargepackagemod.FN.RechargePackage.Name] = bson.M{
+ "$regex": primitive.Regex{Pattern: strings.TrimSpace(req.Name), Options: "i"},
+ }
+ }
+ return filter
+}
+
+// normalizePage 统一分页兜底,限制 pageSize 防止后台误传过大导致慢查询。
+func normalizePage(pageNum, pageSize int64) (int64, int64) {
+ if pageNum <= 0 {
+ pageNum = defaultPageNum
+ }
+ if pageSize <= 0 {
+ pageSize = defaultPageSize
+ }
+ if pageSize > maxPageSize {
+ pageSize = maxPageSize
+ }
+ return pageNum, pageSize
+}
+
+// normalizePackageFields 清理不同套餐类型不生效的字段。
+// 会员套餐不使用金币字段;金币套餐不使用会员有效期字段。
+func normalizePackageFields(doc *rechargepackagemod.RechargePackage) {
+ switch doc.PackageType {
+ case rechargepackagemod.PackageTypeVip:
+ doc.BaseCoin = 0
+ doc.GiftCoin = 0
+ case rechargepackagemod.PackageTypeCoin:
+ doc.DurationDays = 0
+ }
+}
+
+// validatePackage 校验购买套餐配置和套餐类型字段约束。
+func validatePackage(doc *rechargepackagemod.RechargePackage) *srvdef.SrvErr {
+ if !isValidPackageType(doc.PackageType) {
+ return srvdef.New(srvdef.ErrParamError, "套餐类型无效", "")
+ }
+ if strings.TrimSpace(doc.Name) == "" {
+ return srvdef.New(srvdef.ErrParamError, "套餐标题不能为空", "")
+ }
+ if doc.Price <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "套餐价格必须大于0", "")
+ }
+ if !isValidStatus(doc.Status) {
+ return srvdef.New(srvdef.ErrParamError, "状态无效", "")
+ }
+ if doc.PackageType == rechargepackagemod.PackageTypeVip && doc.DurationDays <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "会员套餐有效天数必须大于0", "")
+ }
+ if doc.PackageType == rechargepackagemod.PackageTypeCoin && doc.BaseCoin+doc.GiftCoin <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "金币套餐到账金币数必须大于0", "")
+ }
+ return nil
+}
+
+func isValidPackageType(packageType rechargepackagemod.PackageType) bool {
+ return packageType == rechargepackagemod.PackageTypeVip || packageType == rechargepackagemod.PackageTypeCoin
+}
+
+func isValidStatus(status rechargepackagemod.Status) bool {
+ return status == rechargepackagemod.StatusEnabled || status == rechargepackagemod.StatusDisabled
+}
+
+// applyUpdate 将更新请求合并到现有文档副本,再统一做类型归一化和校验。
+func applyUpdate(doc *rechargepackagemod.RechargePackage, req *RechargePackageUpdateReq) {
+ if req.PackageType != nil {
+ doc.PackageType = *req.PackageType
+ }
+ if req.Name != nil {
+ doc.Name = strings.TrimSpace(*req.Name)
+ }
+ if req.Price != nil {
+ doc.Price = *req.Price
+ }
+ if req.OriginalPrice != nil {
+ doc.OriginalPrice = *req.OriginalPrice
+ }
+ if req.DurationDays != nil {
+ doc.DurationDays = *req.DurationDays
+ }
+ if req.BaseCoin != nil {
+ doc.BaseCoin = *req.BaseCoin
+ }
+ if req.GiftCoin != nil {
+ doc.GiftCoin = *req.GiftCoin
+ }
+ if req.Sort != nil {
+ doc.Sort = *req.Sort
+ }
+ if req.Status != nil {
+ doc.Status = *req.Status
+ }
+ if req.Remark != nil {
+ doc.Remark = strings.TrimSpace(*req.Remark)
+ }
+}
+
+// buildSubTitle 根据套餐类型和核心配置生成展示副标题,不由后台请求手动传入。
+func buildSubTitle(doc *rechargepackagemod.RechargePackage) string {
+ switch doc.PackageType {
+ case rechargepackagemod.PackageTypeVip:
+ return fmt.Sprintf("%d × 24 小时会员", doc.DurationDays)
+ case rechargepackagemod.PackageTypeCoin:
+ if doc.GiftCoin > 0 {
+ return fmt.Sprintf("基础%d + 赠送%d", doc.BaseCoin, doc.GiftCoin)
+ }
+ return fmt.Sprintf("基础%d", doc.BaseCoin)
+ default:
+ return ""
+ }
+}
+
+// rechargePackageToWebItem 转换后台列表展示项,并补充派生字段。
+func rechargePackageToWebItem(item *rechargepackagemod.RechargePackage) *RechargePackageItem {
+ return &RechargePackageItem{
+ Id: item.Id,
+ PackageType: item.PackageType,
+ Name: item.Name,
+ SubTitle: item.SubTitle,
+ Price: item.Price,
+ OriginalPrice: item.OriginalPrice,
+ DurationDays: item.DurationDays,
+ DurationHours: item.DurationDays * 24,
+ BaseCoin: item.BaseCoin,
+ GiftCoin: item.GiftCoin,
+ TotalCoin: item.BaseCoin + item.GiftCoin,
+ Sort: item.Sort,
+ Status: item.Status,
+ Remark: item.Remark,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
+// dedupePositiveInt64Ids 去重并过滤非法ID,批量操作只处理正整数ID。
+func dedupePositiveInt64Ids(ids []int64) []int64 {
+ result := make([]int64, 0, len(ids))
+ seen := make(map[int64]struct{}, len(ids))
+ for _, id := range ids {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ return result
+}
diff --git a/web/service/srv_report/report.go b/web/service/srv_report/report.go
new file mode 100644
index 0000000..6760381
--- /dev/null
+++ b/web/service/srv_report/report.go
@@ -0,0 +1,73 @@
+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"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// ReportVideoStatusChangeEvent 视频状态变更事件上报
+func ReportVideoStatusChangeEvent(media *videoinformationmod.VideoInformation, flag, userId int64, ua useragent.UA) {
+ if media == nil || media.Id == 0 || flag == 0 {
+ return
+ }
+
+ var lsjId string
+
+ var videoTypeID = "default"
+ var videoTypeName = "默认分类"
+ if len(media.CategoryIds) > 0 {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+ category, _ := videocategorymod.FindOneVideoCategory(ctx, bson.M{
+ videocategorymod.FN.VideoCategory.Id: media.CategoryIds[0],
+ })
+ if category != nil {
+ videoTypeID = fmt.Sprintf("%v", category.Id)
+ videoTypeName = category.Name
+ }
+ }
+ event := dataReport.NewCommonField("", dataReport.EventTypeVideoStatusChange, "", ua, strconv.FormatInt(userId, 10), "")
+
+ 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 videoinformationmod.InformationSatus) int64 {
+ switch status {
+ case videoinformationmod.InformationSatusVerifyOK:
+ return 1 // 上架
+ case videoinformationmod.InformationSatusVerifyCancel:
+ 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/define.go b/web/service/srv_search/define.go
new file mode 100644
index 0000000..22da4fd
--- /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
+}
diff --git a/web/service/srv_search/es.go b/web/service/srv_search/es.go
new file mode 100644
index 0000000..d6330a9
--- /dev/null
+++ b/web/service/srv_search/es.go
@@ -0,0 +1,177 @@
+package srv_search
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+var syncIsRuining = false
+
+func StartES() {
+ log.Info("srv_search:Start...")
+ defer log.Info("srv_search:Start!!!")
+ // 初始化 ES 索引 mapping(已存在时报错可忽略)
+ //InitArticleESIndex()
+ //InitToolESIndex()
+ startSyncEs()
+ _, _ = skdCron.AddFunc("0 0/17 * * * ?", func() {
+ startSyncEs()
+ })
+ skdCron.Start()
+}
+
+func startSyncEs() {
+ ctx := context.Background()
+ updateEsData(ctx, esmod.GetVideoIndex(), getVideoEsData)
+}
+
+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()) {
+ esData, serr = fn(ctx, nil, nil, esIndex)
+ if serr != nil {
+ log.Warn("updateEsData fn return err", log.Any("index", esIndex), log.E(serr))
+ return
+ }
+ _ = delAllFromES(esIndex)
+ 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, now); err != nil {
+ return
+ }
+ }
+ } else {
+ timestamp := esRecord.RecordTime
+ var recentMinute = utils.RecentMinute(now, esmod.FiveMinuteScale)
+ 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 {
+ 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, now); err != nil {
+ return
+ }
+ }
+ }
+ timestamp = end
+ }
+ }
+}
+
+func commit(data esmanager.M, esIndex string, syncTime time.Time) (bool, error) {
+ bulkData := handleBulkCountLimit(data)
+ isHaveSuccess := false
+ if bulkData != nil && len(bulkData) > 0 {
+ for _, doc := range bulkData {
+ if err := esmanager.Instance.Bulk(esIndex, doc); err != nil {
+ log.Warn(fmt.Sprintf("Elastic sync commit video data SyncDataToES failed!, err:%+v\n", err))
+ // 出错继续
+ } else {
+ isHaveSuccess = true
+ }
+ }
+ }
+ if isHaveSuccess {
+ 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
+}
+
+// DelVideoFromES 从 ES 实时删除单条视频(视频删除/下架时调用)
+func DelVideoFromES(id int64) {
+ if esmanager.Instance == nil {
+ return
+ }
+ strId := strconv.FormatInt(id, 10)
+ if err := batchDelFromES(esmod.GetVideoIndex(), []string{strId}); err != nil {
+ log.Warn("srv_search:DelVideoFromES failed", log.Any("id", id), log.E(err))
+ }
+}
+
+// UpsertVideoToES 将单条视频实时写入/更新 ES(视频上架时调用)
+func UpsertVideoToES(info *videoinformationmod.VideoInformation) {
+ if esmanager.Instance == nil {
+ return
+ }
+ esData := videoToEsData([]*videoinformationmod.VideoInformation{info})
+ if err := AddMultiDataToES(esmod.GetVideoIndex(), esData); err != nil {
+ log.Warn("srv_search:UpsertVideoToES failed", log.Any("id", info.Id), log.E(err))
+ }
+}
diff --git a/web/service/srv_search/es_playlet.go b/web/service/srv_search/es_playlet.go
new file mode 100644
index 0000000..6b438ef
--- /dev/null
+++ b/web/service/srv_search/es_playlet.go
@@ -0,0 +1,247 @@
+package srv_search
+
+import (
+ "context"
+ "strconv"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/playletmod"
+ "videoweb/pub/models/playlettagmod"
+ "videoweb/pub/models/playletthememod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var playletESProjection = bson.D{
+ {Key: playletmod.FN.Playlet.Id, Value: 1},
+ {Key: playletmod.FN.Playlet.Status, Value: 1},
+ {Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
+ {Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
+ {Key: playletmod.FN.Playlet.OperateType, Value: 1},
+ {Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1},
+ {Key: playletmod.FN.Playlet.Banner, Value: 1},
+ {Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
+ {Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
+ {Key: playletmod.FN.Playlet.Title, Value: 1},
+ {Key: playletmod.FN.Playlet.PlayName, Value: 1},
+ {Key: playletmod.FN.Playlet.Description, Value: 1},
+ {Key: playletmod.FN.Playlet.Slug, Value: 1},
+ {Key: playletmod.FN.Playlet.Desc, Value: 1},
+ {Key: playletmod.FN.Playlet.Anchor, Value: 1},
+ {Key: playletmod.FN.Playlet.Cover, Value: 1},
+ {Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
+ {Key: playletmod.FN.Playlet.TagIds, Value: 1},
+ {Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
+ {Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
+ {Key: playletmod.FN.Playlet.HeatValue, Value: 1},
+ {Key: playletmod.FN.Playlet.CountCollect, Value: 1},
+ {Key: playletmod.FN.Playlet.CountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
+ {Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
+ {Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
+ {Key: "seoInfo", Value: 1},
+}
+
+// SyncPlayletStatusToES 根据短剧上下架状态同步ES搜索数据
+func SyncPlayletStatusToES(ctx context.Context, ids []int64, status playletmod.Status) *srvdef.SrvErr {
+ // ES只保留已上架短剧;上架写入/更新索引,下架从索引删除。
+ if len(ids) == 0 {
+ return nil
+ }
+ if esmanager.Instance == nil {
+ log.Error("短剧ES同步失败,ES未初始化", log.Any("短剧ID", ids), log.Any("状态", status))
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ switch status {
+ case playletmod.StatusUp:
+ return upsertPlayletsToES(ctx, ids)
+ case playletmod.StatusDown:
+ return deletePlayletsFromES(ids)
+ default:
+ return nil
+ }
+}
+
+// upsertPlayletsToES 批量写入已上架短剧ES文档
+func upsertPlayletsToES(ctx context.Context, ids []int64) *srvdef.SrvErr {
+ // 只查询已上架数据,避免并发状态变化后把已下架短剧重新写进ES。
+ list, err := playletmod.FindManyPlaylet(ctx, bson.M{
+ playletmod.FN.Playlet.Id: bson.M{"$in": ids},
+ playletmod.FN.Playlet.Status: playletmod.StatusUp,
+ }, options.Find().SetProjection(playletESProjection))
+ if err != nil {
+ return err
+ }
+ if len(list) == 0 {
+ return nil
+ }
+ themeNameMap, err := buildPlayletESThemeNameMap(ctx, list)
+ if err != nil {
+ return err
+ }
+ tagNameMap, err := buildPlayletESTagNameMap(ctx, list)
+ if err != nil {
+ return err
+ }
+ esData := make(esmanager.M, len(list))
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ esData[strconv.FormatInt(item.Id, 10)] = playletToESDoc(item, themeNameMap, tagNameMap)
+ }
+ if len(esData) == 0 {
+ return nil
+ }
+ if err = AddMultiDataToES(esmod.GetPlayletIndex(), esData); err != nil {
+ log.Error("批量写入短剧ES失败", log.Any("短剧ID", ids), log.E(err))
+ return err
+ }
+ log.Info("批量写入短剧ES成功", log.Any("数量", len(esData)), log.Any("短剧ID", ids))
+ return nil
+}
+
+// deletePlayletsFromES 批量删除短剧ES文档
+func deletePlayletsFromES(ids []int64) *srvdef.SrvErr {
+ // ES文档ID和Mongo短剧业务ID保持一致,可以直接按ID批量删除。
+ strIds := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if id <= 0 {
+ continue
+ }
+ strIds = append(strIds, strconv.FormatInt(id, 10))
+ }
+ if len(strIds) == 0 {
+ return nil
+ }
+ if err := batchDelFromES(esmod.GetPlayletIndex(), strIds); err != nil {
+ log.Error("批量删除短剧ES失败", log.Any("短剧ID", ids), log.E(err))
+ return err
+ }
+ log.Info("批量删除短剧ES成功", log.Any("数量", len(strIds)), log.Any("短剧ID", ids))
+ return nil
+}
+
+// playletToESDoc 转换短剧Mongo数据为ES文档
+func playletToESDoc(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string) *esmod.ESPlaylet {
+ // ES文档保留题材名和标签名,app搜索可直接命中剧名、题材名、简介和标签名。
+ return &esmod.ESPlaylet{
+ Id: item.Id,
+ Status: item.Status,
+ IsFeatured: item.IsFeatured,
+ IsRecommend: item.IsRecommend,
+ OperateType: playletmod.NormalizeOperateType(item.OperateType),
+ ShowOnBanner: item.ShowOnBanner,
+ Banner: item.Banner,
+ OnlineAt: item.OnlineAt,
+ ThemeIds: item.ThemeIds,
+ ThemeNames: playletESNamesByIds(item.ThemeIds, themeNameMap),
+ Title: item.Title,
+ PlayName: item.PlayName,
+ Description: item.Description,
+ Slug: item.Slug,
+ Desc: item.Desc,
+ Anchor: item.Anchor,
+ Cover: item.DisplayCover(),
+ TagIds: item.TagIds,
+ TagNames: playletESNamesByIds(item.TagIds, tagNameMap),
+ TotalEpisode: item.TotalEpisode,
+ UpdateStatus: item.UpdateStatus,
+ CountBrowse: item.CountBrowse,
+ HeatValue: item.HeatValue,
+ CountCollect: item.CountCollect,
+ CountLike: item.CountLike + item.VirtualCountLike,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ SeoInfo: item.SeoInfo,
+ }
+}
+
+// buildPlayletESThemeNameMap 批量查询短剧题材名称
+func buildPlayletESThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.ThemeIds
+ })
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
+ playletthememod.FN.PlayletTheme.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playletthememod.FN.PlayletTheme.Id, Value: 1},
+ {Key: playletthememod.FN.PlayletTheme.Name, Value: 1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, theme := range themes {
+ if theme != nil {
+ result[theme.Id] = theme.Name
+ }
+ }
+ return result, nil
+}
+
+// buildPlayletESTagNameMap 批量查询短剧标签名称
+func buildPlayletESTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
+ ids := collectPlayletESIds(list, func(item *playletmod.Playlet) []int64 {
+ return item.TagIds
+ })
+ result := make(map[int64]string, len(ids))
+ if len(ids) == 0 {
+ return result, nil
+ }
+ tags, err := playlettagmod.FindManyPlayletTag(ctx, bson.M{
+ playlettagmod.FN.PlayletTag.Id: bson.M{"$in": ids},
+ }, options.Find().SetProjection(bson.D{
+ {Key: playlettagmod.FN.PlayletTag.Id, Value: 1},
+ {Key: playlettagmod.FN.PlayletTag.Name, Value: 1},
+ }))
+ if err != nil {
+ return nil, err
+ }
+ for _, tag := range tags {
+ if tag != nil {
+ result[tag.Id] = tag.Name
+ }
+ }
+ return result, nil
+}
+
+// collectPlayletESIds 汇总并去重短剧题材或标签ID
+func collectPlayletESIds(list playletmod.PlayletList, pick func(item *playletmod.Playlet) []int64) []int64 {
+ result := make([]int64, 0)
+ seen := make(map[int64]struct{})
+ for _, item := range list {
+ if item == nil {
+ continue
+ }
+ for _, id := range pick(item) {
+ if id <= 0 {
+ continue
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ result = append(result, id)
+ }
+ }
+ return result
+}
+
+// playletESNamesByIds 按ID顺序返回ES文档名称集合
+func playletESNamesByIds(ids []int64, nameMap map[int64]string) []string {
+ result := make([]string, 0, len(ids))
+ for _, id := range ids {
+ if name := nameMap[id]; name != "" {
+ result = append(result, name)
+ }
+ }
+ return result
+}
diff --git a/web/service/srv_search/esmedia.go b/web/service/srv_search/esmedia.go
new file mode 100644
index 0000000..0e5cf43
--- /dev/null
+++ b/web/service/srv_search/esmedia.go
@@ -0,0 +1,97 @@
+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{
+ videoinformationmod.FN.VideoInformation.MediaType: bson.M{
+ "$in": []interface{}{
+ videoinformationmod.MediaType_Video,
+ videoinformationmod.MediaType_VideoV2,
+ },
+ },
+ }
+ 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,
+ CategoryIds: v.CategoryIds,
+ Tags: v.Tags,
+ Real: v.Real,
+ VideoCount: v.VideoCount,
+ }
+ 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/keyword.go b/web/service/srv_search/keyword.go
new file mode 100644
index 0000000..904e172
--- /dev/null
+++ b/web/service/srv_search/keyword.go
@@ -0,0 +1,71 @@
+package srv_search
+
+import (
+ "regexp"
+ "strings"
+)
+
+// 分隔符:空格、英文逗号、中文逗号、顿号、斜杠、竖线
+var kwSplitRe = regexp.MustCompile(`[\s,,、/|]+`)
+
+// ParseKeywords 解析搜索字符串为关键词列表。
+// 规则:去除首尾空格,转小写,按空格/逗号/顿号/斜杠/竖线分割,过滤空串。
+func ParseKeywords(q string) []string {
+ q = strings.TrimSpace(q)
+ if q == "" {
+ return nil
+ }
+ parts := kwSplitRe.Split(strings.ToLower(q), -1)
+ result := make([]string, 0, len(parts))
+ for _, p := range parts {
+ if p = strings.TrimSpace(p); p != "" {
+ result = append(result, p)
+ }
+ }
+ return result
+}
+
+// buildMultiKeywordQuery 构建多关键词 ES bool should 查询(关键词之间 OR 关系)。
+// 每个关键词同时生成两个子句:
+// - 精确 multi_match(权重更高,boost=2)
+// - 模糊 multi_match(fuzziness=AUTO,prefix_length=1),覆盖拼写错误/多余字符等场景
+//
+// 示例:ChatGPTT→ChatGPT(1次编辑)、midjorny→midjourney(2次编辑)、数智人→数字人(1次编辑)
+func buildMultiKeywordQuery(keywords []string, fields []string, from, size int) map[string]interface{} {
+ shouldClauses := make([]map[string]interface{}, 0, len(keywords)*2)
+ for _, kw := range keywords {
+ // 精确匹配,分数更高
+ shouldClauses = append(shouldClauses, map[string]interface{}{
+ "multi_match": map[string]interface{}{
+ "query": kw,
+ "fields": fields,
+ "type": "best_fields",
+ "boost": 2,
+ },
+ })
+ // 模糊匹配:AUTO fuzziness(长度≥6时允许2次编辑),prefix_length=1 避免过度误召回
+ shouldClauses = append(shouldClauses, map[string]interface{}{
+ "multi_match": map[string]interface{}{
+ "query": kw,
+ "fields": fields,
+ "type": "best_fields",
+ "fuzziness": "AUTO",
+ "prefix_length": 1,
+ "fuzzy_rewrite": "constant_score_blended",
+ },
+ })
+ }
+ return map[string]interface{}{
+ "from": from,
+ "size": size,
+ "query": map[string]interface{}{
+ "bool": map[string]interface{}{
+ "should": shouldClauses,
+ "minimum_should_match": 1,
+ },
+ },
+ "sort": []map[string]interface{}{
+ {"_score": map[string]interface{}{"order": "desc"}},
+ },
+ }
+}
diff --git a/web/service/srv_search/search.go b/web/service/srv_search/search.go
new file mode 100644
index 0000000..de0a9e8
--- /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()
+}
diff --git a/web/service/srv_search/seo.go b/web/service/srv_search/seo.go
new file mode 100644
index 0000000..23ffd9c
--- /dev/null
+++ b/web/service/srv_search/seo.go
@@ -0,0 +1,15 @@
+package srv_search
+
+import (
+ "strings"
+ "videoweb/pub/pubdef"
+)
+
+func flattenSeoInfo(seo pubdef.SeoValue) (title, keywords, desc string) {
+ title = seo.Title
+ desc = seo.Description
+ if len(seo.Keyworld) > 0 {
+ keywords = strings.Join(seo.Keyworld, " ")
+ }
+ return
+}
diff --git a/web/service/srv_search/sync_common.go b/web/service/srv_search/sync_common.go
new file mode 100644
index 0000000..88aee07
--- /dev/null
+++ b/web/service/srv_search/sync_common.go
@@ -0,0 +1,47 @@
+package srv_search
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+)
+
+const esSyncOverlap = time.Minute
+
+// needFullESSync 判断是否需要全量同步 ES
+func needFullESSync(ctx context.Context, esIndex string) (bool, *esmod.ESRecord) {
+ esRecord, serr := esmod.GetRecordTimeByESIndex(ctx, esIndex)
+ if serr != nil || esRecord == nil {
+ return true, esRecord
+ }
+
+ count, err := esmanager.Instance.Count(esIndex, esmanager.M{
+ "query": esmanager.M{"match_all": esmanager.M{}},
+ })
+ if err != nil || count == 0 {
+ return true, esRecord
+ }
+
+ now := time.Now()
+ if (now.Hour() >= 5 && esRecord.UpdatedAt.Local().Hour() < 5) ||
+ esRecord.UpdatedAt.Local().Day() < now.Day() {
+ return true, esRecord
+ }
+ return false, esRecord
+}
+
+// esSyncSinceTime 取增量同步起点(RecordTime 优先,回退 1 分钟避免边界遗漏)
+func esSyncSinceTime(esRecord *esmod.ESRecord) time.Time {
+ if esRecord == nil {
+ return time.Time{}
+ }
+ since := esRecord.RecordTime
+ if since.IsZero() {
+ since = esRecord.UpdatedAt
+ }
+ if since.IsZero() {
+ return time.Time{}
+ }
+ return since.Add(-esSyncOverlap)
+}
diff --git a/web/service/srv_search/updater.go b/web/service/srv_search/updater.go
new file mode 100644
index 0000000..6c02e5a
--- /dev/null
+++ b/web/service/srv_search/updater.go
@@ -0,0 +1,223 @@
+package srv_search
+
+import (
+ "context"
+ "fmt"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "strconv"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/mongos"
+)
+
+// resetAllDocsToES 重设所有文档到数据库
+func resetAllDocsToES(ctx context.Context, param *Param) *srvdef.SrvErr {
+ log.Info("srv_search:resetAllDocsToES...", log.Any("ESIndex", param.EsIndex))
+ defer log.Info("srv_search:startActress!!!")
+ //删除所有记录
+ delAllFromES(param.EsIndex)
+
+ var pageNum int64 = 1
+ var pageSize int64 = 200
+ updSize := 0
+ for {
+ findOpt := constructFindOption(pageNum, pageSize)
+ docs, serr := findManyFromDb(ctx, param, findOpt)
+ if serr != nil {
+ return serr
+ }
+ if len(docs) == 0 {
+ break
+ }
+ updSize = updSize + len(docs)
+ updateESData, serr := sliceMapToJsonMap(docs, param)
+ log.Info("srv_search:resetAllDocsToES", log.Any("updateESData", updateESData))
+ if serr = batchAddToES(param.EsIndex, updateESData); serr != nil {
+ return serr
+ }
+ pageNum++
+ log.Info("resetAllToES>>>>>>>>>>>>>>>>>>", log.Any("updateSize:", updSize))
+ }
+ return nil
+}
+
+// constructMatch 构建match
+func constructMatch(param *Param) bson.M {
+ updateFilter := bson.A{}
+ for _, fieldName := range param.CollFields {
+ filter := bson.M{"updateDescription.updatedFields." + fieldName: bson.M{"$exists": true}}
+ updateFilter = append(updateFilter, filter)
+ }
+ match := bson.M{"$or": bson.A{
+ bson.M{"operationType": bson.M{"$in": []string{"delete", "insert", "replace"}}},
+ bson.M{"$and": bson.A{
+ bson.M{"operationType": bson.M{"$in": []string{"update"}}},
+ bson.M{"$or": updateFilter}},
+ }}}
+ return match
+}
+
+// constructActtressOption 查找选项
+func constructFindOption(pageNum int64, pageSize int64) *options.FindOptions {
+ opt := &options.FindOptions{}
+ opt.SetSort(bson.M{"createdAt": -1})
+ skip := (pageNum - 1) * pageSize
+ opt.SetSkip(skip)
+ opt.SetLimit(pageSize)
+ return opt
+}
+
+// AddMultiDataToES 添加一批Ex
+func AddMultiDataToES(esIndex string, esvs esmanager.M) *srvdef.SrvErr {
+ if err := esmanager.Instance.Bulk(esIndex, esvs); err != nil {
+ info := fmt.Sprintf("(srv_search:AddMultiDataToES) ES Bulk failed! err:%+v:", err)
+ log.Warn(info)
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// findManyFromDb 查找一批数据
+func findManyFromDb(ctx context.Context, param *Param,
+ opts ...*options.FindOptions) ([]bson.M, *srvdef.SrvErr) {
+ docs := make([]bson.M, 0)
+ if err := param.Coll.Find(ctx, &docs, param.CollFilter, opts...); err != nil {
+ log.Warn("srv_search:findManyFromDb db.Find failed", log.E(err), log.Any("filter", param.CollFilter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return docs, nil
+}
+
+// sliceMapToJsonMap 从map切片构建Map对象
+func sliceMapToJsonMap(docs []bson.M, param *Param) (esmanager.M, *srvdef.SrvErr) {
+ esMap := make(esmanager.M)
+ for _, doc := range docs {
+ fieldMap := make(esmanager.M)
+ for key, value := range doc {
+ if utils.IncludeString(param.CollFields, key) {
+ fieldMap[key] = value
+ } else if key == "_id" {
+ fieldMap["id"] = value
+ }
+ }
+ if id, ok := doc["_id"].(int64); ok {
+ strId := strconv.FormatInt(id, 10)
+ esMap[strId] = fieldMap
+ } else {
+ log.Warn("srv_search:sliceMapToJsonMap Key doesn't exist", log.Any("keyId", "_id"))
+ }
+ }
+ return esMap, nil
+}
+
+// getInfoFromChangeData 从改变流中获取数据信息
+func getInfoFromChangeData(changeData bson.M, param *Param) (
+ string, *esmod.ESTimestamp, string, esmanager.M, *srvdef.SrvErr) {
+ operationType, ok := changeData["operationType"].(string)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData operationType type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ ptimestamp, ok := changeData["clusterTime"].(primitive.Timestamp)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData clusterTime type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ estimestamp := &esmod.ESTimestamp{T: ptimestamp.T, I: ptimestamp.I}
+ documentKey, ok := changeData["documentKey"].(primitive.M)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData documentKey type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ docKey, ok := documentKey["_id"].(int64)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData documentKey[_id] type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+ strId := strconv.FormatInt(docKey, 10)
+ esDocMap := make(esmanager.M)
+ if changeData["fullDocument"] != nil {
+ docMap, ok := changeData["fullDocument"].(primitive.M)
+ if ok == false {
+ log.Error("srv_search:getInfoFromChangeData fullDocument type error")
+ return "", nil, "", nil, srvdef.New(srvdef.ErrData, "", "")
+ }
+
+ for key, value := range docMap {
+ if utils.IncludeString(param.CollFields, key) {
+ esDocMap[key] = value
+ } else if key == "_id" {
+ esDocMap["id"] = value
+ }
+ }
+ }
+ return operationType, estimestamp, strId, esDocMap, nil
+}
+
+// delAllFromES 删除所有数据
+func delAllFromES(esIndex string) *srvdef.SrvErr {
+ query := esmanager.M{
+ "query": esmanager.M{
+ "match_all": esmanager.M{},
+ },
+ }
+ if err := esmanager.Instance.DelMany([]string{esIndex}, query); err != nil {
+ log.Warn(fmt.Sprintf("(srv_search:DelAllFromEs) Es DelMany failed! err:%+v:", err),
+ log.Any("query", query))
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// batchAddToES 添加一批数据到ES
+func batchAddToES(esIndex string, esvs esmanager.M) *srvdef.SrvErr {
+ log.Info("srv_search:resetAllDocsToES", log.Any("esIndex", esIndex), log.Any("esvs", esvs))
+ if err := esmanager.Instance.Bulk(esIndex, esvs); err != nil {
+ info := fmt.Sprintf("(srv_search:batchAddToES) ES Bulk failed! err:%+v:", err)
+ log.Warn(info)
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// batchDelFromES 删除一批数据从ES
+func batchDelFromES(esIndex string, ids []string) *srvdef.SrvErr {
+ query := esmanager.M{
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {
+ "ids": esmanager.M{
+ "values": ids,
+ },
+ },
+ },
+ },
+ },
+ }
+ if err := esmanager.Instance.DelMany([]string{esIndex}, query); err != nil {
+ info := fmt.Sprintf(
+ "(srv_search:batchDelFromES) Es DelMany failed! err:%+v:", err)
+ log.Warn(info)
+ return srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ return nil
+}
+
+// watchOpLog 监控表变化
+func watchOpLog(ctx context.Context, db *mongos.Collection, pipeline []bson.M,
+ opt *options.ChangeStreamOptions) (*mongo.ChangeStream, *srvdef.SrvErr) {
+ changeStream, err := db.WatchOpLog(ctx, pipeline, opt)
+ if err != nil {
+ log.Warn("srv_search:watchOpLog db.WatchOpLog failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return changeStream, nil
+}
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..9ee27f3
--- /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, serr := translationmod.CountTranslationInfos(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := translationmod.FindManyTranslationInfos(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddTranslationInfo 添加一视频标签
+func AddTranslationInfo(ctx context.Context,
+ ac *translationmod.TranslationInfo) *srvdef.SrvErr {
+
+ ac.Id = mongos.Lid.Translation.Get()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ serr := translationmod.InsertOneTranslationInfo(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+// DelTranslationInfo 删除指定视频标签
+func DelTranslationInfo(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{translationmod.FN.TranslationInfo.Id: id}
+ //删除
+ count, serr := translationmod.DelOneTranslationInfo(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateTranslationInfo 更新指定视频标签
+func UpdateTranslationInfo(ctx context.Context, id int64, param TranslationInfoUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{translationmod.FN.TranslationInfo.Id: id}
+ updateDoc := bson.M{"$set": param}
+ serr := translationmod.UpdateOneTranslationInfo(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
diff --git a/web/service/srv_translationcategory/define.go b/web/service/srv_translationcategory/define.go
new file mode 100644
index 0000000..7dc8cd8
--- /dev/null
+++ b/web/service/srv_translationcategory/define.go
@@ -0,0 +1,36 @@
+package srv_translationcategory
+
+import (
+ "time"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/pubdef"
+)
+
+type TranslationCategoryReq struct {
+ Id *int64 `json:"id"` // 分类ID,精确匹配
+ Name string `json:"name"` // 分类名称,模糊搜索
+ pubdef.Page
+}
+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
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+}
+type TranslationCategoryUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param TranslationCategoryUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type TranslationCategoryDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_translationcategory/translation.go b/web/service/srv_translationcategory/translation.go
new file mode 100644
index 0000000..83d91b4
--- /dev/null
+++ b/web/service/srv_translationcategory/translation.go
@@ -0,0 +1,90 @@
+package srv_translationcategory
+
+import (
+ "context"
+ "regexp"
+ "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{}
+ if param.Id != nil && *param.Id > 0 {
+ filter[translationcategorymod.FN.TranslationCategory.Id] = *param.Id
+ }
+ if name := strings.TrimSpace(param.Name); name != "" {
+ filter[translationcategorymod.FN.TranslationCategory.Name] = bson.M{"$regex": regexp.QuoteMeta(name), "$options": "i"}
+ }
+ opt := &options.FindOptions{}
+ resp := &TranslationCategoryResp{}
+
+ opt.SetSort(bson.M{translationcategorymod.FN.TranslationCategory.Rank: -1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := translationcategorymod.CountTranslationCategorys(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := translationcategorymod.FindManyTranslationCategorys(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// 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..0408d0f
--- /dev/null
+++ b/web/service/srv_user/define.go
@@ -0,0 +1,38 @@
+package srv_user
+
+import (
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/pubdef"
+)
+
+type UserListReq struct {
+ pubdef.Page
+ Id *int64 `json:"id"` // 用户ID
+ AccountNo string `json:"accountNo"` // 账号(精确)
+ Email string `json:"email"` // 邮箱(精确)
+ NickName string `json:"nickName"` // 昵称(模糊)
+ DistrictCode string `json:"districtCode"` // 渠道码
+ Status *pubdef.UserStatus `json:"status"` // 状态
+ Type *pubdef.UserType `json:"type"` // 用户类型
+}
+
+type UserListResp struct {
+ Total int64 `json:"total"`
+ List usermod.UserList `json:"list"`
+}
+
+type UserUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param usermod.UserUpdate `json:"param"`
+}
+
+type GiftCoinReq struct {
+ UserId int64 `json:"userId" binding:"required"` // 目标用户ID
+ Coin int64 `json:"coin" binding:"required"` // 赠送金币数(正数=增加,负数=扣除)
+ Remark string `json:"remark"` // 备注(记录在流水表)
+}
+
+type UpdateVipReq struct {
+ UserId int64 `json:"userId" binding:"required"` // 用户ID
+ PackageId int64 `json:"packageId" binding:"required"` // 会员畅享套餐ID
+}
diff --git a/web/service/srv_user/user.go b/web/service/srv_user/user.go
new file mode 100644
index 0000000..f59285b
--- /dev/null
+++ b/web/service/srv_user/user.go
@@ -0,0 +1,166 @@
+package srv_user
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/cointransmod"
+ "videoweb/pub/models/rechargepackagemod"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/payment"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetUsers 分页查询用户列表
+func GetUsers(ctx context.Context, req *UserListReq) (*UserListResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.Id != nil && *req.Id > 0 {
+ filter[usermod.FN.User.Id] = *req.Id
+ }
+ if accountNo := strings.TrimSpace(req.AccountNo); accountNo != "" {
+ filter[usermod.FN.User.AccountNo] = accountNo
+ }
+ if email := strings.TrimSpace(req.Email); email != "" {
+ filter[usermod.FN.User.Email] = email
+ }
+ if req.NickName != "" {
+ filter[usermod.FN.User.NickName] = bson.M{"$regex": req.NickName}
+ }
+ if districtCode := strings.TrimSpace(req.DistrictCode); districtCode != "" {
+ filter[usermod.FN.User.DistrictCode] = districtCode
+ }
+ if req.Status != nil {
+ filter[usermod.FN.User.Status] = *req.Status
+ }
+ if req.Type != nil {
+ filter[usermod.FN.User.Type] = *req.Type
+ }
+
+ opts := options.Find().
+ SetSort(bson.M{usermod.FN.User.Id: -1}).
+ SetSkip((req.PageNum - 1) * req.PageSize).
+ SetLimit(req.PageSize)
+
+ resp := &UserListResp{}
+ total, serr := usermod.CountUsers(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ list, serr := usermod.FindManyUser(ctx, filter, opts)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = list
+ return resp, nil
+}
+
+// UpdateUser 更新指定用户信息
+func UpdateUser(ctx context.Context, id int64, param usermod.UserUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = &now
+ return usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: id},
+ bson.M{"$set": param},
+ )
+}
+
+// GiftCoin 给用户赠送/扣除金币,并写入流水记录
+// coin 为正数时增加余额,为负数时扣减(不校验余额是否充足,由调用方确保)
+func GiftCoin(ctx context.Context, req *GiftCoinReq) *srvdef.SrvErr {
+ if req.Coin == 0 {
+ return srvdef.New(srvdef.ErrParamError, "金币数量不能为 0", "")
+ }
+
+ // 原子更新余额
+ if serr := usermod.UpdateOneUser(ctx,
+ bson.M{usermod.FN.User.Id: req.UserId},
+ bson.M{
+ "$inc": bson.M{usermod.FN.User.Recharge: req.Coin},
+ "$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
+ },
+ ); serr != nil {
+ return serr
+ }
+
+ // 写流水(更新后再查余额,作为 balanceAfter)
+ flowDir := cointransmod.FlowIn
+ amount := req.Coin
+ if req.Coin < 0 {
+ flowDir = cointransmod.FlowOut
+ amount = -req.Coin
+ }
+ remark := req.Remark
+ if remark == "" {
+ remark = payment.CoinTransRemark(cointransmod.TranTypeAdminAdjust, "")
+ }
+ _ = payment.WriteCoinTransByUser(ctx, payment.WriteCoinTransOpt{
+ UserId: req.UserId,
+ FlowDir: flowDir,
+ TranType: cointransmod.TranTypeAdminAdjust,
+ Amount: amount,
+ Remark: remark,
+ })
+
+ return nil
+}
+
+// UpdateVip 根据会员畅享套餐修改用户会员信息。
+func UpdateVip(ctx context.Context, req *UpdateVipReq) *srvdef.SrvErr {
+ if req == nil || req.UserId <= 0 || req.PackageId <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "用户ID和套餐ID不能为空", "")
+ }
+ user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId})
+ if serr != nil || user == nil {
+ return srvdef.New(srvdef.ErrUserNotExsit, "", "")
+ }
+ pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
+ rechargepackagemod.FN.RechargePackage.Id: req.PackageId,
+ rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeVip,
+ rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
+ })
+ if serr != nil || pkg == nil {
+ return srvdef.New(srvdef.ErrNoData, "会员套餐不存在或未启用", "")
+ }
+ if pkg.DurationDays <= 0 {
+ return srvdef.New(srvdef.ErrParamError, "会员套餐有效天数无效", "")
+ }
+
+ now := time.Now()
+ baseTime := now
+ if user.CurrentVipExpire.After(now) {
+ baseTime = user.CurrentVipExpire
+ }
+ newExpire := baseTime.Add(time.Duration(pkg.DurationDays) * 24 * time.Hour)
+ updateDoc := bson.M{
+ "$set": bson.M{
+ usermod.FN.User.VipType: vipTypeFromDurationDays(pkg.DurationDays),
+ usermod.FN.User.ShowCardId: pkg.Id,
+ usermod.FN.User.ShowCardName: pkg.Name,
+ usermod.FN.User.CurrentVipExpire: newExpire,
+ usermod.FN.User.UpdatedAt: now,
+ },
+ }
+
+ if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId}, updateDoc); serr != nil {
+ return serr
+ }
+ return nil
+}
+
+func vipTypeFromDurationDays(days int64) pubdef.VipType {
+ if days >= 365 {
+ return pubdef.VipTypeYear
+ }
+ if days > 0 {
+ return pubdef.VipTypeMonth
+ }
+ return pubdef.VipTypeNone
+}
diff --git a/web/service/srv_videocategory/category.go b/web/service/srv_videocategory/category.go
new file mode 100644
index 0000000..f07b042
--- /dev/null
+++ b/web/service/srv_videocategory/category.go
@@ -0,0 +1,100 @@
+package srv_videocategory
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/mongos"
+
+ "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{}
+ opt := &options.FindOptions{}
+ resp := &VideoCategoryResp{}
+ if param.ID > 0 {
+ filter[videocategorymod.FN.VideoCategory.Id] = param.ID
+ }
+ if param.Name != "" {
+ filter[videocategorymod.FN.VideoCategory.Name] = bson.M{
+ "$regex": param.Name,
+ }
+ }
+
+ 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 {
+
+ ac.Id = mongos.Lid.VideoCategory.Get()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ if ac.Name == "" {
+ return srvdef.New(srvdef.ErrParamError, "", "")
+ }
+
+ serr := videocategorymod.InsertOneVideoCategory(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ 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, "", "")
+ }
+ 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}
+ 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
+ }
+ return nil
+}
diff --git a/web/service/srv_videocategory/define.go b/web/service/srv_videocategory/define.go
new file mode 100644
index 0000000..d96541f
--- /dev/null
+++ b/web/service/srv_videocategory/define.go
@@ -0,0 +1,39 @@
+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"` //分类名称
+}
+type VideoCategoryResp struct {
+ Total int64 `json:"total"`
+ List videocategorymod.VideoCategoryList `json:"list"`
+}
+type VideoCategoryUpdate struct {
+ AppId *int32 `json:"appId" bson:"appId,omitempty"` // appId,唯一id
+ Name *string `json:"name" bson:"name,omitempty"` // 名称
+ Title *string `json:"title" bson:"title,omitempty"` // APP显示
+ Desc *string `json:"desc" bson:"desc,omitempty"` // 描述
+ SortType *int64 `json:"sortType" bson:"sortType,omitempty"` // 排序方式 0:上线时间 1:观看次数 2排序
+ ContentType *int64 `json:"contentType" bson:"contentType,omitempty"` // 内容:0: 默认 1:首页 2:今日
+ Rank *int64 `json:"rank" bson:"rank,omitempty"` // 排序
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
+ Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
+ 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_videocreater/creater.go b/web/service/srv_videocreater/creater.go
new file mode 100644
index 0000000..c9da552
--- /dev/null
+++ b/web/service/srv_videocreater/creater.go
@@ -0,0 +1,101 @@
+package srv_videocreater
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "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{}
+ if param.ID > 0 {
+ filter[videocreatermod.FN.VideoCreaterInfo.Id] = param.ID
+ }
+ if param.Name != "" {
+ filter[videocreatermod.FN.VideoCreaterInfo.Name] = bson.M{
+ "$regex": param.Name,
+ }
+ }
+
+ 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()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ if ac.Name == "" {
+ return srvdef.New(srvdef.ErrParamError, "", "")
+ }
+
+ 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}
+ 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..3a61818
--- /dev/null
+++ b/web/service/srv_videocreater/define.go
@@ -0,0 +1,35 @@
+package srv_videocreater
+
+import (
+ "time"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoCreaterReq struct {
+ pubdef.Page
+ ID int64 `json:"id"` //分类ID
+ Name string `json:"name"` //分类名称
+}
+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"` // 名称
+ 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"` // 是否重新提交索引
+ IndexNow *int64 `bson:"indexNow,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..5105877
--- /dev/null
+++ b/web/service/srv_videoinfo/define.go
@@ -0,0 +1,104 @@
+package srv_videoinfo
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoInfoReq struct {
+ Id *int64 `json:"id"` // Id
+ Title *string `json:"title"` // 标题
+ CreatorId *int64 `json:"creatorId"` // 作者Id
+ Status *videoinformationmod.InformationSatus `json:"status"` // 帖子上架状态
+ CategoryId *int64 `json:"categoryId"` // 分类Id
+ TopicId *int64 `json:"topicId"` // 专题Id
+ Tag *int64 `json:"tag" ` // 标签
+ IsHot *bool `json:"isHot"` // 是否热门
+ MediaType videoinformationmod.MediaType `json:"mediaType"` // 资源类型
+ CreatorName *string `json:"creatorName"` // 作者名称
+ Keyworld *string `json:"keyworld"` // 关键字
+ VideoTaskId *string `json:"videoTaskId"` // 视频任务id
+ VideoFileId *string `json:"videoFileId"` // 切片id(切片列表的ID)
+ IsTop *bool `json:"isTop" bson:"isTop"` // 是否置顶
+ IsH265 *bool `json:"isH265"` // 是否支持 H265
+ Sort int64 `json:"sort"` // 排序方式 0=创建时间 1=上架时间 2=更新时间 3=排序值 4=播放次数
+ pubdef.Page
+ StartTime *string `json:"startTime"` // 创建时间-开始
+ EndTime *string `json:"endTime"` // 创建时间-截止
+ OptionAccount *string `json:"optionAccount"` // 最后操作人
+}
+
+type VideoInfoResp struct {
+ Total int64 `json:"total"`
+ List videoinformationmod.VideoInformationList `json:"list"`
+}
+type AddVideoReq struct {
+ FileIds []string `json:"fileId"` // 资源id
+}
+type VideoInfoUpdate struct {
+ Title *string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
+ AddedTime *time.Time `json:"addedTime,omitempty" bson:"addedTime,omitempty"`
+ CreatorId *int64 `json:"creatorId,omitempty" bson:"creatorId,omitempty"` // 作者Id
+ Keyworld *[]string `bson:"keyworld,omitempty" json:"keyworld"` //
+ MediaType *videoinformationmod.MediaType `json:"mediaType,omitempty" bson:"mediaType,omitempty"` // 资源类型
+ FirstTag *string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
+ SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
+ CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
+ UnencryptedImage *string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
+ Remark *string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注
+ Nodes *[]videoinformationmod.Node `json:"nodes,omitempty" bson:"nodes,omitempty"` // 节点
+ Status *videoinformationmod.InformationSatus `json:"status,omitempty" bson:"status,omitempty"` // 帖子上架状态
+ Sort *int64 `json:"sort,omitempty" bson:"sort,omitempty"` // 排序
+
+ Cover *string `json:"cover,omitempty" bson:"cover,omitempty"` // 视频封面
+ Height *int64 `json:"height,omitempty" bson:"height,omitempty"` // 分辨率高(视频才会存在)
+ Width *int64 `json:"width,omitempty" bson:"width,omitempty"` // 分辨率宽(视频才会存在)
+ VideoTime *int32 `bson:"videoTime,omitempty" json:"videoTime,omitempty"` // 影片长度
+
+ CategoryIds *[]int64 `json:"categoryIds,omitempty" bson:"categoryIds,omitempty"` // 分类Id
+ TopicIds *[]int64 `json:"topicIds,omitempty" bson:"topicIds,omitempty"` // 专题ID
+ Tags *[]int64 `json:"tags,omitempty" bson:"tags,omitempty"` // 标签
+ IsHot *bool `json:"isHot,omitempty" bson:"isHot,omitempty"` // 是否热门
+ UpdatedAt *time.Time `json:"updatedAt,omitempty" bson:"updatedAt,omitempty"` // 更新时间
+
+ CreatorName *string `bson:"creatorName,omitempty"` // 作者名
+ IsTop *bool `json:"isTop,omitempty" bson:"isTop,omitempty"` // 是否置顶
+ Video *videoinformationmod.VideoInfo `bson:"video,omitempty"` // 视频
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow"` // 是否重新提交索引
+ IndexNow *int64 `bson:"indexNow,omitempty"`
+ Content *string `json:"content,omitempty" bson:"content,omitempty"`
+ SecondUpdatedAt *time.Time `json:"secondUpdatedAt,omitempty" bson:"secondUpdatedAt,omitempty"` // 二次修改时间
+}
+
+type VideoInfoUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param VideoInfoUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type VideoInfoDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+
+type AddVideoCollectionReq struct {
+ Title string `json:"title,omitempty" bson:"title,omitempty"` // 帖子标题
+ 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 SetFakeWatchTimesReq struct {
+ Ids []int64 `json:"ids"` // 视频ID集合,支持单个或批量
+ WatchTimesMin int64 `json:"watchTimesMin"` // 假观看次数随机最小值
+ WatchTimesMax int64 `json:"watchTimesMax"` // 假观看次数随机最大值
+}
+
+type SetFakeCountResp struct {
+ MatchedCount int64 `json:"matchedCount"` // 匹配到的视频数量
+ ModifiedCount int64 `json:"modifiedCount"` // 实际修改的视频数量
+}
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..bd2ba0c
--- /dev/null
+++ b/web/service/srv_videoinfo/sync.go
@@ -0,0 +1,211 @@
+package srv_videoinfo
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "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/options"
+)
+
+func SyncOneVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
+ filter := bson.M{
+ "$or": []bson.M{
+ {videoinformationmod.FN.VideoInformation.Video.FileId: media.Id.Hex()},
+ {videoinformationmod.FN.VideoInformation.SyncId: media.Id.Hex()},
+ },
+ }
+ videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, filter)
+ if serr != nil {
+ // 新加入的视频
+ if serr.Code == srvdef.ErrNoData {
+ videoInfo = media.ToVideoInfo()
+ if videoInfo == nil {
+ log.Error("VideoSync FindOneVideoInformation", log.Any("MediaId", media.Id))
+ return srvdef.New(srvdef.ErrNoData, "videoInfo is nil")
+ }
+ videoInfo.IndexNow = 0
+ if len(media.Publish) > 0 {
+ creater, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Name: media.Publish,
+ })
+ if serr != nil && serr.Code != srvdef.ErrNoData {
+ log.Error("VideoSync.FindOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
+ return serr
+ }
+ if creater == nil {
+ createrId := mongos.Lid.VideoCreator.Get()
+ creater = &videocreatermod.VideoCreaterInfo{
+ Id: createrId,
+ Name: media.Publish,
+ Status: true,
+ Sort: 0,
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ }
+ serr = videocreatermod.InsertOneVideoCreaterInfo(ctx, creater)
+ if serr != nil {
+ log.Error("VideoSync.InsertOneVideoCreaterInfo", log.Any("media", media), log.E(serr))
+ return serr
+ }
+ }
+ videoInfo.CreatorId = creater.Id
+ videoInfo.CreatorName = creater.Name
+ }
+ serr = videoinformationmod.InsertOneVideoInformation(ctx, videoInfo)
+ if serr != nil {
+ log.Error("VideoSync InsertOneVideoInformation", log.Any("videoInfo", videoInfo), log.E(serr))
+ return serr
+ }
+ } else {
+ log.Error("VideoSync FindOneVideoInformation", log.Any("filter", filter), log.E(serr))
+ return serr
+ }
+ } else {
+ // 更新资源
+ nodes := make([]videoinformationmod.Node, 0)
+ if videoInfo != nil {
+ // 更新节点里面图片水印 以本地视频列表的节点为主
+ for _, node := range videoInfo.Nodes {
+ isFind := false
+ if len(node.NodeId) > 0 {
+ for _, v := range media.Nodes {
+ if node.NodeId == v.NodeId {
+ isFind = true
+ node.Images = v.Images
+ nodes = append(nodes, node)
+ break
+ }
+ }
+ }
+ if !isFind {
+ nodes = append(nodes, node)
+ }
+ }
+ } else {
+ nodes = media.Nodes
+ }
+ setDoc := bson.M{
+ videoinformationmod.FN.VideoInformation.Nodes: nodes,
+ videoinformationmod.FN.VideoInformation.Keyworld: media.Keyworld,
+ videoinformationmod.FN.VideoInformation.FirstTag: media.FirstTag,
+ videoinformationmod.FN.VideoInformation.SecondTags: media.SecondTags,
+ videoinformationmod.FN.VideoInformation.CoverImg: media.CoverImg,
+ videoinformationmod.FN.VideoInformation.UnencryptedImage: media.UnencryptedImage,
+ }
+ if len(media.FieldNameFs) > 0 {
+ video := videoinformationmod.VideoInfo{
+ TaskId: media.TaskID,
+ FileId: media.Id.Hex(),
+ Url: media.FieldNameFs,
+ Cover: media.CoverImg,
+ Height: media.Height,
+ Width: media.Width,
+ VideoTime: media.VideoTime,
+ UnencryptedPreMp4: media.UnencryptedPreMp4,
+ PreMp4FileNameFs: media.PreMp4FileNameFs,
+ }
+ if !strings.HasSuffix(video.Url, ".m3u8") {
+ video.Url += ".m3u8"
+ }
+ setDoc[videoinformationmod.FN.VideoInformation.Video.TaskId] = video.TaskId
+ setDoc[videoinformationmod.FN.VideoInformation.Video.FileId] = video.FileId
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Url] = video.Url
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Cover] = video.Cover
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Height] = video.Height
+ setDoc[videoinformationmod.FN.VideoInformation.Video.Width] = video.Width
+ setDoc[videoinformationmod.FN.VideoInformation.Video.VideoTime] = video.VideoTime
+ setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = video.UnencryptedPreMp4
+ }
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id}, bson.M{"$set": setDoc})
+ if serr != nil {
+ return serr
+ }
+ }
+ _ = syncNodeVideo(ctx, media)
+ return nil
+}
+
+// SyncUnencryptedOne 仅补齐单条资源的 UnencryptedImage / UnencryptedPreMp4 字段。
+// 源数据对应字段为空时跳过该字段(避免覆盖本地已有值);本地无对应资源时直接返回。
+// 幂等:可重复执行,用于历史数据补齐。
+func SyncUnencryptedOne(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
+ if media.UnencryptedImage == "" && media.UnencryptedPreMp4 == "" {
+ return nil
+ }
+ filter := bson.M{videoinformationmod.FN.VideoInformation.Video.FileId: media.Id.Hex()}
+ videoInfo, serr := videoinformationmod.FindOneVideoInformation(ctx, filter)
+ if serr != nil {
+ return serr
+ }
+ setDoc := bson.M{}
+ if media.UnencryptedImage != "" {
+ setDoc[videoinformationmod.FN.VideoInformation.UnencryptedImage] = media.UnencryptedImage
+ }
+ // 仅当源端有主视频文件时,才补 Video.UnencryptedPreMp4(避免向空的 Video 子结构写字段)
+ if media.UnencryptedPreMp4 != "" && len(media.FieldNameFs) > 0 {
+ setDoc[videoinformationmod.FN.VideoInformation.Video.UnencryptedPreMp4] = media.UnencryptedPreMp4
+ }
+ if len(setDoc) == 0 {
+ return nil
+ }
+ return videoinformationmod.UpdateOneVideoInformation(ctx,
+ bson.M{videoinformationmod.FN.VideoInformation.Id: videoInfo.Id},
+ bson.M{"$set": setDoc})
+}
+
+// syncNodeVideo 同步节点里的视频
+func syncNodeVideo(ctx context.Context, media proxy.MediaResp) *srvdef.SrvErr {
+ filter := bson.M{"nodes.nodeId": media.Id.Hex()}
+ opts := &options.FindOptions{}
+ opts.SetLimit(100)
+ videoInfo, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opts)
+ if serr != nil {
+ log.Warn("syncNodeVideo", log.Any("filter", filter), log.E(serr))
+ return serr
+ }
+ if !strings.HasSuffix(media.FieldNameFs, ".m3u8") {
+ media.FieldNameFs += ".m3u8"
+ }
+ if len(videoInfo) > 0 {
+ // 更新视频路径
+ for _, video := range videoInfo {
+ if video.Nodes != nil && len(video.Nodes) > 0 {
+ isFind := false
+ for i, node := range video.Nodes {
+ if node.Videos != nil && len(node.Videos) > 0 {
+ for j, v := range node.Videos {
+ if v.FileId == media.Id.Hex() {
+ node.Videos[j].Url = media.FieldNameFs
+ isFind = true
+ break
+ }
+ }
+ }
+ if isFind {
+ video.Nodes[i] = node
+ break
+ }
+ }
+ if isFind {
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: video.Id}, bson.M{"$set": bson.M{
+ videoinformationmod.FN.VideoInformation.UpdatedAt: time.Now(),
+ videoinformationmod.FN.VideoInformation.Nodes: video.Nodes,
+ }})
+ if serr != nil {
+ log.Warn("syncNodeVideo UpdateOneVideoInformation failed", log.Any("filter", filter), log.E(serr))
+ continue
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
diff --git a/web/service/srv_videoinfo/videoinfo.go b/web/service/srv_videoinfo/videoinfo.go
new file mode 100644
index 0000000..5ccdea1
--- /dev/null
+++ b/web/service/srv_videoinfo/videoinfo.go
@@ -0,0 +1,364 @@
+package srv_videoinfo
+
+import (
+ "context"
+ "math/rand"
+ "net/http"
+ "time"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/videocreatermod"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+ "videoweb/pub/storage"
+ "videoweb/pub/storage/proxy"
+ "videoweb/web/service/srv_report"
+ "videoweb/web/service/srv_search"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetVideoInfos 按照参数搜索APP
+func GetVideoInfos(ctx context.Context, param *VideoInfoReq) (*VideoInfoResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ filter[videoinformationmod.FN.VideoInformation.MediaType] = param.MediaType
+ if param.Id != nil && *param.Id > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Id] = *param.Id
+ }
+ if param.Title != nil && len(*param.Title) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Title] = bson.M{"$regex": *param.Title}
+ }
+ if param.CreatorId != nil && *param.CreatorId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CreatorId] = *param.CreatorId
+ }
+ if param.TopicId != nil && *param.TopicId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.TopicIds] = bson.M{"$in": []int64{*param.TopicId}}
+ }
+ if param.Status != nil {
+ filter[videoinformationmod.FN.VideoInformation.Status] = *param.Status
+ }
+ if param.CategoryId != nil && *param.CategoryId > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CategoryIds] = bson.M{"$in": []int64{*param.CategoryId}}
+ }
+ if param.Tag != nil && *param.Tag > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Tags] = bson.M{"$in": []int64{*param.Tag}}
+ }
+ if param.IsHot != nil {
+ filter[videoinformationmod.FN.VideoInformation.IsHot] = *param.IsHot
+ }
+ if param.CreatorName != nil && len(*param.CreatorName) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.CreatorName] = bson.M{"$regex": *param.CreatorName}
+ }
+ if param.Keyworld != nil && len(*param.Keyworld) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Keyworld] = bson.M{"$in": []string{*param.Keyworld}}
+ }
+ if param.VideoTaskId != nil && len(*param.VideoTaskId) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Video.TaskId] = *param.VideoTaskId
+ }
+ if param.VideoFileId != nil && len(*param.VideoFileId) > 0 {
+ filter[videoinformationmod.FN.VideoInformation.Video.FileId] = *param.VideoFileId
+ }
+ if param.IsTop != nil {
+ filter[videoinformationmod.FN.VideoInformation.IsTop] = *param.IsTop
+ }
+ if param.IsH265 != nil {
+ if *param.IsH265 {
+ filter[videoinformationmod.FN.VideoInformation.HevcStatus] = videoinformationmod.HevcStatusSuccess
+ } else {
+ filter[videoinformationmod.FN.VideoInformation.HevcStatus] = bson.M{"$in": []videoinformationmod.HevcStatus{
+ videoinformationmod.HevcStatusNone,
+ videoinformationmod.HevcStatusPending,
+ videoinformationmod.HevcStatusFailed,
+ }}
+ }
+ }
+ if param.StartTime != nil && param.EndTime != nil {
+ start := utils.GetDayBeginByTime(*param.StartTime)
+ end := utils.GetDayEndByTime(*param.EndTime)
+ if end.Unix() > start.Unix() {
+ filter[videoinformationmod.FN.VideoInformation.CreatedAt] = bson.M{
+ "$gte": start, "$lte": end,
+ }
+ }
+ }
+ if param.OptionAccount != nil {
+ filter[videoinformationmod.FN.VideoInformation.OptionAccount] = *param.OptionAccount
+ }
+ opt := &options.FindOptions{}
+ resp := &VideoInfoResp{
+ Total: 0,
+ List: make(videoinformationmod.VideoInformationList, 0),
+ }
+
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: -1})
+ switch param.Sort {
+ case 0:
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.CreatedAt: -1})
+ case 1:
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.AddedTime: -1})
+ case 2:
+ opt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.UpdatedAt: -1})
+ case 3:
+ opt.SetSort(bson.D{
+ {videoinformationmod.FN.VideoInformation.Sort, -1},
+ {videoinformationmod.FN.VideoInformation.AddedTime, -1},
+ })
+ case 4:
+ opt.SetSort(bson.D{
+ {videoinformationmod.FN.VideoInformation.Real.WatchTimes, -1},
+ {videoinformationmod.FN.VideoInformation.AddedTime, -1},
+ })
+ }
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := videoinformationmod.CountVideoInformations(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// SyncVideoInfo 同步视频
+func SyncVideoInfo(ctx context.Context,
+ req *AddVideoReq) *srvdef.SrvErr {
+
+ ids := make([]string, 0)
+ for _, id := range req.FileIds {
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ return nil
+ }
+
+ queryMediaReq := &proxy.MediaQueryesReq{
+ Ids: ids,
+ }
+ resp, err := storage.QueryMedias(ctx, queryMediaReq, "")
+ if err != nil || resp.Code != http.StatusOK {
+ if resp != nil {
+ log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err), log.Any("respCode", resp.Code))
+ } else {
+ log.Warn("AddVideoInfo.QueryMedias failed", log.Any("filter", req.FileIds), log.E(err))
+ }
+ return srvdef.New(srvdef.ErrMediaNotExsit, err.Error(), err.Error())
+ }
+ if len(resp.Data) == 0 {
+ return nil
+ }
+ for _, media := range resp.Data {
+ serr := SyncOneVideo(ctx, media)
+ if serr != nil {
+ log.Warn("SyncVideoInfo SYncOneVideo failed", log.Any("filter", req.FileIds), log.E(serr))
+ continue
+ }
+ }
+ return nil
+}
+
+// DelVideoInfo 删除指定APP
+func DelVideoInfo(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
+ //删除
+ count, serr := videoinformationmod.DelOneVideoInformation(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ // 同步删除 ES 中的数据,避免搜索到已删除的视频
+ go srv_search.DelVideoFromES(id)
+ return nil
+}
+
+// SetVideoFakeWatchTimes 单/批量随机设置视频假观看次数
+func SetVideoFakeWatchTimes(ctx context.Context, req *SetFakeWatchTimesReq) (*SetFakeCountResp, *srvdef.SrvErr) {
+ if req == nil || len(req.Ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ if req.WatchTimesMin < 0 || req.WatchTimesMax < req.WatchTimesMin {
+ return nil, srvdef.New(srvdef.ErrParamError, "观看次数区间无效")
+ }
+ ids, err := normalizeFakeFlowIds(req.Ids)
+ if err != nil {
+ return nil, err
+ }
+ return setVideoFakeField(ctx, ids, videoinformationmod.FN.VideoInformation.Fake.WatchTimes, req.WatchTimesMin, req.WatchTimesMax)
+}
+
+// setVideoFakeField 使用BulkWrite批量设置假流量字段,避免批量请求逐条访问数据库
+func setVideoFakeField(ctx context.Context, ids []int64, field string, min, max int64) (*SetFakeCountResp, *srvdef.SrvErr) {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ docs := make([]mongo.WriteModel, 0, len(ids))
+ for _, id := range ids {
+ docs = append(docs, mongo.NewUpdateOneModel().
+ SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: id}).
+ SetUpdate(bson.M{"$set": bson.M{field: randomFakeFlowValue(r, min, max)}}))
+ }
+ result, err := videoinformationmod.BulkWriteVideoInformation(ctx, docs, options.BulkWrite().SetOrdered(false))
+ if err != nil {
+ return nil, err
+ }
+ if result.MatchedCount == 0 {
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &SetFakeCountResp{MatchedCount: result.MatchedCount, ModifiedCount: result.ModifiedCount}, nil
+}
+
+// normalizeFakeFlowIds 校验并去重视频ID,减少无效批量写入
+func normalizeFakeFlowIds(rawIds []int64) ([]int64, *srvdef.SrvErr) {
+ ids := make([]int64, 0, len(rawIds))
+ seen := make(map[int64]struct{}, len(rawIds))
+ for _, id := range rawIds {
+ if id <= 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids包含无效id")
+ }
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ seen[id] = struct{}{}
+ ids = append(ids, id)
+ }
+ if len(ids) == 0 {
+ return nil, srvdef.New(srvdef.ErrParamError, "ids不能为空")
+ }
+ return ids, nil
+}
+
+func randomFakeFlowValue(r *rand.Rand, min, max int64) int64 {
+ if min == max {
+ return min
+ }
+ return min + r.Int63n(max-min+1)
+}
+
+// UpdateVideoInfo 更新指定APP
+func UpdateVideoInfo(ctx context.Context, id, userId int64, param VideoInfoUpdate, ua useragent.UA) *srvdef.SrvErr {
+ now := time.Now()
+ info, serr := videoinformationmod.FindOneVideoInformation(ctx, bson.M{videoinformationmod.FN.VideoInformation.Id: id})
+ if serr != nil {
+ return serr
+ }
+ param.UpdatedAt = &now
+ param.SecondUpdatedAt = &now
+ if param.CreatorId != nil {
+ name := ""
+ if *param.CreatorId == 0 {
+ param.CreatorName = &name
+ } else {
+ // 查询作者
+ c, serr := videocreatermod.FindOneVideoCreaterInfo(ctx, bson.M{
+ videocreatermod.FN.VideoCreaterInfo.Id: *param.CreatorId,
+ })
+ if serr != nil {
+ return serr
+ }
+ name = c.Name
+ param.CreatorName = &name
+ }
+ }
+
+ if param.Nodes != nil && len(*param.Nodes) > 0 {
+ nodes := *param.Nodes
+ for i, node := range nodes {
+ if len(node.NodeId) == 0 {
+ node.NodeId = primitive.NewObjectID().Hex()
+ nodes[i] = node
+ }
+ }
+ param.Nodes = &nodes
+ }
+ if param.Cover != nil && len(*param.Cover) > 0 {
+ if len(info.Video.Url) > 0 {
+ info.Video.Cover = *param.Cover
+ param.Video = &info.Video
+ }
+ }
+ //if param.Status != nil && *param.Status != info.Status {
+ // if *param.Status == videoinformationmod.InformationSatusVerifyOK {
+ // param.AddedTime = &now
+ // }
+ //}
+ adFilter := bson.M{videoinformationmod.FN.VideoInformation.Id: id}
+ param.IndexNow = nil
+ if (param.Status != nil && *param.Status == videoinformationmod.InformationSatusVerifyOK) ||
+ (param.IsReIndexNow != nil && *param.IsReIndexNow) {
+ indexNow := int64(0)
+ param.IndexNow = &indexNow
+ }
+ serr = videoinformationmod.UpdateOneVideoInformation(ctx, adFilter, bson.M{"$set": param})
+ if serr != nil {
+ return serr
+ }
+ // status 发生变更时实时同步 ES,避免搜索结果与 MongoDB 不一致
+ if param.Status != nil {
+ newStatus := *param.Status
+ go func() {
+ if newStatus != videoinformationmod.InformationSatusVerifyOK {
+ // 下架/重置状态:立即从 ES 删除
+ srv_search.DelVideoFromES(id)
+ } else {
+ // 上架:从 MongoDB 读取最新完整数据后写入 ES
+ latest, serr := videoinformationmod.FindOneVideoInformation(context.Background(), adFilter)
+ if serr != nil {
+ log.Warn("UpdateVideoInfo: read latest for ES upsert failed", log.Any("id", id), log.E(serr))
+ return
+ }
+ srv_search.UpsertVideoToES(latest)
+ }
+ }()
+ }
+ if param.Status != nil && info.Status != *param.Status {
+ if flag := srv_report.VideoStatusFlag(*param.Status); flag != 0 {
+ mediaCopy := *info
+ mediaCopy.Status = *param.Status
+ godef.Go(func() {
+ srv_report.ReportVideoStatusChangeEvent(&mediaCopy, flag, userId, ua)
+ })
+ }
+ }
+ return nil
+}
+
+func AddVideoCollection(ctx context.Context, param *AddVideoCollectionReq) *srvdef.SrvErr {
+ info := videoinformationmod.VideoInformation{
+ Id: mongos.Lid.VideoInfo.Get(),
+ Title: param.Title,
+ MediaType: videoinformationmod.MediaType_Collection,
+ AddedTime: time.Now(),
+ Keyworld: param.Keyworld,
+ FirstTag: param.FirstTag,
+ SecondTags: param.SecondTags,
+ CoverImg: param.CoverImg,
+ Desc: param.Desc,
+ Remark: param.Remark,
+ Nodes: make([]videoinformationmod.Node, 0),
+ Status: videoinformationmod.InformationSatusWaiting,
+ Video: videoinformationmod.VideoInfo{},
+ CategoryIds: make([]int64, 0),
+ Tags: param.Tags,
+ Real: videoinformationmod.Flow{},
+ VideoCount: 0,
+ IndexNow: 0,
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ }
+
+ return videoinformationmod.InsertOneVideoInformation(ctx, &info)
+}
diff --git a/web/service/srv_videotag/define.go b/web/service/srv_videotag/define.go
new file mode 100644
index 0000000..97bf96a
--- /dev/null
+++ b/web/service/srv_videotag/define.go
@@ -0,0 +1,35 @@
+package srv_videotag
+
+import (
+ "time"
+ "videoweb/pub/models/videotagmod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoTagInfoReq struct {
+ pubdef.Page
+ Title *string `json:"title"`
+ Id *int64 `json:"id"`
+}
+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相关信息
+ 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..ef24648
--- /dev/null
+++ b/web/service/srv_videotag/hottag.go
@@ -0,0 +1,97 @@
+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} 索引)
+// watchTimes 由 app/service/videoser.VideoDetails 在用户进入视频详情页时对关联标签各 $inc: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 IpStatResp struct {
+ TodayIP int64 `json:"todayIP"`
+ YesterdayIP int64 `json:"yesterdayIP"`
+}
+
+func FetchIpStat(ctx context.Context) *IpStatResp {
+ resp := &IpStatResp{}
+ now := time.Now().In(utils.LocBJ)
+ today := now.Format("20060102")
+ yesterday := now.AddDate(0, 0, -1).Format("20060102")
+
+ if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, today); err == nil {
+ resp.TodayIP = cnt
+ } else {
+ log.Warn("FetchIpStat: FindCount today failed",
+ log.Any("appId", pubdef.AppId), log.E(err))
+ }
+ if cnt, err := ipdailystatmod.FindCount(ctx, pubdef.AppId, yesterday); err == nil {
+ resp.YesterdayIP = cnt
+ } else {
+ log.Warn("FetchIpStat: FindCount yesterday failed",
+ log.Any("appId", pubdef.AppId), log.E(err))
+ }
+ return resp
+}
diff --git a/web/service/srv_videotag/taginfo.go b/web/service/srv_videotag/taginfo.go
new file mode 100644
index 0000000..b89f672
--- /dev/null
+++ b/web/service/srv_videotag/taginfo.go
@@ -0,0 +1,97 @@
+package srv_videotag
+
+import (
+ "context"
+ "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"
+)
+
+// GetVideoTagInfos 按照参数搜索视频标签
+func GetVideoTagInfos(ctx context.Context, param *VideoTagInfoReq) (*VideoTagInfoResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ opt := &options.FindOptions{}
+ resp := &VideoTagInfoResp{}
+ if param.Id != nil && *param.Id > 0 {
+ filter[videotagmod.FN.VideoTagInfo.Id] = *param.Id
+ }
+ if param.Title != nil && *param.Title != "" {
+ filter[videotagmod.FN.VideoTagInfo.Title] = bson.M{"$regex": *param.Title}
+ }
+
+ opt.SetSort(bson.M{videotagmod.FN.VideoTagInfo.Id: 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
+ if ac.Title == "" {
+ return srvdef.New(srvdef.ErrParamError, "", "")
+ }
+
+ 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/service/srv_videotopic/define.go b/web/service/srv_videotopic/define.go
new file mode 100644
index 0000000..4c5d8f1
--- /dev/null
+++ b/web/service/srv_videotopic/define.go
@@ -0,0 +1,39 @@
+package srv_videotopic
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videotopicmod"
+ "videoweb/pub/pubdef"
+)
+
+type VideoTopicReq struct {
+ VideoCategoryId *int64 `json:"videoCategoryId" bson:"videoCategoryId"`
+ pubdef.Page
+}
+type VideoTopicResp struct {
+ Total int64 `json:"total"`
+ List videotopicmod.VideoTopicModelList `json:"list"`
+}
+type VideoTopicUpdate struct {
+ Name *string `json:"name" bson:"name,omitempty"` // 名称
+ Title *string `json:"title" bson:"title,omitempty"` // APP显示
+ Desc *string `json:"desc" bson:"desc,omitempty"` // 描述
+ Rank *int64 `json:"rank" bson:"rank,omitempty"` //
+ SeoInfo *pubdef.SeoValue `json:"seoInfo" bson:"seoInfo,omitempty"` // Seo相关信息
+ Status *bool `json:"status" bson:"status,omitempty"` // 上架状态
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
+ IsReIndexNow *bool `json:"isReIndexNow" bson:"isReIndexNow,omitempty"` // 是否重新提交索引
+ IndexNow *int64 `bson:"indexNow,omitempty"`
+ Image *string `json:"image" bson:"image,omitempty"` // 背景
+ VideoCategoryId *int64 `json:"videoCategoryId" bson:"videoCategoryId,omitempty"` // 视频分类id
+ VideoTopicType *videoinformationmod.MediaType `json:"videoTopicType" bson:"videoTopicType"` // 视频类型
+}
+type VideoTopicUpdateReq struct {
+ Id int64 `json:"id" binding:"required"`
+ Param VideoTopicUpdate `json:"param" bson:"param"` // appId,唯一id
+}
+
+type VideoTopicDelReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
diff --git a/web/service/srv_videotopic/topic.go b/web/service/srv_videotopic/topic.go
new file mode 100644
index 0000000..86f5ecc
--- /dev/null
+++ b/web/service/srv_videotopic/topic.go
@@ -0,0 +1,90 @@
+package srv_videotopic
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/videotopicmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetVideoTopics 按照参数搜索视频分类
+func GetVideoTopics(ctx context.Context, param *VideoTopicReq) (*VideoTopicResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if param.VideoCategoryId != nil {
+ filter[videotopicmod.FN.VideoTopicModel.VideoCategoryId] = *param.VideoCategoryId
+ }
+ opt := &options.FindOptions{}
+ resp := &VideoTopicResp{}
+
+ opt.SetSort(bson.M{videotopicmod.FN.VideoTopicModel.Rank: -1})
+ opt.SetSkip((param.PageNum - 1) * param.PageSize).SetLimit(param.PageSize)
+
+ total, serr := videotopicmod.CountVideoTopicModels(ctx, filter)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.Total = total
+ if total == 0 {
+ return resp, nil
+ }
+ ads, serr := videotopicmod.FindManyVideoTopicModels(ctx, filter, opt)
+ if serr != nil {
+ return resp, serr
+ }
+ resp.List = ads
+
+ return resp, nil
+}
+
+// AddVideoTopic 添加一个视频分类
+func AddVideoTopic(ctx context.Context,
+ ac *videotopicmod.VideoTopicModel) *srvdef.SrvErr {
+
+ ac.Id = mongos.Lid.VideoTopic.Get()
+ now := time.Now()
+ ac.CreatedAt = now
+ ac.UpdatedAt = now
+
+ serr := videotopicmod.InsertOneVideoTopicModel(ctx, ac)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
+
+// DelVideoTopic 删除指定视频分类
+func DelVideoTopic(ctx context.Context, id int64) *srvdef.SrvErr {
+ adFilter := bson.M{videotopicmod.FN.VideoTopicModel.Id: id}
+ //删除
+ count, serr := videotopicmod.DelOneVideoTopicModel(ctx, adFilter)
+ if serr != nil {
+ return serr
+ }
+ if count == 0 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateVideoTopic 更新指定视频分类
+func UpdateVideoTopic(ctx context.Context, id int64, param VideoTopicUpdate) *srvdef.SrvErr {
+ now := time.Now()
+ param.UpdatedAt = now
+
+ adFilter := bson.M{videotopicmod.FN.VideoTopicModel.Id: id}
+ param.IndexNow = nil
+ if param.IsReIndexNow != nil && *param.IsReIndexNow {
+ indexNow := int64(0)
+ param.IndexNow = &indexNow
+ }
+ updateDoc := bson.M{"$set": param}
+ serr := videotopicmod.UpdateOneVideoTopicModel(ctx, adFilter, updateDoc)
+ if serr != nil {
+ return serr
+ }
+ return nil
+}
diff --git a/web/service/srv_viporder/define.go b/web/service/srv_viporder/define.go
new file mode 100644
index 0000000..1347782
--- /dev/null
+++ b/web/service/srv_viporder/define.go
@@ -0,0 +1,44 @@
+package srv_viporder
+
+import (
+ "time"
+ "videoweb/pub/models/vipordermod"
+ "videoweb/pub/pubdef"
+)
+
+type VipOrderReq struct {
+ pubdef.Page
+ UserId *int64 `json:"userId"` // 用户ID
+ OrderType *vipordermod.OrderType `json:"orderType"` // 订单类型:1会员 2金币
+ Status *pubdef.VipOrderStatus `json:"status"` // 订单状态
+ TradeNo *string `json:"tradeNo"` // 订单号
+ DistrictCode string `json:"districtCode"` // 渠道码
+ StartTime *string `json:"startTime"` // 创建时间-开始
+ EndTime *string `json:"endTime"` // 创建时间-结束
+}
+
+type VipOrderResp struct {
+ Total int64 `json:"total"`
+ List vipordermod.VipOrderList `json:"list"`
+}
+
+type SyncDistrictCodeReq struct {
+ MaxSize int64 `json:"maxSize"` // 本次最多扫描的空渠道码订单数,默认500,最大2000
+}
+
+type SyncDistrictCodeResp struct {
+ Scanned int64 `json:"scanned"` // 本次扫描的空渠道码订单数
+ OrderUpdated int64 `json:"orderUpdated"` // 成功回填的订单数
+ UserUpdated int64 `json:"userUpdated"` // 成功回填的用户数
+ UserSkipped int64 `json:"userSkipped"` // 用户本身没有渠道码
+ HasMore bool `json:"hasMore"` // 是否还有未处理的历史订单
+}
+
+type timeLayout = string
+
+const layout timeLayout = "2006-01-02 15:04:05"
+
+func parseTime(s string) (time.Time, bool) {
+ t, err := time.ParseInLocation(layout, s, time.Local)
+ return t, err == nil
+}
diff --git a/web/service/srv_viporder/order.go b/web/service/srv_viporder/order.go
new file mode 100644
index 0000000..69ded25
--- /dev/null
+++ b/web/service/srv_viporder/order.go
@@ -0,0 +1,234 @@
+package srv_viporder
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/models/vipordermod"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// GetVipOrders 分页查询会员购买记录(固定 OrderType=会员)
+func GetVipOrders(ctx context.Context, req *VipOrderReq) (*VipOrderResp, *srvdef.SrvErr) {
+ orderType := vipordermod.OrderTypeVip
+ reqCopy := *req
+ reqCopy.OrderType = &orderType
+ return GetOrders(ctx, &reqCopy)
+}
+
+// GetOrders 分页查询充值订单,支持按 OrderType 筛选(不传则查全部类型)
+func GetOrders(ctx context.Context, req *VipOrderReq) (*VipOrderResp, *srvdef.SrvErr) {
+ filter := bson.M{}
+ if req.OrderType != nil {
+ filter[vipordermod.FN.VipOrder.OrderType] = *req.OrderType
+ }
+ if req.UserId != nil && *req.UserId > 0 {
+ filter[vipordermod.FN.VipOrder.UserId] = *req.UserId
+ }
+ if req.Status != nil {
+ filter[vipordermod.FN.VipOrder.Status] = *req.Status
+ }
+ if req.TradeNo != nil && *req.TradeNo != "" {
+ filter[vipordermod.FN.VipOrder.TradeNo] = *req.TradeNo
+ }
+ if districtCode := strings.TrimSpace(req.DistrictCode); districtCode != "" {
+ filter[vipordermod.FN.VipOrder.DistrictCode] = districtCode
+ }
+
+ timeFilter := bson.M{}
+ if req.StartTime != nil && *req.StartTime != "" {
+ if t, ok := parseTime(*req.StartTime); ok {
+ timeFilter["$gte"] = t
+ }
+ }
+ if req.EndTime != nil && *req.EndTime != "" {
+ if t, ok := parseTime(*req.EndTime); ok {
+ timeFilter["$lte"] = t
+ }
+ }
+ if len(timeFilter) > 0 {
+ filter[vipordermod.FN.VipOrder.CreatedAt] = timeFilter
+ }
+
+ total, serr := vipordermod.CountVipOrder(ctx, filter)
+ if serr != nil {
+ return nil, serr
+ }
+ resp := &VipOrderResp{Total: total}
+ if total == 0 {
+ resp.List = vipordermod.VipOrderList{}
+ return resp, nil
+ }
+
+ opt := options.Find().
+ SetSort(bson.D{{Key: vipordermod.FN.VipOrder.CreatedAt, Value: -1}}).
+ SetSkip((req.PageNum - 1) * req.PageSize).
+ SetLimit(req.PageSize)
+
+ list, serr := vipordermod.FindManyVipOrder(ctx, filter, opt)
+ if serr != nil {
+ return nil, serr
+ }
+ resp.List = list
+ return resp, nil
+}
+
+const (
+ syncDistrictDefaultSize = int64(500)
+ syncDistrictMaxSize = int64(2000)
+ syncDistrictBatchSize = int64(200)
+)
+
+// SyncDistrictCode 将历史订单中空的渠道码,按用户当前 districtCode 回填。
+func SyncDistrictCode(ctx context.Context, req *SyncDistrictCodeReq) (*SyncDistrictCodeResp, *srvdef.SrvErr) {
+ maxSize := syncDistrictDefaultSize
+ if req != nil && req.MaxSize > 0 {
+ maxSize = req.MaxSize
+ }
+ if maxSize > syncDistrictMaxSize {
+ maxSize = syncDistrictMaxSize
+ }
+
+ resp := &SyncDistrictCodeResp{}
+ lastId := int64(0)
+ updatedUsers := make(map[int64]struct{})
+ skippedUsers := make(map[int64]struct{})
+
+ for resp.Scanned < maxSize {
+ limit := syncDistrictBatchSize
+ if remain := maxSize - resp.Scanned; remain < limit {
+ limit = remain
+ }
+ filter := emptyDistrictSyncFilter(lastId, skippedUsers)
+ list, serr := vipordermod.FindManyVipOrder(ctx, filter, options.Find().
+ SetProjection(bson.D{
+ {Key: vipordermod.FN.VipOrder.Id, Value: 1},
+ {Key: vipordermod.FN.VipOrder.UserId, Value: 1},
+ }).
+ SetSort(bson.D{{Key: vipordermod.FN.VipOrder.Id, Value: 1}}).
+ SetLimit(limit))
+ if serr != nil {
+ return nil, serr
+ }
+ if len(list) == 0 {
+ break
+ }
+ resp.Scanned += int64(len(list))
+ lastId = list[len(list)-1].Id
+ if int64(len(list)) == limit && resp.Scanned >= maxSize {
+ resp.HasMore = true
+ }
+
+ userIds := make([]int64, 0, len(list))
+ seen := make(map[int64]struct{}, len(list))
+ for _, order := range list {
+ if order == nil || order.UserId <= 0 {
+ continue
+ }
+ if _, ok := seen[order.UserId]; ok {
+ continue
+ }
+ seen[order.UserId] = struct{}{}
+ userIds = append(userIds, order.UserId)
+ }
+ if len(userIds) == 0 {
+ continue
+ }
+
+ users, serr := usermod.FindManyUser(ctx, bson.M{
+ usermod.FN.User.Id: bson.M{"$in": userIds},
+ }, options.Find().SetProjection(bson.D{
+ {Key: usermod.FN.User.Id, Value: 1},
+ {Key: usermod.FN.User.DistrictCode, Value: 1},
+ }))
+ if serr != nil {
+ return nil, serr
+ }
+ userMap := make(map[int64]*usermod.User, len(users))
+ for _, user := range users {
+ if user != nil {
+ userMap[user.Id] = user
+ }
+ }
+
+ now := time.Now()
+ for _, userId := range userIds {
+ user := userMap[userId]
+ code := ""
+ if user != nil {
+ code = strings.TrimSpace(user.DistrictCode)
+ }
+ if code == "" {
+ skippedUsers[userId] = struct{}{}
+ continue
+ }
+ modified, serr := vipordermod.UpdateManyVipOrder(ctx,
+ mergeEmptyDistrictFilter(bson.M{vipordermod.FN.VipOrder.UserId: userId}),
+ bson.M{"$set": bson.M{
+ vipordermod.FN.VipOrder.DistrictCode: code,
+ vipordermod.FN.VipOrder.UpdatedAt: now,
+ }},
+ )
+ if serr != nil {
+ return nil, serr
+ }
+ if modified > 0 {
+ updatedUsers[userId] = struct{}{}
+ resp.OrderUpdated += modified
+ }
+ }
+ }
+
+ resp.UserUpdated = int64(len(updatedUsers))
+ resp.UserSkipped = int64(len(skippedUsers))
+ if !resp.HasMore {
+ remain, serr := vipordermod.FindManyVipOrder(ctx, emptyDistrictSyncFilter(lastId, skippedUsers),
+ options.Find().SetProjection(bson.D{{Key: vipordermod.FN.VipOrder.Id, Value: 1}}).SetLimit(1))
+ if serr != nil {
+ return nil, serr
+ }
+ resp.HasMore = len(remain) > 0
+ }
+ return resp, nil
+}
+
+func emptyDistrictSyncFilter(lastId int64, skippedUsers map[int64]struct{}) bson.M {
+ conds := []bson.M{vipordermod.EmptyDistrictFilter()}
+ if lastId > 0 {
+ conds = append(conds, bson.M{vipordermod.FN.VipOrder.Id: bson.M{"$gt": lastId}})
+ }
+ if skipIds := mapKeys(skippedUsers); len(skipIds) > 0 {
+ conds = append(conds, bson.M{vipordermod.FN.VipOrder.UserId: bson.M{"$nin": skipIds}})
+ }
+ if len(conds) == 1 {
+ return conds[0]
+ }
+ return bson.M{"$and": conds}
+}
+
+func mergeEmptyDistrictFilter(extra bson.M) bson.M {
+ filter := vipordermod.EmptyDistrictFilter()
+ if len(extra) == 0 {
+ return filter
+ }
+ conds := []bson.M{filter}
+ for key, value := range extra {
+ conds = append(conds, bson.M{key: value})
+ }
+ return bson.M{"$and": conds}
+}
+
+func mapKeys(m map[int64]struct{}) []int64 {
+ if len(m) == 0 {
+ return nil
+ }
+ keys := make([]int64, 0, len(m))
+ for id := range m {
+ keys = append(keys, id)
+ }
+ return keys
+}
diff --git a/web/service/srv_viporder/summary.go b/web/service/srv_viporder/summary.go
new file mode 100644
index 0000000..c46467e
--- /dev/null
+++ b/web/service/srv_viporder/summary.go
@@ -0,0 +1,165 @@
+package srv_viporder
+
+import (
+ "context"
+ "strings"
+ "time"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/vipordermod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+// RechargeSummaryReq 充值汇总请求,时间按北京时间解析。
+type RechargeSummaryReq struct {
+ StartTime string `form:"startTime" json:"startTime"` // 开始时间,北京时间:2006-01-02 / 2006-01-02 15:04:05 / RFC3339
+ EndTime string `form:"endTime" json:"endTime"` // 结束时间,北京时间:2006-01-02 / 2006-01-02 15:04:05 / RFC3339
+}
+
+// RechargeSummaryResp 充值汇总结果,金额单位为分。
+type RechargeSummaryResp struct {
+ StartTime string `json:"startTime"` // 实际统计开始时间(北京时间)
+ EndTime string `json:"endTime"` // 实际统计结束时间(北京时间)
+ OrderCount int64 `json:"orderCount"` // 成功订单数
+ TotalAmount int64 `json:"totalAmount"` // 成功订单金额合计(分)
+ VipOrderCount int64 `json:"vipOrderCount"` // 会员成功订单数
+ VipAmount int64 `json:"vipAmount"` // 会员成功订单金额(分)
+ CoinOrderCount int64 `json:"coinOrderCount"` // 金币成功订单数
+ CoinAmount int64 `json:"coinAmount"` // 金币成功订单金额(分)
+ UserCount int64 `json:"userCount"` // 成功充值用户数(去重)
+}
+
+type rechargeSummaryAgg struct {
+ OrderCount int64 `bson:"orderCount"`
+ TotalAmount int64 `bson:"totalAmount"`
+ VipOrderCount int64 `bson:"vipOrderCount"`
+ VipAmount int64 `bson:"vipAmount"`
+ CoinOrderCount int64 `bson:"coinOrderCount"`
+ CoinAmount int64 `bson:"coinAmount"`
+ UserCount int64 `bson:"userCount"`
+}
+
+// GetRechargeSummary 按支付完成时间汇总成功充值订单。
+func GetRechargeSummary(ctx context.Context, req *RechargeSummaryReq) (*RechargeSummaryResp, *srvdef.SrvErr) {
+ if req == nil {
+ return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空")
+ }
+ start, end, err := parseBeijingTimeRange(req.StartTime, req.EndTime)
+ if err != nil {
+ return nil, err
+ }
+ resp := &RechargeSummaryResp{
+ StartTime: start.In(utils.LocBJ).Format(pubdef.TimeISO),
+ EndTime: end.In(utils.LocBJ).Format(pubdef.TimeISO),
+ }
+ list := make([]rechargeSummaryAgg, 0)
+ if serr := vipordermod.AggregateVipOrder(ctx, &list, rechargeSummaryPipeline(start, end)); serr != nil {
+ return nil, serr
+ }
+ if len(list) == 0 {
+ return resp, nil
+ }
+ item := list[0]
+ resp.OrderCount = item.OrderCount
+ resp.TotalAmount = item.TotalAmount
+ resp.VipOrderCount = item.VipOrderCount
+ resp.VipAmount = item.VipAmount
+ resp.CoinOrderCount = item.CoinOrderCount
+ resp.CoinAmount = item.CoinAmount
+ resp.UserCount = item.UserCount
+ return resp, nil
+}
+
+func rechargeSummaryPipeline(start, end time.Time) []bson.M {
+ vipType := bson.M{"$eq": bson.A{bson.M{"$ifNull": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeVip}}, vipordermod.OrderTypeVip}}
+ return []bson.M{
+ {
+ "$match": bson.M{
+ vipordermod.FN.VipOrder.Status: pubdef.VipOrderSuccess,
+ vipordermod.FN.VipOrder.FinishedAt: bson.M{
+ "$gte": start,
+ "$lte": end,
+ },
+ },
+ },
+ {
+ "$group": bson.M{
+ "_id": nil,
+ "orderCount": bson.M{"$sum": 1},
+ "totalAmount": bson.M{"$sum": "$" + vipordermod.FN.VipOrder.Amount},
+ "vipOrderCount": bson.M{"$sum": bson.M{"$cond": bson.A{vipType, 1, 0}}},
+ "vipAmount": bson.M{"$sum": bson.M{"$cond": bson.A{
+ vipType,
+ "$" + vipordermod.FN.VipOrder.Amount,
+ 0,
+ }}},
+ "coinOrderCount": bson.M{"$sum": bson.M{"$cond": bson.A{
+ bson.M{"$eq": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeCoin}},
+ 1,
+ 0,
+ }}},
+ "coinAmount": bson.M{"$sum": bson.M{"$cond": bson.A{
+ bson.M{"$eq": bson.A{"$" + vipordermod.FN.VipOrder.OrderType, vipordermod.OrderTypeCoin}},
+ "$" + vipordermod.FN.VipOrder.Amount,
+ 0,
+ }}},
+ "userIds": bson.M{"$addToSet": "$" + vipordermod.FN.VipOrder.UserId},
+ },
+ },
+ {
+ "$project": bson.M{
+ "_id": 0,
+ "orderCount": 1,
+ "totalAmount": 1,
+ "vipOrderCount": 1,
+ "vipAmount": 1,
+ "coinOrderCount": 1,
+ "coinAmount": 1,
+ "userCount": bson.M{"$size": "$userIds"},
+ },
+ },
+ }
+}
+
+func parseBeijingTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) {
+ start, ok := parseBeijingTime(startRaw, true)
+ if !ok {
+ return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间无效")
+ }
+ end, ok := parseBeijingTime(endRaw, false)
+ if !ok {
+ return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间无效")
+ }
+ if end.Before(start) {
+ return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间不能早于开始时间")
+ }
+ return start, end, nil
+}
+
+func parseBeijingTime(raw string, isStart bool) (time.Time, bool) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return time.Time{}, false
+ }
+ layouts := []string{
+ pubdef.TimeISO,
+ time.RFC3339,
+ time.RFC3339Nano,
+ "2006-01-02 15:04:05 -0700 MST",
+ "2006-01-02 15:04:05.999999999 -0700 MST",
+ }
+ for _, layout := range layouts {
+ if t, err := time.ParseInLocation(layout, raw, utils.LocBJ); err == nil {
+ return t, true
+ }
+ }
+ if t, err := time.ParseInLocation(pubdef.DateISO, raw, utils.LocBJ); err == nil {
+ if isStart {
+ return utils.BeginOfTime(t), true
+ }
+ return utils.EndOfTime(t), true
+ }
+ return time.Time{}, false
+}
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/llmstxt/llmstxt.go b/web/skd/job/llmstxt/llmstxt.go
new file mode 100644
index 0000000..70ff3b0
--- /dev/null
+++ b/web/skd/job/llmstxt/llmstxt.go
@@ -0,0 +1,386 @@
+package llmstxt
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/pubdef"
+ "videoweb/web/service/srv_cfg"
+)
+
+const (
+ maxRecentToolsInBrief = 50 // llms.txt 中最新工具条数上限
+ maxRecentArticlesBrief = 50 // llms.txt 中最新文章条数上限
+ maxRecentInFull = 500 // llms-full.txt 中最新工具/文章条数上限
+)
+
+var generateMu sync.Mutex
+
+// GenerateLLMSTxt 生成 llms.txt(摘要)和 llms-full.txt(详细)
+func GenerateLLMSTxt() {
+ generateMu.Lock()
+ defer generateMu.Unlock()
+
+ ctx := context.Background()
+ config, serr := srv_cfg.GetSystemConfig(ctx)
+ if serr != nil || config == nil {
+ log.Error("GenerateLLMSTxt GetSystemConfig", log.Any("err", serr))
+ return
+ }
+
+ //baseURL := strings.TrimSuffix(config.WebAddr, "/")
+
+ //categories, subCategories, cateNameMap, subCateNameMap := loadCategories(ctx)
+ //recentTools := loadRecentTools(ctx, maxRecentToolsInBrief)
+ //recentArticles := loadRecentArticles(ctx, maxRecentArticlesBrief)
+ //
+ //var brief strings.Builder
+ //writeBrief(&brief, config.WebTitle, config.WebDescription, baseURL, config.WebKeyworld, recentTools, recentArticles)
+ //
+ //if err := writeTextFile("llms.txt", brief.String()); err != nil {
+ // log.Error("GenerateLLMSTxt write llms.txt", log.Any("err", err))
+ // return
+ //}
+ //if err := commitFile("llms.txt"); err != nil {
+ // log.Error("GenerateLLMSTxt commit llms.txt", log.Any("err", err))
+ // return
+ //}
+ //
+ //allTools := loadRecentTools(ctx, maxRecentInFull)
+ //allArticles := loadRecentArticles(ctx, maxRecentInFull)
+ //
+ //var full strings.Builder
+ //writeFull(&full, config.WebTitle, config.WebDescription, baseURL, config.WebKeyworld, categories, subCategories, cateNameMap, subCateNameMap, allTools, allArticles)
+ //
+ //if err := writeTextFile("llms-full.txt", full.String()); err != nil {
+ // log.Error("GenerateLLMSTxt write llms-full.txt", log.Any("err", err))
+ // return
+ //}
+ //if err := commitFile("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(categories)),
+ // log.Any("subCategories", len(subCategories)),
+ // log.Any("tools", len(allTools)),
+ // log.Any("articles", len(allArticles)),
+ //)
+}
+
+// ── 数据加载 ──────────────────────────────────────────────────
+//
+//func loadCategories(ctx context.Context) (
+// categories toolcategorymod.ToolCategoryModelList,
+// subCategories toolsubcategorymod.ToolSubCategoryModelList,
+// cateNameMap map[int64]string,
+// subCateNameMap map[int64]string,
+//) {
+// cateFilter := bson.M{
+// toolcategorymod.FN.ToolCategory.Status: 1,
+// toolcategorymod.FN.ToolCategory.Deleted: bson.M{"$ne": 1},
+// }
+// cateOpt := options.Find().SetSort(bson.D{
+// {Key: toolcategorymod.FN.ToolCategory.Sort, Value: 1},
+// {Key: toolcategorymod.FN.ToolCategory.Id, Value: 1},
+// })
+// categories, _ = toolcategorymod.FindMany(ctx, cateFilter, cateOpt)
+//
+// subFilter := bson.M{
+// toolsubcategorymod.FN.ToolSubCategory.Status: 1,
+// toolsubcategorymod.FN.ToolSubCategory.Deleted: bson.M{"$ne": 1},
+// }
+// subOpt := options.Find().SetSort(bson.D{
+// {Key: toolsubcategorymod.FN.ToolSubCategory.Sort, Value: 1},
+// {Key: toolsubcategorymod.FN.ToolSubCategory.Id, Value: 1},
+// })
+// subCategories, _ = toolsubcategorymod.FindMany(ctx, subFilter, subOpt)
+//
+// cateNameMap = make(map[int64]string, len(categories))
+// for _, c := range categories {
+// cateNameMap[c.Id] = c.Name
+// }
+// subCateNameMap = make(map[int64]string, len(subCategories))
+// for _, s := range subCategories {
+// subCateNameMap[s.Id] = s.Name
+// }
+// return categories, subCategories, cateNameMap, subCateNameMap
+//}
+//
+//func loadRecentTools(ctx context.Context, limit int64) toolmod.ToolModelList {
+// opt := options.Find().
+// SetSort(bson.D{
+// {Key: toolmod.FN.Tool.Sort, Value: 1},
+// {Key: toolmod.FN.Tool.Hot, Value: -1},
+// {Key: toolmod.FN.Tool.CreatedAt, Value: -1},
+// }).
+// SetLimit(limit)
+// filter := bson.M{
+// toolmod.FN.Tool.ReviewStatus: 1,
+// toolmod.FN.Tool.Slug: bson.M{"$nin": []interface{}{"", nil}},
+// }
+// list, _ := toolmod.FindMany(ctx, filter, opt)
+// return list
+//}
+//
+//func loadRecentArticles(ctx context.Context, limit int64) articlemod.ArticleModelList {
+// opt := options.Find().
+// SetSort(bson.D{{Key: articlemod.FN.Article.CreatedAt, Value: -1}}).
+// SetLimit(limit)
+// filter := bson.M{
+// articlemod.FN.Article.ReviewStatus: 1,
+// articlemod.FN.Article.Slug: bson.M{"$nin": []interface{}{"", nil}},
+// }
+// list, _ := articlemod.FindMany(ctx, filter, opt)
+// return list
+//}
+//
+//// ── 内容构建 ──────────────────────────────────────────────────
+//
+//func writeBrief(
+// b *strings.Builder,
+// title, desc, baseURL string,
+// keywords []string,
+// recentTools toolmod.ToolModelList,
+// recentArticles articlemod.ArticleModelList,
+//) {
+// writeHeader(b, title, desc, keywords)
+// writeImportantPages(b, baseURL)
+//
+// if len(recentTools) > 0 {
+// fmt.Fprintf(b, "## 最新 AI 工具\n\n")
+// for _, t := range recentTools {
+// if t == nil || t.Slug == "" {
+// continue
+// }
+// summary := t.Intro
+// if summary == "" {
+// summary = t.Desc
+// }
+// if summary == "" {
+// summary = t.Title
+// }
+// fmt.Fprintf(b, "- [%s](%s): %s\n", t.Title, toolDetailURL(baseURL, t.Slug), truncate(summary, 100))
+// }
+// b.WriteString("\n")
+// }
+//
+// if len(recentArticles) > 0 {
+// fmt.Fprintf(b, "## 最新 AI 资讯\n\n")
+// for _, a := range recentArticles {
+// if a == nil || a.Slug == "" {
+// continue
+// }
+// summary := a.Intro
+// if summary == "" {
+// summary = a.Title
+// }
+// fmt.Fprintf(b, "- [%s](%s): %s\n", a.Title, articleDetailURL(baseURL, a.Slug), truncate(summary, 100))
+// }
+// b.WriteString("\n")
+// }
+//}
+//
+//func GetCategorySlug(id int64, categories toolcategorymod.ToolCategoryModelList) string {
+// if id <= 0 {
+// return ""
+// }
+// if len(categories) == 0 {
+// return ""
+// }
+// for _, category := range categories {
+// if category.Id == id {
+// return category.Slug
+// }
+// }
+// return ""
+//}
+//func GetSubCategorySlug(id int64, categories toolsubcategorymod.ToolSubCategoryModelList) string {
+// if id <= 0 {
+// return ""
+// }
+// if len(categories) == 0 {
+// return ""
+// }
+// for _, category := range categories {
+// if category.Id == id {
+// return category.Slug
+// }
+// }
+// return ""
+//}
+//func writeFull(
+// b *strings.Builder,
+// title, desc, baseURL string,
+// keywords []string,
+// categories toolcategorymod.ToolCategoryModelList,
+// subCategories toolsubcategorymod.ToolSubCategoryModelList,
+// cateNameMap, subCateNameMap map[int64]string,
+// tools toolmod.ToolModelList,
+// articles articlemod.ArticleModelList,
+//) {
+// writeHeader(b, title, desc, keywords)
+// writeImportantPages(b, baseURL)
+// writeCategorySections(b, baseURL, categories, subCategories, true)
+//
+// if len(tools) > 0 {
+// fmt.Fprintf(b, "## AI 工具详情(最新 %d 条)\n\n", len(tools))
+// for _, t := range tools {
+// if t == nil || t.Slug == "" {
+// continue
+// }
+// fmt.Fprintf(b, "### [%s](%s)\n\n", t.Title, toolDetailURL(baseURL, t.Slug))
+//
+// summary := t.Desc
+// if summary == "" {
+// summary = t.Intro
+// }
+// if summary != "" {
+// fmt.Fprintf(b, "%s\n\n", truncate(summary, 500))
+// }
+// if subName := subCateNameMap[t.SubCateId]; subName != "" {
+// fmt.Fprintf(b, "- 子分类:[%s](%s)\n", subName, subCategoryListURL(baseURL, GetCategorySlug(t.CategoryId, categories), GetSubCategorySlug(t.SubCateId, subCategories)))
+// }
+// if t.OfficialUrl != "" {
+// fmt.Fprintf(b, "- 官网:%s\n", t.OfficialUrl)
+// }
+// if len(t.SeoInfo.Keyworld) > 0 {
+// fmt.Fprintf(b, "- 关键词:%s\n", strings.Join(t.SeoInfo.Keyworld, "、"))
+// }
+// if !t.CreatedAt.IsZero() {
+// fmt.Fprintf(b, "- 发布时间:%s\n", t.CreatedAt.Format("2006-01-02"))
+// }
+// b.WriteString("\n")
+// }
+// }
+//
+// if len(articles) > 0 {
+// fmt.Fprintf(b, "## AI 资讯详情(最新 %d 条)\n\n", len(articles))
+// for _, a := range articles {
+// if a == nil || a.Slug == "" {
+// continue
+// }
+// fmt.Fprintf(b, "### [%s](%s)\n\n", a.Title, articleDetailURL(baseURL, a.Slug))
+//
+// if a.Intro != "" {
+// fmt.Fprintf(b, "%s\n\n", truncate(a.Intro, 500))
+// }
+// if a.Author != "" {
+// fmt.Fprintf(b, "- 作者:%s\n", a.Author)
+// }
+// if len(a.SeoInfo.Keyworld) > 0 {
+// fmt.Fprintf(b, "- 关键词:%s\n", strings.Join(a.SeoInfo.Keyworld, "、"))
+// }
+// if !a.CreatedAt.IsZero() {
+// fmt.Fprintf(b, "- 发布时间:%s\n", a.CreatedAt.Format("2006-01-02"))
+// }
+// b.WriteString("\n")
+// }
+// }
+//}
+//
+//func writeHeader(b *strings.Builder, title, desc string, keywords []string) {
+// 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)
+//}
+//
+//func writeImportantPages(b *strings.Builder, baseURL string) {
+// fmt.Fprintf(b, "## 重要页面\n\n")
+// fmt.Fprintf(b, "- [首页](%s/): AI 工具导航主页\n", baseURL)
+// fmt.Fprintf(b, "- [工具列表](%s/tool/list): 全部 AI 工具\n", baseURL)
+// fmt.Fprintf(b, "- [热门推荐](%s/tool/hot): 热门 AI 工具\n", baseURL)
+// fmt.Fprintf(b, "- [AI 资讯](%s/article/list): AI 行业资讯文章\n", baseURL)
+// fmt.Fprintf(b, "- [搜索](%s/search): 搜索工具与资讯\n", baseURL)
+// fmt.Fprintf(b, "- [关于我们](%s/about/): 站点介绍\n", baseURL)
+// fmt.Fprintf(b, "- [Sitemap](%s/sitemap.xml): 网站地图\n\n", baseURL)
+//}
+//
+//func writeCategorySections(
+// b *strings.Builder,
+// baseURL string,
+// categories toolcategorymod.ToolCategoryModelList,
+// subCategories toolsubcategorymod.ToolSubCategoryModelList,
+// withSeo bool,
+//) {
+//
+// if len(subCategories) > 0 {
+// fmt.Fprintf(b, "## 工具子分类\n\n")
+// for _, s := range subCategories {
+// if s == nil {
+// continue
+// }
+// subDesc := subCategoryDesc(s, withSeo)
+// fmt.Fprintf(b, "- [%s](%s): %s\n", s.Name, subCategoryListURL(baseURL, GetCategorySlug(s.CategoryId, categories), GetSubCategorySlug(s.Id, subCategories)), subDesc)
+// }
+// b.WriteString("\n")
+// }
+//}
+//
+//func subCategoryDesc(s *toolsubcategorymod.ToolSubCategoryModel, withSeo bool) string {
+// if withSeo && s.SeoInfo.Description != "" {
+// return s.SeoInfo.Description
+// }
+// return s.Name + "相关 AI 工具"
+//}
+
+// ── URL 构建 ──────────────────────────────────────────────────
+
+func toolDetailURL(baseURL, slug string) string {
+ return utils.JoinUrlPath(baseURL, fmt.Sprintf("/tool/details/%s", slug))
+}
+
+func articleDetailURL(baseURL, slug string) string {
+ return utils.JoinUrlPath(baseURL, fmt.Sprintf("/article/details/%s", slug))
+}
+
+func subCategoryListURL(baseURL string, categorySlug, subCateSlug string) string {
+ return utils.JoinUrlPath(baseURL, fmt.Sprintf("/classification/%s/%s", categorySlug, subCateSlug))
+}
+
+// ── 工具函数 ──────────────────────────────────────────────────
+
+func truncate(s string, maxLen int) string {
+ runes := []rune(s)
+ if len(runes) <= maxLen {
+ return s
+ }
+ return string(runes[:maxLen]) + "…"
+}
+
+func writeTextFile(filename, content string) error {
+ if err := os.MkdirAll(pubdef.OutputDir, 0755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", pubdef.OutputDir, err)
+ }
+ tmpPath := filepath.Join(pubdef.OutputDir, 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(filename string) error {
+ src := filepath.Join(pubdef.OutputDir, filename+".tmp")
+ dst := filepath.Join(pubdef.OutputDir, 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/web/skd/job/reportdata/event.go b/web/skd/job/reportdata/event.go
new file mode 100644
index 0000000..2ec6201
--- /dev/null
+++ b/web/skd/job/reportdata/event.go
@@ -0,0 +1,124 @@
+package job
+
+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/rss/rss.go b/web/skd/job/rss/rss.go
new file mode 100644
index 0000000..6a43622
--- /dev/null
+++ b/web/skd/job/rss/rss.go
@@ -0,0 +1,310 @@
+package rss
+
+import (
+ "context"
+ "encoding/xml"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/netutil"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/pubdef"
+ "videoweb/web/service/srv_cfg"
+)
+
+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"`
+}
+
+// rssEntry 内部排序用,合并文章与工具后统一输出
+type rssEntry struct {
+ Id int64
+ Title string
+ Link string
+ Description string
+ PubAt time.Time
+ Cover string
+}
+
+func GenerateRSS() {
+ generateMu.Lock()
+ defer generateMu.Unlock()
+
+ ctx := context.Background()
+ config, serr := srv_cfg.GetSystemConfig(ctx)
+ if serr != nil {
+ log.Error("GenerateRSS GetSystemConfig", log.Any("err", serr))
+ return
+ }
+ if config == nil {
+ log.Error("GenerateRSS GetSystemConfig nil config")
+ return
+ }
+ //
+ //entries := loadRSSEntries(ctx, config.WebAddr, maxRSSItems)
+ //items := buildRSSItems(config, entries)
+ //feedURL := utils.JoinUrlPath(config.WebAddr, "/rss.xml")
+ //feed := rssFeed{
+ // Version: "2.0",
+ // Atom: "http://www.w3.org/2005/Atom",
+ // Channel: rssChannel{
+ // Title: config.WebTitle,
+ // Link: config.WebAddr,
+ // Description: channelDescription(config),
+ // 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(filename, feed); err != nil {
+ // log.Error("GenerateRSS write", log.Any("err", err))
+ // return
+ //}
+ //
+ //src := filepath.Join(pubdef.OutputDir, filename+".tmp")
+ //dst := filepath.Join(pubdef.OutputDir, 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)))
+}
+
+func channelDescription(config *cfg.SysCfg) string {
+ if config.WebDescription != "" {
+ return config.WebDescription
+ }
+ return "AI 工具导航与资讯更新"
+}
+
+//
+//func loadRSSEntries(ctx context.Context, baseURL string, limit int) []rssEntry {
+// baseURL = strings.TrimSuffix(baseURL, "/")
+// articleFilter := bson.M{
+// articlemod.FN.Article.ReviewStatus: 1,
+// articlemod.FN.Article.Slug: bson.M{"$nin": []interface{}{"", nil}},
+// }
+// articleOpt := options.Find().
+// SetSort(bson.D{{Key: articlemod.FN.Article.CreatedAt, Value: -1}}).
+// SetLimit(int64(limit))
+// articles, _ := articlemod.FindMany(ctx, articleFilter, articleOpt)
+//
+// toolFilter := bson.M{
+// toolmod.FN.Tool.ReviewStatus: 1,
+// toolmod.FN.Tool.Slug: bson.M{"$nin": []interface{}{"", nil}},
+// }
+// toolOpt := options.Find().
+// SetSort(bson.D{{Key: toolmod.FN.Tool.CreatedAt, Value: -1}}).
+// SetLimit(int64(limit))
+// tools, _ := toolmod.FindMany(ctx, toolFilter, toolOpt)
+//
+// entries := make([]rssEntry, 0, len(articles)+len(tools))
+// for _, a := range articles {
+// if a == nil || a.Slug == "" {
+// continue
+// }
+// desc := a.Intro
+// if desc == "" {
+// desc = a.Title
+// }
+// entries = append(entries, rssEntry{
+// Id: a.Id,
+// Title: a.Title,
+// Link: articleDetailURL(baseURL, a.Slug),
+// Description: desc,
+// PubAt: a.CreatedAt,
+// Cover: a.Cover,
+// })
+// }
+// for _, t := range tools {
+// if t == nil || t.Slug == "" {
+// continue
+// }
+// desc := t.Intro
+// if desc == "" {
+// desc = t.Desc
+// }
+// if desc == "" {
+// desc = t.Title
+// }
+// cover := t.Logo
+// if cover == "" {
+// cover = t.Snapshot
+// }
+// entries = append(entries, rssEntry{
+// Id: t.Id,
+// Title: t.Title,
+// Link: toolDetailURL(baseURL, t.Slug),
+// Description: desc,
+// PubAt: t.CreatedAt,
+// Cover: cover,
+// })
+// }
+//
+// sort.Slice(entries, func(i, j int) bool {
+// return entries[i].PubAt.After(entries[j].PubAt)
+// })
+// if len(entries) > limit {
+// entries = entries[:limit]
+// }
+// return entries
+//}
+//
+//func buildRSSItems(config *cfg.SysCfg, entries []rssEntry) []rssItem {
+// logoURL := utils.JoinUrlPath(strings.TrimSuffix(config.WebAddr, "/"), "/static/images/logo.png")
+// imgCdn := imageCdn(config)
+//
+// items := make([]rssItem, 0, len(entries))
+// for _, e := range entries {
+// if e.Link == "" {
+// continue
+// }
+//
+// pubDate := ""
+// if !e.PubAt.IsZero() {
+// pubDate = e.PubAt.Format(time.RFC1123Z)
+// }
+//
+// item := rssItem{
+// Title: e.Title,
+// Link: e.Link,
+// Description: e.Description,
+// PubDate: pubDate,
+// GUID: e.Link,
+// }
+//
+// encURL, encType := resolveEnclosureURL(config, imgCdn, logoURL, e.Cover, e.Id)
+// item.Enclosure = &rssEnclosure{
+// URL: encURL,
+// Type: encType,
+// Length: "0",
+// }
+// items = append(items, item)
+// }
+// return items
+//}
+
+func imageCdn(config *cfg.SysCfg) string {
+ if config.ImgWebCdn != "" {
+ return config.ImgWebCdn
+ }
+ return config.UnencryptedCdn
+}
+
+func resolveEnclosureURL(config *cfg.SysCfg, imgCdn, logoURL, cover string, id int64) (url, mime string) {
+ mime = "image/jpeg"
+ if cover != "" && imgCdn != "" {
+ url = netutil.MergeURL(imgCdn, cover)
+ } else if len(config.SitemapDefaultImages) > 0 && imgCdn != "" {
+ img := config.SitemapDefaultImages[absModIndex(id, len(config.SitemapDefaultImages))]
+ url = netutil.MergeURL(imgCdn, img)
+ } else {
+ url = logoURL
+ mime = "image/png"
+ }
+ if strings.HasSuffix(strings.ToLower(url), ".png") {
+ mime = "image/png"
+ } else if strings.HasSuffix(strings.ToLower(url), ".webp") {
+ mime = "image/webp"
+ }
+ return url, mime
+}
+
+func toolDetailURL(baseURL, slug string) string {
+ return utils.JoinUrlPath(baseURL, fmt.Sprintf("/tool/details/%s", slug))
+}
+
+func articleDetailURL(baseURL, slug string) string {
+ return utils.JoinUrlPath(baseURL, fmt.Sprintf("/article/details/%s", slug))
+}
+
+func absModIndex(id int64, length int) int {
+ if length <= 0 {
+ return 0
+ }
+ idx := id % int64(length)
+ if idx < 0 {
+ idx = -idx
+ }
+ return int(idx)
+}
+
+func writeRSSFile(filename string, data interface{}) error {
+ if err := os.MkdirAll(pubdef.OutputDir, 0755); err != nil {
+ return fmt.Errorf("mkdir %s: %w", pubdef.OutputDir, err)
+ }
+ path := filepath.Join(pubdef.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 %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/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/job/vipexpiry/job.go b/web/skd/job/vipexpiry/job.go
new file mode 100644
index 0000000..6b6f10e
--- /dev/null
+++ b/web/skd/job/vipexpiry/job.go
@@ -0,0 +1,45 @@
+package vipexpiry
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/usermod"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+var isRunning = false
+
+// ResetExpiredVip 将 CurrentVipExpire 已过期但 VipType 仍非 None 的用户批量重置为普通用户。
+// 同时清空 ShowCard* 展示字段,保证 DB 数据与实际状态一致。
+func ResetExpiredVip() {
+ if isRunning {
+ return
+ }
+ isRunning = true
+ defer func() { isRunning = false }()
+
+ ctx := context.Background()
+ now := time.Now()
+
+ filter := bson.M{
+ usermod.FN.User.VipType: bson.M{"$gt": pubdef.VipTypeNone},
+ usermod.FN.User.CurrentVipExpire: bson.M{"$lt": now},
+ }
+ update := bson.M{
+ "$set": bson.M{
+ usermod.FN.User.VipType: pubdef.VipTypeNone,
+ usermod.FN.User.ShowCardId: int64(0),
+ usermod.FN.User.ShowCardName: "",
+ usermod.FN.User.UpdatedAt: now,
+ },
+ }
+
+ if serr := usermod.UpdateManyUser(ctx, filter, update); serr != nil {
+ log.Error("vipexpiry:ResetExpiredVip UpdateManyUser failed", log.Any("err", serr))
+ return
+ }
+ log.Info("vipexpiry:ResetExpiredVip done")
+}
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..8103659
--- /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
+ }
+
+ 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,
+ 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,
+ 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,
+ 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.PlayName)
+ if playletName == "" {
+ 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.PlayName: 1,
+ 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..46dd1e5
--- /dev/null
+++ b/web/skd/skd.go
@@ -0,0 +1,69 @@
+package skd
+
+import (
+ "math/rand"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/web/service/srv_mediaresource"
+ job "videoweb/web/skd/job/reportdata"
+ "videoweb/web/skd/job/vipexpiry"
+ "videoweb/web/skd/playletwatermark"
+
+ "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())
+
+ // 短剧水印:每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("0/1 * * * * ?", func() { // 每秒执行一次批量上报
+ job.Report()
+ })
+
+ // 每天凌晨 02:00 重置 VIP 已过期用户的 VipType
+ _, _ = skdCron.AddFunc("0 0 2 * * ?", func() {
+ vipexpiry.ResetExpiredVip()
+ })
+
+ //if webg.Conf.LsjMediaConf.ApiUrl != "" {
+ // _, _ = skdCron.AddFunc("0 30 */2 * * ?", func() {
+ // transcodejob.TranscodeVideoByWatchTimes()
+ // })
+ // _, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
+ // transcodejob.CheckTranscodingResults()
+ // })
+ //}
+
+ skdCron.Start()
+}
+
+func Stop() {
+ log.Info("srv_datinggirl:updater Stop...")
+ defer log.Info("srv_datinggirl: updater Stop!!!")
+ skdCron.Stop()
+}
diff --git a/web/skd/videosync/info.go b/web/skd/videosync/info.go
new file mode 100644
index 0000000..6ea8c64
--- /dev/null
+++ b/web/skd/videosync/info.go
@@ -0,0 +1,77 @@
+package videosync
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+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"` // 文件名称
+ TempFieldNameFs string `bson:"tempFieldNameFs,omitempty" json:"tempFieldNameFs"` // 临时存储
+ Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀
+ UnencryptedPreMp4 string `bson:"unencryptedPreMp4,omitempty" json:"unencryptedPreMp4"` // 不加密的预览MP4
+ 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
+ UploadUserName string `bson:"uploadUserName" json:"uploadUserName,omitempty"` // 上传用户名
+ OptionAccount string `bson:"optionAccount,omitempty" json:"optionAccount"` // 最后操作人
+}
diff --git a/web/skd/videosync/videosync.go b/web/skd/videosync/videosync.go
new file mode 100644
index 0000000..819b9ef
--- /dev/null
+++ b/web/skd/videosync/videosync.go
@@ -0,0 +1,139 @@
+package videosync
+
+import (
+ "context"
+ "errors"
+ 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"
+)
+
+/*
+1.小说采集后上传媒资库
+2.业务后台记录最新更新章节ID(数据ID自增)
+3.查询对应小说ID是否存在,如果不存在需要先同步媒资库小说信息,再更新小说
+注意:业务后台和媒资库 存储的小说ID和章节ID需一致, 资源以媒资库为准, 不单独更新业务后台小说
+*/
+
+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 {
+ log.Error("VideoSync", 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 media.UpdatedAt.After(recordInfo.RecordTime) {
+ recordInfo.RecordTime = media.UpdatedAt
+ }
+ se := srv_videoinfo.SyncOneVideo(ctx, media)
+ if se != nil {
+ log.Error("VideoSync SyncOneVideo", log.Any("media", media), log.E(se))
+ continue
+ }
+ }
+
+ 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))
+}
+
+// SyncUnencryptedFields 全量补齐历史资源的 UnencryptedImage / UnencryptedPreMp4 字段。
+// 从头分页拉取媒资库,仅在源端有值时写回,本地不存在或字段已有值不覆盖。
+// 独立于增量同步的 SyncRecord,用于一次性修复;可安全重复执行。
+func SyncUnencryptedFields() {
+ ctx := context.Background()
+ log.Info("SyncUnencryptedFields begin")
+ pageSize := int64(200)
+ lastUpdateTime := time.Time{}
+ for {
+ req := proxy.MediaTaskSyncReq{
+ AppId: pubdef.AppId,
+ LastUpdateTime: lastUpdateTime,
+ PageSize: pageSize,
+ }
+ result, err := proxy.SyncMedia(&req, "")
+ if err != nil || result.Code != nethttp.StatusOK {
+ log.Error("SyncUnencryptedFields SyncMedia", log.Any("req", req), log.E(err))
+ return
+ }
+ for _, media := range result.Data {
+ // 先推进时间戳,保证 continue 也不阻塞分页
+ if media.UpdatedAt.After(lastUpdateTime) {
+ lastUpdateTime = media.UpdatedAt
+ }
+ se := srv_videoinfo.SyncUnencryptedOne(ctx, media)
+ if se != nil && !errors.Is(se.Code, srvdef.ErrNoData) {
+ log.Warn("SyncUnencryptedFields SyncUnencryptedOne", log.Any("mediaId", media.Id.Hex()), log.E(se))
+ }
+ }
+ if len(result.Data) < int(pageSize) {
+ break
+ }
+ }
+ log.Info("SyncUnencryptedFields success", log.Any("lastUpdateTime", lastUpdateTime))
+}
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..6ad207f
--- /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"`
+ 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"`
+ PlayletMediaResourceDBCfg struct { //嘉华
+ BaseUrl string `yaml:"baseUrl"` // 域名
+ Merchat string `yaml:"merchat"` // 商户号
+ Secret string `yaml:"secret"` // 商户密钥
+ } `yaml:"playletMediaResourceDBCfg"`
+ NewInnerStatUrl string `yaml:"newInnerStatUrl"`
+ DataReport struct {
+ Url string `yaml:"url"`
+ } `yaml:"dataReport"`
+}
+
+func IsDev() bool {
+ return Conf.RunMod == "dev" || Conf.RunMod == "debug"
+}