Initial commit: web_hghl_server
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
tool
|
||||
.DS_Store
|
||||
.project
|
||||
.idea
|
||||
.vscode
|
||||
/temp
|
||||
/run
|
||||
*.tar.gz
|
||||
*.tar.xz
|
||||
# Google 服务账号私钥(Indexing API),只能随部署单独投放,禁止入库。
|
||||
composed-anvil-*.json
|
||||
@@ -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
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
GOPATH:=$(shell go env GOPATH)
|
||||
|
||||
GIT_BRANCH:=$(shell git symbolic-ref --short -q HEAD)
|
||||
GIT_VERSION:=$(shell git log --pretty=oneline -p -1 | head -n 1 | awk '{print $$1}')
|
||||
BUILD_DATE:=$(shell date '+%Y-%m-%d %H:%M:%S %z')
|
||||
BUILD_IP:=$(shell ifconfig | grep inet | grep -v grep | grep -v "inet6" | grep -v "127.0.0.1" | awk '{print $$2}')
|
||||
VERSON_PATH:=pfshop/pub/srvversion
|
||||
|
||||
APP_BUILD_FLAG:=-ldflags "-X '$(VERSON_PATH).GIT_BRANCH=$(GIT_BRANCH)' -X '$(VERSON_PATH).GIT_VERSION=$(GIT_VERSION)' -X '$(VERSON_PATH).BUILD_DATE=$(BUILD_DATE)' -X '$(VERSON_PATH).BUILD_IP=$(BUILD_IP)'"
|
||||
|
||||
|
||||
.PHONY: test deploy
|
||||
deploy:
|
||||
@echo "deploy, GOPATH=$(GOPATH)"
|
||||
go clean
|
||||
rm -f web_mdcom_app
|
||||
env GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_mdcom_app -v
|
||||
tar czf web_mdcom_app.tar.gz web_mdcom_app
|
||||
rm -f web_mdcom_app
|
||||
|
||||
test:
|
||||
@echo "test, GOPATH=$(GOPATH)"
|
||||
GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_mdcom_app
|
||||
tar czf web_mdcom_app.tar.gz web_mdcom_app docs/
|
||||
rm -f web_mdcom_app
|
||||
localtest:
|
||||
@echo "test, GOPATH=$(GOPATH)"
|
||||
go build $(APP_BUILD_FLAG) -o app
|
||||
@@ -0,0 +1,57 @@
|
||||
package actressapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/actressser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// QueryActressWorkList 指定女优详情和作品混排列表
|
||||
// @Tags 女优相关接口
|
||||
// @Summary 指定女优详情和作品列表 -- by 王鸣
|
||||
// @Description 查询指定女优详情和作品列表,支持上架时间/观看量/收藏量升降序,每间隔8条作品数据插入一个广告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryActressWorkListReq true "指定女优作品列表请求"
|
||||
// @Success 200 {object} proto.QueryActressWorkListResp{list=[]proto.QueryVideoListItem{VideoInfo=proto.VideoInfo}} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/actress/work/list [get]
|
||||
func QueryActressWorkList(c *gin.Context) {
|
||||
var req proto.QueryActressWorkListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := actressser.QueryActressWorkList(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// ActressList 女优榜单列表
|
||||
// @Tags 女优相关接口
|
||||
// @Summary 女优榜单列表 -- by 王鸣
|
||||
// @Description 返回导航、广告和按热度值降序排列的女优列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.QueryActressListResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/actress/list [get]
|
||||
func ActressList(c *gin.Context) {
|
||||
data, err := actressser.ActressListData(c, c.Request.URL.Path)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
common.ServerJSON(c, srvdef.ErrDbQueryError, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package appinfoapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/appser"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// QueryAppList 查询应用列表
|
||||
// @Tags 应用列表
|
||||
// @Summary 查询应用列表
|
||||
// @Description 返回应用分类列表,每个分类下含应用信息列表;categoryId=0 返回第一个分类
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param categoryId query int false "当前分类ID,0表示默认第一个分类"
|
||||
// @Success 200 {object} proto.AppListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/applist/list [get]
|
||||
func QueryAppList(c *gin.Context) {
|
||||
var req struct {
|
||||
CategoryId int64 `form:"categoryId"`
|
||||
}
|
||||
if err := c.ShouldBindQuery(&req); err != nil || req.CategoryId < 0 {
|
||||
req.CategoryId = 0
|
||||
}
|
||||
|
||||
categoryList, category, serr := appser.AppCategoryData(c, req.CategoryId)
|
||||
if serr != nil {
|
||||
common.ServerJSON(c, serr, "")
|
||||
return
|
||||
}
|
||||
|
||||
if categoryList == nil {
|
||||
categoryList = make([]proto.AppCategoryInfo, 0)
|
||||
}
|
||||
|
||||
resp := proto.AppListResp{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
CategoryList: categoryList,
|
||||
Category: category,
|
||||
}
|
||||
configser.RewriteImageCdn(c, &resp)
|
||||
common.ServerJSON(c, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package authapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/wordsfilterser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// extractUA 从 gin context 中提取 UA 中间件解析到的设备/系统信息
|
||||
func extractUA(ctx *gin.Context) authser.UAInfo {
|
||||
return authser.UAInfo{
|
||||
Device: ctx.GetString(consts.CtxDevType),
|
||||
SysType: pubdef.SysType(ctx.GetString(consts.CtxSysType)),
|
||||
AppVer: ctx.GetString(consts.CtxVersion),
|
||||
IP: ctx.ClientIP(),
|
||||
}
|
||||
}
|
||||
|
||||
type guestLoginReq struct {
|
||||
DevID string `json:"devID"` // 设备 ID(首选)
|
||||
Mac string `json:"mac"` // 设备 MAC 地址(备用)
|
||||
DeviceId string `json:"deviceId"` // 设备 ID 第二版本(备用)
|
||||
}
|
||||
|
||||
type registerReq struct {
|
||||
AccountNo string `json:"accountNo" binding:"required"` // 账号
|
||||
Password string `json:"password" binding:"required"` // 明文密码
|
||||
AgentInviteCode string `json:"agentInviteCode"` // 邀请人的邀请码(可选)
|
||||
}
|
||||
|
||||
type loginReq struct {
|
||||
Account string `json:"account" binding:"required"` // 账号
|
||||
Password string `json:"password" binding:"required"` // 明文密码
|
||||
}
|
||||
|
||||
type updateProfileReq struct {
|
||||
AccountNo string `json:"accountNo" binding:"required"` // 账号(必填,用于定位用户)
|
||||
Avatar *string `json:"avatar"` // 新头像 URL(可选)
|
||||
NikeName *string `json:"nikeName"` // 昵称
|
||||
OldPassword *string `json:"oldPassword"` // 旧密码明文(修改密码时必填)
|
||||
NewPassword *string `json:"newPassword"` // 新密码明文(修改密码时必填)
|
||||
}
|
||||
|
||||
// GuestLogin 游客登录/自动注册
|
||||
// @Tags 用户认证
|
||||
// @Summary 游客登录
|
||||
// @Description 按设备 ID 查找已有游客账号,找不到则自动注册。若请求携带有效 token 则优先复用该用户。
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body guestLoginReq true "设备信息"
|
||||
// @Success 200 {object} authser.LoginResp "登录成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/login/guest [POST]
|
||||
func GuestLogin(ctx *gin.Context) {
|
||||
var req guestLoginReq
|
||||
_ = ctx.ShouldBind(&req)
|
||||
|
||||
// 若设备信息为空,尝试从 UA 中间件补充 DevID
|
||||
if req.DevID == "" {
|
||||
req.DevID = ctx.GetString(consts.CtxDevId)
|
||||
}
|
||||
|
||||
currentUserId := common.GetUserId(ctx)
|
||||
resp, serr := authser.GuestLogin(ctx, &authser.GuestLoginReq{
|
||||
DevID: req.DevID,
|
||||
Mac: req.Mac,
|
||||
DeviceId: req.DeviceId,
|
||||
UA: extractUA(ctx),
|
||||
}, currentUserId)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Register 账号注册(游客升级 or 直接注册)
|
||||
// @Tags 用户认证
|
||||
// @Summary 注册
|
||||
// @Description 已登录游客传入账号密码完成升级;未登录用户直接注册新账号。
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body registerReq true "注册参数"
|
||||
// @Success 200 {object} authser.LoginResp "注册成功,返回 token 及用户基本信息"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/register [POST]
|
||||
func Register(ctx *gin.Context) {
|
||||
var req registerReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
currentUserId := common.GetUserId(ctx)
|
||||
resp, serr := authser.Register(ctx, req.AccountNo, req.Password, req.AgentInviteCode, currentUserId, extractUA(ctx))
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
// @Tags 用户认证
|
||||
// @Summary 登录
|
||||
// @Description 账号均可作为登录凭证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body loginReq true "登录参数"
|
||||
// @Success 200 {object} authser.LoginResp "登录成功,返回 token 及用户基本信息"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/login [POST]
|
||||
func Login(ctx *gin.Context) {
|
||||
var req loginReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, serr := authser.Login(ctx, req.Account, req.Password, extractUA(ctx))
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// GetInfo 获取当前登录用户信息
|
||||
// @Tags 用户认证
|
||||
// @Summary 获取当前登录用户信息
|
||||
// @Description 根据当前登录 token 查询用户基础信息
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} authser.UserInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/getInfo [get]
|
||||
func GetInfo(ctx *gin.Context) {
|
||||
userId := common.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, nil)
|
||||
return
|
||||
}
|
||||
resp, serr := authser.GetInfo(ctx, userId)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改用户资料
|
||||
// @Tags 用户认证
|
||||
// @Summary 修改用户资料(头像 / 密码,可任意组合)
|
||||
// @Description 账号必填;avatar、oldPassword+newPassword 均为可选,至少提供一项。修改密码时旧密码和新密码必须同时提供。
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body updateProfileReq true "修改资料参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/profile [POST]
|
||||
func UpdateProfile(ctx *gin.Context) {
|
||||
var req updateProfileReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
if req.NikeName != nil && !wordsfilterser.Validate(*req.NikeName) {
|
||||
common.ServerJSON(ctx, srvdef.ErrContainIllegalWord, "包含非法字符")
|
||||
return
|
||||
}
|
||||
serr := authser.UpdateProfile(ctx, &authser.UpdateProfileReq{
|
||||
AccountNo: req.AccountNo,
|
||||
Avatar: req.Avatar,
|
||||
NikeName: req.NikeName,
|
||||
OldPassword: req.OldPassword,
|
||||
NewPassword: req.NewPassword,
|
||||
})
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, "")
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package commentapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/commentser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List 评论列表
|
||||
// @Tags 评论APP
|
||||
// @Summary 评论列表 -- by 王鸣
|
||||
// @Description 按对象查询树形评论列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.CommentListReq true "评论列表请求"
|
||||
// @Success 200 {object} proto.CommentListResp{list=[]proto.CommentInfo} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/comment/list [get]
|
||||
func List(ctx *gin.Context) {
|
||||
var req proto.CommentListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := commentser.ListCommentTree(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SendComment 发布评论
|
||||
// @Tags 评论APP
|
||||
// @Summary 发布评论 -- by 王鸣
|
||||
// @Description 发布评论
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.CommentSendReq true "参数列表"
|
||||
// @Success 200 {object} string "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/comment/send [post]
|
||||
func SendComment(ctx *gin.Context) {
|
||||
var in proto.CommentSendReq
|
||||
if err := ctx.ShouldBindJSON(&in); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userId, exists := ctx.Get(consts.CtxUserID)
|
||||
if !exists {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, nil)
|
||||
return
|
||||
}
|
||||
userIdInt64, ok := userId.(int64)
|
||||
if !ok || userIdInt64 <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ip := ctx.ClientIP()
|
||||
ua, _ := common.GetUA(ctx)
|
||||
err := commentser.SendComment(ctx, ip, userIdInt64, &in, ua)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, "success")
|
||||
}
|
||||
|
||||
// SetLike 设置评论点赞状态
|
||||
// @Tags 评论APP
|
||||
// @Summary 评论点赞/取消点赞 -- by 王鸣
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.CommentLikeSetReq true "点赞参数"
|
||||
// @Success 200 {object} proto.CommentLikeSetResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/comment/like/set [post]
|
||||
func SetLike(ctx *gin.Context) {
|
||||
var req proto.CommentLikeSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, nil)
|
||||
return
|
||||
}
|
||||
resp, err := commentser.SetCommentLike(ctx, userId, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FooterInfo 查询APP底部信息
|
||||
// @Tags app配置相关接口
|
||||
// @Summary 查询APP底部信息 -- by 王鸣
|
||||
// @Description 返回APP底部品牌、底部导航和联系方式
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.FooterInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/config/footer/info [get]
|
||||
func FooterInfo(ctx *gin.Context) {
|
||||
resp := &proto.FooterInfoResp{
|
||||
FooterInfo: configser.GetFooterInfo(),
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// RichTextList 查询APP富文本内容列表
|
||||
// @Tags app配置相关接口
|
||||
// @Summary 查询APP富文本内容列表 -- by 王鸣
|
||||
// @Description 返回启用状态的富文本内容,支持按ids或titles过滤
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param ids query string false "富文本ID,多个用英文逗号分隔"
|
||||
// @Param titles query string false "富文本标题,多个用英文逗号分隔"
|
||||
// @Success 200 {object} proto.AppRichTextListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/config/richtext/list [get]
|
||||
func RichTextList(ctx *gin.Context) {
|
||||
resp := configser.GetAppRichTextList(ctx.Query("ids"), ctx.Query("titles"))
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ContentTagList 查询APP统一标签列表
|
||||
// @Tags app配置相关接口
|
||||
// @Summary 查询APP统一标签列表 -- by 王鸣
|
||||
// @Description 返回启用且有绑定内容数量的统一标签,contentTagType非必填,按绑定内容数量倒序排序
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.AppContentTagListReq false "统一标签列表请求"
|
||||
// @Success 200 {object} proto.AppContentTagListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/content/tag/list [get]
|
||||
func ContentTagList(ctx *gin.Context) {
|
||||
req := &proto.AppContentTagListReq{}
|
||||
if err := ctx.ShouldBindQuery(req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := configser.AppContentTagList(ctx, req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package dhapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/searchser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DHApi 导航
|
||||
// @Tags 导航
|
||||
// @Summary 导航
|
||||
// @Description 导航
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.DHHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /dh [get]
|
||||
func DHApi(c *gin.Context) {
|
||||
data := proto.DHHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
HotNavs: configser.GetHotNavs(),
|
||||
}
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// BusinessCooperationApi 导航
|
||||
// @Tags 导航
|
||||
// @Summary 导航
|
||||
// @Description 导航
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.HomeHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /businessCooperation [get]
|
||||
func BusinessCooperationApi(c *gin.Context) {
|
||||
data := proto.DHHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
data.CompletePath = "/businessCooperation"
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// ServicesProjectApi 导航
|
||||
// @Tags 导航
|
||||
// @Summary 导航
|
||||
// @Description 导航
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.HomeHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /servicesProject [get]
|
||||
func ServicesProjectApi(c *gin.Context) {
|
||||
data := proto.DHHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
data.CompletePath = "/servicesProject"
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// ConsultingApi 招募
|
||||
// @Tags 招募
|
||||
// @Summary 招募
|
||||
// @Description 招募
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.HomeHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /consulting [get]
|
||||
func ConsultingApi(c *gin.Context) {
|
||||
data := proto.DHConsultingHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
}
|
||||
data.CompletePath = "/consulting"
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// ConsultingInfoApi 招募详情
|
||||
// @Tags 招募详情
|
||||
// @Summary 招募详情
|
||||
// @Description 招募详情
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.HomeHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /consultinginfo/:id [get]
|
||||
func ConsultingInfoApi(c *gin.Context) {
|
||||
data := proto.DHConsultingInfoHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err == nil && id > 0 {
|
||||
data.CompletePath = fmt.Sprintf("/consultingInfo/%d", id)
|
||||
}
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"videoweb/app/service/homeser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Home 首页
|
||||
// @Tags 首页
|
||||
// @Summary 首页 --by 旺鸣
|
||||
// @Description 首页分类数据
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.DataResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/home [get]
|
||||
func Home(c *gin.Context) {
|
||||
data, err := homeser.HomeData(c)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// HomeBase 首页公共数据
|
||||
// @Tags 首页
|
||||
// @Summary 首页公共数据 --by 旺鸣
|
||||
// @Description 只返回翻译、配置、底部翻译、广告和公告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.HomeBaseResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/home/base [get]
|
||||
func HomeBase(c *gin.Context) {
|
||||
common.JSONWriterCache(c, srvdef.Success, homeser.HomeBaseData(c))
|
||||
}
|
||||
|
||||
// HomeTags 首页标签数据
|
||||
// @Tags 首页
|
||||
// @Summary 首页标签数据 --by 旺鸣
|
||||
// @Description 返回视频、短剧的标签信息,以及基础导航、配置、广告和公告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.HomeTagsResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/home/tags [get]
|
||||
func HomeTags(c *gin.Context) {
|
||||
common.JSONWriterCache(c, srvdef.Success, homeser.HomeTagsData(c))
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package lawandtrust
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/searchser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserAgreementApi 用户协议
|
||||
// @Tags 用户协议
|
||||
// @Summary 用户协议
|
||||
// @Description 用户协议
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /useragreement [get]
|
||||
func UserAgreementApi(c *gin.Context) {
|
||||
data := proto.LawAndTrustHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
}
|
||||
|
||||
data.CompletePath = "/useragreement"
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
|
||||
}
|
||||
|
||||
// PrivacyPolicyApi 隐私策略
|
||||
// @Tags 隐私策略
|
||||
// @Summary 隐私策略
|
||||
// @Description 隐私策略
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /privacyPolicy [get]
|
||||
func PrivacyPolicyApi(c *gin.Context) {
|
||||
data := proto.LawAndTrustHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
}
|
||||
data.CompletePath = "/useragreement"
|
||||
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
|
||||
}
|
||||
|
||||
// DmcaApi dmca
|
||||
// @Tags dmca
|
||||
// @Summary dmca
|
||||
// @Description dmca
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /dmca [get]
|
||||
func DmcaApi(c *gin.Context) {
|
||||
data := proto.LawAndTrustHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
}
|
||||
|
||||
data.CompletePath = "/useragreement"
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// StatementApi 合规声明
|
||||
// @Tags 合规声明
|
||||
// @Summary 合规声明
|
||||
// @Description 合规声明
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.LawAndTrustHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /statement [get]
|
||||
func StatementApi(c *gin.Context) {
|
||||
data := proto.LawAndTrustHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
HotSearch: searchser.GetHotSearchItem(c),
|
||||
}
|
||||
|
||||
data.CompletePath = "/useragreement"
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package navigationapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/navigationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HeaderNavigation 查询头部导航配置
|
||||
// @Tags app导航相关接口
|
||||
// @Summary 查询头部导航配置 -- by 王鸣
|
||||
// @Description 查询导航类型为URL模式和二级导航的头部导航配置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.AppTranslationInfo "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/navigation/header [get]
|
||||
func HeaderNavigation(ctx *gin.Context) {
|
||||
resp, err := navigationser.HeaderNavigation(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.JSONWriterCache(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package playletapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/app/service/slugser"
|
||||
"videoweb/app/service/videoser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HomePlaylet app端短剧首页
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧首页 -- by 王鸣
|
||||
// @Description 返回短剧Banner、首页配置模块、公告、广告和SEO信息
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.HomePlayletResponse "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/home [get]
|
||||
func HomePlaylet(ctx *gin.Context) {
|
||||
resp, err := playletser.Home(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// HomeConfigList 按首页配置分页查询内容
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端首页配置内容列表 -- by 王鸣
|
||||
// @Description 按首页配置ID或slug分页查询内容,同时传入时优先使用slug
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletHomeConfigListReq true "首页配置内容分页请求"
|
||||
// @Success 200 {object} proto.PlayletHomeConfigListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/home-config/list [get]
|
||||
func HomeConfigList(ctx *gin.Context) {
|
||||
var req proto.PlayletHomeConfigListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.HomeConfigList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TopicDetail 查询短剧题材或视频分类专题详情
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端专题详情 -- by 王鸣
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletTopicDetailReq true "专题详情请求"
|
||||
// @Success 200 {object} proto.PlayletTopicDetailResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/topic/detail [get]
|
||||
func TopicDetail(ctx *gin.Context) {
|
||||
var req proto.PlayletTopicDetailReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.TopicDetail(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ThemeList 查询全部已启用短剧题材
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧题材列表
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.PlayletThemeListResp "成功"
|
||||
// @Router /api/app/playlet/theme/list [get]
|
||||
func ThemeList(ctx *gin.Context) {
|
||||
resp, err := playletser.PlayletThemeList(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// List 分页查询已上架短剧并穿插广告
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧列表 -- by 王鸣
|
||||
// @Description 按题材和排序筛选已上架短剧,最近更新按更新时间降序,每4部短剧穿插1条广告
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletListReq true "短剧列表请求"
|
||||
// @Success 200 {object} proto.PlayletListResp{list=[]proto.PlayletRecommendListItem} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/list [get]
|
||||
func List(ctx *gin.Context) {
|
||||
var req proto.PlayletListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.PlayletList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ContentList 分页查询上架短剧或视频并穿插广告
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧或视频全站排行榜列表 -- by 王鸣
|
||||
// @Description 按观看量查询上架内容,dataType 为 1 时每 4 条短剧穿插 1 条广告,为 2 时每 5 条视频穿插 1 条广告,并返回广告配置
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletContentListReq true "短剧或视频列表请求"
|
||||
// @Success 200 {object} proto.PlayletContentListResp{list=[]proto.PlayletContentListItem} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/content/list [get]
|
||||
func ContentList(ctx *gin.Context) {
|
||||
var req proto.PlayletContentListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ContentList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// RecommendList app端推荐短剧列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端推荐短剧列表 -- by 王鸣
|
||||
// @Description 分页查询短剧列表,带穿插列表广告,每间隔8条短剧数据插入一个广告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletRecommendListReq true "推荐短剧列表请求"
|
||||
// @Success 200 {object} proto.PlayletRecommendListResp{list=[]proto.PlayletRecommendListItem} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/recommend/list [get]
|
||||
func RecommendList(ctx *gin.Context) {
|
||||
var req proto.PlayletRecommendListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.PlayletRecommendList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// GuessList 分页查询猜你喜欢短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 猜你喜欢短剧列表 -- by 王鸣
|
||||
// @Description id可选,传入时排除当前短剧,按观看量降序查询上架短剧
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletGuessListReq true "猜你喜欢短剧请求"
|
||||
// @Success 200 {object} proto.PlayletGuessListResp{list=[]proto.PlayletRecommendListItem} "成功"
|
||||
// @Router /api/app/playlet/guess/list [get]
|
||||
func GuessList(ctx *gin.Context) {
|
||||
var req proto.PlayletGuessListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.PlayletGuessList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TagList app端根据标签ID查询短剧列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端根据标签ID查询短剧列表 -- by 王鸣
|
||||
// @Description 根据统一标签ID分页查询已上架短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Param pageNum query int false "页码,默认1"
|
||||
// @Param pageSize query int false "每页数量,默认20,最大100"
|
||||
// @Success 200 {object} proto.PlayletRecommendListResp{list=[]proto.PlayletRecommendListItem} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/tag/{id} [get]
|
||||
func TagList(ctx *gin.Context) {
|
||||
var req proto.ContentTagPageReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
pathID, pathSlug := slugser.ParseIDOrSlug(ctx.Param("id"))
|
||||
if req.Id <= 0 {
|
||||
req.Id = pathID
|
||||
}
|
||||
if req.Slug == "" {
|
||||
req.Slug = pathSlug
|
||||
}
|
||||
resp, err := playletser.PlayletTagList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryPlayletInfo 短剧详情
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 短剧详情 -- by 王鸣
|
||||
// @Description 查询短剧详情、剧集列表、点赞状态和收藏状态
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryPlayletInfoReq true "短剧详情查询请求"
|
||||
// @Success 200 {object} proto.QueryPlayletInfoResp{playletInfo=proto.PlayletDetailInfo,chapterList=[]proto.PlayletChapterInfo} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/info [get]
|
||||
func QueryPlayletInfo(ctx *gin.Context) {
|
||||
var req proto.QueryPlayletInfoReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.QueryPlayletInfo(ctx, common.GetUserId(ctx), videoser.H5M3U8RequestIP(ctx), ctx.Request.UserAgent(), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryPlayletChapterInfo 短剧剧集详情
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧剧集详情 -- by 王鸣
|
||||
// @Description 根据剧集ID查询短剧剧集详情、所属短剧、剧集列表、热门短剧列表和点赞状态
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryPlayletChapterInfoReq true "短剧剧集详情查询请求"
|
||||
// @Success 200 {object} proto.QueryPlayletChapterInfoResp{chapterInfo=proto.PlayletChapterInfo,playletInfo=proto.PlayletData,chapterList=[]proto.PlayletChapterInfo,hotPlayletList=[]proto.PlayletData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/chapter/info [get]
|
||||
func QueryPlayletChapterInfo(ctx *gin.Context) {
|
||||
var req proto.QueryPlayletChapterInfoReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.QueryPlayletChapterInfo(ctx, common.GetUserId(ctx), videoser.H5M3U8RequestIP(ctx), ctx.Request.UserAgent(), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// LikePlaylet 点赞或取消点赞短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 点赞或取消点赞短剧 -- by 王鸣
|
||||
// @Description 当前登录用户点赞或取消点赞指定短剧,isLike=true表示点赞,isLike=false表示取消点赞
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.LikePlayletReq true "点赞或取消点赞请求"
|
||||
// @Success 200 {object} proto.LikePlayletResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/like [post]
|
||||
func LikePlaylet(ctx *gin.Context) {
|
||||
var req proto.LikePlayletReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
ua, _ := common.GetUA(ctx)
|
||||
if ua.UserAgent == "" {
|
||||
ua.UserAgent = ctx.Request.UserAgent()
|
||||
}
|
||||
resp, err := playletser.LikePlaylet(ctx, userId, ctx.ClientIP(), ua, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryLikePlayletList 当前用户点赞短剧分页列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 当前用户点赞短剧分页列表
|
||||
// @Description 按点赞时间倒序返回当前用户点赞的上架短剧
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryLikePlayletListReq true "点赞短剧分页请求"
|
||||
// @Success 200 {object} proto.QueryLikePlayletListResp{list=[]proto.PlayletData} "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/like/list [get]
|
||||
func QueryLikePlayletList(ctx *gin.Context) {
|
||||
var req proto.QueryLikePlayletListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userID := common.GetUserId(ctx)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.QueryLikePlayletList(ctx, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// FavoritePlaylet 收藏或取消收藏短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 收藏或取消收藏短剧
|
||||
// @Description isFavorite=true 收藏 false 取消收藏
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.FavoritePlayletReq true "短剧收藏请求"
|
||||
// @Success 200 {object} proto.FavoritePlayletResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/favorite [post]
|
||||
func FavoritePlaylet(ctx *gin.Context) {
|
||||
var req proto.FavoritePlayletReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userID := common.GetUserId(ctx)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
ua, _ := common.GetUA(ctx)
|
||||
if ua.UserAgent == "" {
|
||||
ua.UserAgent = ctx.Request.UserAgent()
|
||||
}
|
||||
resp, err := playletser.FavoritePlaylet(ctx, userID, ctx.ClientIP(), ua, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryFavoritePlayletList 当前用户收藏短剧分页列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 当前用户收藏短剧分页列表
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryFavoritePlayletListReq true "收藏短剧分页请求"
|
||||
// @Success 200 {object} proto.QueryFavoritePlayletListResp{list=[]proto.PlayletData} "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/favorite/list [get]
|
||||
func QueryFavoritePlayletList(ctx *gin.Context) {
|
||||
var req proto.QueryFavoritePlayletListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userID := common.GetUserId(ctx)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.QueryFavoritePlayletList(ctx, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ReportPlayletHistory 播放成功后记录当前用户的短剧剧集
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 上报短剧观看历史
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletHistoryReportReq true "当前播放短剧和剧集"
|
||||
// @Success 200 {object} proto.PlayletHistoryReportResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/history [post]
|
||||
func ReportPlayletHistory(ctx *gin.Context) {
|
||||
userID := common.GetUserId(ctx)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
var req proto.PlayletHistoryReportReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ReportPlayletHistory(ctx, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryPlayletHistoryList 当前用户短剧观看历史分页列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 短剧观看历史列表
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletHistoryListReq true "分页请求"
|
||||
// @Success 200 {object} proto.PlayletHistoryListResp{list=[]proto.PlayletHistoryItem} "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/history/list [get]
|
||||
func QueryPlayletHistoryList(ctx *gin.Context) {
|
||||
userID := common.GetUserId(ctx)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
var req proto.PlayletHistoryListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.QueryPlayletHistoryList(ctx, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ClearPlayletHistory 清空当前用户短剧观看历史
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 清空短剧观看历史
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.PlayletHistoryClearResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/history [delete]
|
||||
func ClearPlayletHistory(ctx *gin.Context) {
|
||||
userID := common.GetUserId(ctx)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ClearPlayletHistory(ctx, userID)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryPlayletRankList 短剧排行榜分页列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 短剧排行榜分页列表 -- by 王鸣
|
||||
// @Description 分页查询短剧排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletRankReq true "短剧排行榜分页请求"
|
||||
// @Success 200 {object} proto.PlayletRankResp{list=[]proto.PlayletRankData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/rank/list [get]
|
||||
func QueryPlayletRankList(ctx *gin.Context) {
|
||||
var req proto.PlayletRankReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := playletser.QueryPlayletRank(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// QueryActressWorkListReq 指定女优作品列表请求
|
||||
type QueryActressWorkListReq struct {
|
||||
pubdef.Page
|
||||
// 女优ID
|
||||
Id int64 `json:"id" form:"id"`
|
||||
// 女优slug,非空时优先按slug解析女优ID
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
// 上架时间升序/降序(1-升序 2降序)
|
||||
AddedTimeSort int64 `json:"addedTimeSort" form:"addedTimeSort"`
|
||||
// 按观看量升序/降序(1-升序 2降序)
|
||||
WatchTimesSort int64 `json:"watchTimesSort" form:"watchTimesSort"`
|
||||
// 按收藏(点赞)量升序/降序(1-升序 2降序)
|
||||
LikesSort int64 `json:"likesSort" form:"likesSort"`
|
||||
}
|
||||
|
||||
// QueryActressWorkListResp 指定女优详情和作品混排列表响应
|
||||
type QueryActressWorkListResp struct {
|
||||
// 分页信息;List 类型为 []*QueryVideoListItem,包含视频项(type=0)和广告项(type=2)
|
||||
response.BasePageResp
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 女优信息
|
||||
ActressInfo ActressInfoData `json:"actressInfo"`
|
||||
}
|
||||
|
||||
// QueryActressListResp 女优榜单列表响应
|
||||
type QueryActressListResp struct {
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 底部导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 女优列表
|
||||
ActressInfoList []ActressInfoData `json:"actressInfoList"`
|
||||
}
|
||||
|
||||
type ActressInfoData struct {
|
||||
Id int64 `json:"id" bson:"_id"` // Id
|
||||
Alias string `bson:"alias" json:"alias"` // 别名
|
||||
Slug string `json:"slug"` // slug
|
||||
Avatar string `bson:"avatar" json:"avatar"` // 头像
|
||||
Cover string `bson:"cover" json:"cover"` // 封面大图
|
||||
Desc string `bson:"desc" json:"desc"` // 详细描述
|
||||
Meas actress.Meas `bson:"meas" json:"meas"` // 三围
|
||||
MeasValue string `bson:"measValue" json:"measValue"` // 三围值
|
||||
Office string `json:"office" bson:"office"` // 事务所
|
||||
DebutDate time.Time `bson:"debutDate" json:"debutDate" ` // 出道日期
|
||||
BornDate time.Time `bson:"bornDate" json:"bornDate"` // 出生日期
|
||||
Region string `json:"region" bson:"region"` // 地区/国家
|
||||
Score string `json:"score" bson:"score"` // 评分
|
||||
Works int64 `json:"works" bson:"works"` // 作品数
|
||||
HotValue int64 `json:"hotValue" bson:"hotValue"` // 热度值
|
||||
WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
|
||||
ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
|
||||
Comments int64 `json:"comments" bson:"comments"` // 评论条数
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
func ActressInfoToData(actress2 actress.Actress, imageCdn string) ActressInfoData {
|
||||
return ActressInfoData{
|
||||
Id: actress2.Id,
|
||||
Alias: actress2.Alias,
|
||||
Slug: actress2.Slug,
|
||||
Avatar: netutil.MergeURL(imageCdn, actress2.Avatar),
|
||||
Cover: netutil.MergeURL(imageCdn, actress2.Cover),
|
||||
Desc: actress2.Desc,
|
||||
Meas: actress2.Meas,
|
||||
Office: actress2.Office,
|
||||
DebutDate: actress2.DebutDate,
|
||||
BornDate: actress2.BornDate,
|
||||
Region: actress2.Region,
|
||||
Score: actress2.Score,
|
||||
MeasValue: actress2.Meas.Measurements(),
|
||||
Works: actress2.Works,
|
||||
HotValue: actress2.HotValue,
|
||||
WatchTimes: actress2.Real.WatchTimes*100 + 2273,
|
||||
ShowTimes: actress2.Real.WatchTimes*100 + 3273,
|
||||
Comments: actress2.Real.Comments,
|
||||
SeoInfo: actress2.SeoInfo,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/storage/proxy"
|
||||
|
||||
type AdsClickReq struct {
|
||||
AdsId string `json:"adsId,omitempty" binding:"required"`
|
||||
Location int64 `json:"location,omitempty" binding:"required"` // 位置ID
|
||||
}
|
||||
|
||||
type AdsClickInfo struct {
|
||||
AppId int64 `json:"appId,omitempty" binding:"required"` // appId
|
||||
AdsId int64 `json:"adsId,omitempty" binding:"required"`
|
||||
Location int64 `json:"location,omitempty" binding:"required"` // 位置ID
|
||||
}
|
||||
|
||||
type AdvitiseData struct {
|
||||
HomeList []proxy.AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表
|
||||
DetailTopList []proxy.AdvitiseInfo `json:"detailTopList" bson:"detailTopList"` // 详情页顶部广告
|
||||
NineGridList []proxy.AdvitiseInfo `json:"nineGridList" bson:"nineGridList"` // 9宫格
|
||||
DetailbannerList []proxy.AdvitiseInfo `json:"detailbannerList" bson:"detailbannerList"` // 详情banner
|
||||
LaunchScreenList []proxy.AdvitiseInfo `json:"launchScreenList"` // 启动页广告
|
||||
FloatingList []proxy.AdvitiseInfo `json:"floatingList" bson:"floatingList"` // 悬浮小方块
|
||||
AList []proxy.AdvitiseInfo `json:"AList"` // 固定12广告位
|
||||
BList []proxy.AdvitiseInfo `json:"BList"` // 固定13广告位
|
||||
DialogList []proxy.AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告
|
||||
PostList []proxy.AdvitiseInfo `json:"postList" bson:"postList"` // 列表穿插广告(视频、短剧、女优列表使用)
|
||||
MustAppList []proxy.AdvitiseInfo `json:"mustAppList" bson:"mustAppList"` // 必备精品
|
||||
TitleAppList []proxy.AdvitiseInfo `json:"titleAppList" bson:"titleAppList"` // 标题广告(APP名称)
|
||||
FriendList []proxy.AdvitiseInfo `json:"friendList" bson:"friendList"` // 友链
|
||||
VideoList []proxy.AdvitiseInfo `json:"videoList" bson:"videoList"` // 视频列表
|
||||
|
||||
//VideoInfoList []proxy.AdvitiseInfo `json:"videoInfoList" bson:"videoInfoList"` // 视频详情
|
||||
//DownList []proxy.AdvitiseInfo `json:"downList" bson:"downList"` // 底部广告
|
||||
//HomeBanner []proxy.AdvitiseInfo `json:"homeBanner" bson:"homeBanner"` // 首页banner小广告
|
||||
//PlayList []proxy.AdvitiseInfo `json:"playList" bson:"playList"` // 播放强制跳转
|
||||
//PauseyList []proxy.AdvitiseInfo `json:"pauseyList" bson:"pauseyList"` // 暂停广告
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package proto
|
||||
|
||||
// AppListResp 应用列表接口响应
|
||||
type AppListResp struct {
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 底部导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 公告
|
||||
Announcement AnnouncementInfo `json:"announcement"`
|
||||
// 应用分类列表(含各分类下的应用列表)
|
||||
CategoryList []AppCategoryInfo `json:"categoryList"`
|
||||
// 当前分类(含应用列表)
|
||||
Category AppCategoryInfo `json:"category"`
|
||||
}
|
||||
|
||||
// AppCategoryInfo 应用分类信息
|
||||
type AppCategoryInfo struct {
|
||||
// 分类ID
|
||||
Id int64 `json:"id"`
|
||||
// 分类标题
|
||||
Title string `json:"title"`
|
||||
// 分类简介
|
||||
Intro string `json:"intro"`
|
||||
// 排序
|
||||
Sort int64 `json:"sort"`
|
||||
// 分类编码(特殊值:12=广告位A 13=广告位B)
|
||||
Code int64 `json:"code"`
|
||||
// 应用列表
|
||||
AppList []AppInfoData `json:"appList"`
|
||||
}
|
||||
|
||||
// AppInfoData 应用信息
|
||||
type AppInfoData struct {
|
||||
// 应用ID
|
||||
Id int64 `json:"id"`
|
||||
// 所属分类ID
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
// 应用名称
|
||||
Name string `json:"name"`
|
||||
// 简介
|
||||
Intro string `json:"intro"`
|
||||
// 详细描述
|
||||
Description string `json:"description"`
|
||||
// Logo 图片地址
|
||||
Logo string `json:"logo"`
|
||||
// 跳转链接(已含渠道码)
|
||||
Url string `json:"url"`
|
||||
// 排序
|
||||
Sort int64 `json:"sort"`
|
||||
// 广告ID(广告条目时返回)
|
||||
AdvitiseId string `json:"advitiseId"`
|
||||
// 广告位置ID
|
||||
Location int64 `json:"location"`
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type CommentListReq struct {
|
||||
ObjectId int64 `form:"objectId" json:"objectId"` // 对象ID
|
||||
ObjectType pubdef.CommentObjectType `form:"objectType" json:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
|
||||
SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1=热门 2=最新 默认最新
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
type CommentListResp struct {
|
||||
response.BasePageResp
|
||||
}
|
||||
|
||||
type GetCommentListReq struct {
|
||||
ObjectId int64 `form:"objectId" json:"objectId"` // 对象ID
|
||||
ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
type GetCommentListResp struct {
|
||||
ObjectId int64 `json:"objectId" bson:"objectId"`
|
||||
Total int64 `json:"total" bson:"total"` // 总数量
|
||||
List []CommentInfo `json:"list" bson:"list"` // 数据
|
||||
}
|
||||
|
||||
type CommentSendReq struct {
|
||||
ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
|
||||
ObjectType pubdef.CommentObjectType `json:"objectType" bson:"objectType"` // 对象类型 0=视频 4=短剧 11=短剧剧集
|
||||
ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
|
||||
Text string `json:"text" bson:"text"` // 评论内容
|
||||
}
|
||||
|
||||
type CommentInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
|
||||
ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
|
||||
UserName string `json:"userName" bson:"userName"` // 用户名称
|
||||
Avatar string `json:"avatar" bson:"avatar"` // 用户头像
|
||||
Level int64 `json:"level" bson:"level"` // 评论级别
|
||||
ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
|
||||
ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
|
||||
Text string `json:"text" bson:"text"` // 评论内容
|
||||
CommentCount int64 `json:"commentCount" bson:"commentCount"` // 该评论回复数量
|
||||
CountLike int64 `json:"countLike" bson:"countLike"` // 点赞数
|
||||
IsLike bool `json:"isLike" bson:"-"` // 当前用户是否点赞
|
||||
SubComment []CommentInfo `json:"subComment" bson:"subComment"` // 子评论
|
||||
CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
type CommentLikeSetReq struct {
|
||||
CommentId int64 `json:"commentId" binding:"required"` // 评论ID
|
||||
IsLike *bool `json:"isLike" binding:"required"` // 是否点赞
|
||||
}
|
||||
|
||||
type CommentLikeSetResp struct {
|
||||
CommentId int64 `json:"commentId"` // 评论ID
|
||||
IsLike bool `json:"isLike"` // 当前点赞状态
|
||||
CountLike int64 `json:"countLike"` // 点赞数
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
type ConfigInfo struct {
|
||||
PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
|
||||
NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
|
||||
NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
|
||||
NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
|
||||
ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
|
||||
GmailAddr string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
|
||||
AppAddr string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
|
||||
WeiChart string `json:"weiChart" bson:"weiChart"` //微信群
|
||||
QQ string `json:"qq" bson:"qq"` //qq群
|
||||
TgGroup string `json:"tgGroup" bson:"tgGroup"` //tg群
|
||||
PotaoGroup string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
|
||||
GitAddr string `json:"gitAddr" bson:"gitAddr"` //git地址
|
||||
TwitterAddr string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
|
||||
Telegram string `json:"telegram" bson:"telegram"` //商务tg
|
||||
TelegramAdd string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
|
||||
TranslationAddr string `json:"translationAddr" bson:"translationAddr"` //应用导航
|
||||
ImgCdn string `json:"imgCdn" bson:"imgCdn"` // 图片CDN
|
||||
TsCdn string `json:"tsCdn" bson:"tsCdn"` // 视频CDN
|
||||
ImgAppUsCdn string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片CDN(海外app使用)
|
||||
MediaAppUsCdn string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn"` // 视频CDN(海外app使用)
|
||||
WebTitle string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
|
||||
WebKeyworld []string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
|
||||
WebDescription string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
|
||||
WebSiteName string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
|
||||
WebAddr string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
|
||||
DHNewUrlAddr1 string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路1
|
||||
DHNewUrlAddr2 string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
|
||||
DHNewUrlAddr3 string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
|
||||
FreeVPN string `json:"freeVPN" bson:"freeVPN"` // 免费vpn链接
|
||||
UseIngAddress []string `json:"useIngAddress" bson:"useIngAddress"` // 所有使用中的域名
|
||||
DHTitle string `json:"dhTitle" bson:"dhTitle"` // 导航SEO标题
|
||||
DHKeyworld []string `json:"dhKeyworld" bson:"dhKeyworld"` // 导航SEO关键字
|
||||
DHDescription string `json:"dhDescription" bson:"dhDescription"` // 导航SEO描述
|
||||
DHSiteName string `json:"dhSiteName" bson:"dhSiteName"` // 导航SEO网站名称
|
||||
ReportSDKConfig string `json:"reportSdkConfig"` // 数据上报SDK域名配置
|
||||
ChannelCode string `json:"channelCode"` // 渠道码
|
||||
EnableH265 bool `json:"enableH265" bson:"enableH265"` // 是否允许客户端使用 H265 地址播放
|
||||
M3U8DayRateLimitMax int64 `json:"m3u8DayRateLimitMax" bson:"m3u8DayRateLimitMax"` // 每 IP 每天最多不同正片数
|
||||
|
||||
}
|
||||
|
||||
// AnnouncementInfo 公告数据
|
||||
type AnnouncementInfo struct {
|
||||
VideoInfo []VideoInfoData `json:"videoInfo" bson:"videoInfo"` // 热门信息
|
||||
PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
|
||||
NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` // 新国内地址
|
||||
NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` // 备用地址1
|
||||
NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` // 备用地址1
|
||||
ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` // 永久海外地址(翻墙访问 永不迷路)
|
||||
AppAddr string `json:"appAddr" bson:"appAddr"` // 吃瓜app (免费吃瓜 每日更新)
|
||||
TranslationAddr string `json:"translationAddr" bson:"translationAddr"` // 应用导航
|
||||
}
|
||||
|
||||
type AdvitiseInfo struct {
|
||||
Id int64 `json:"id" bson:"_id"` // 广告id
|
||||
Title string `json:"title" bson:"title"` // 广告标题
|
||||
Images string `json:"images" bson:"images"` // 视频banner
|
||||
Link string `json:"link" bson:"link"` // 链接
|
||||
}
|
||||
|
||||
// FooterInfoResp APP底部信息响应
|
||||
type FooterInfoResp struct {
|
||||
FooterInfo FooterInfo `json:"footerInfo"` // 底部信息
|
||||
}
|
||||
|
||||
// FooterInfo APP底部信息
|
||||
type FooterInfo struct {
|
||||
Brand FooterBrand `json:"brand"` // 品牌信息
|
||||
NavList []TranslationCategory `json:"navList"` // 底部导航
|
||||
ContactList []FooterContact `json:"contactList"` // 联系方式
|
||||
}
|
||||
|
||||
// FooterBrand APP底部品牌信息
|
||||
type FooterBrand struct {
|
||||
Title string `json:"title"` // 品牌标题
|
||||
Content string `json:"content"` // 品牌标语
|
||||
}
|
||||
|
||||
// FooterContact APP底部联系方式
|
||||
type FooterContact struct {
|
||||
Name string `json:"name"` // 联系方式名称
|
||||
Value string `json:"value"` // 联系方式值
|
||||
}
|
||||
|
||||
// AppContentTagListReq app端统一标签列表请求
|
||||
type AppContentTagListReq struct {
|
||||
ContentTagType *contenttagmod.ContentTagType `form:"contentTagType" json:"contentTagType"` // 内容类型 1-视频 5-短剧,不传返回全部有绑定数量的标签
|
||||
}
|
||||
|
||||
// ContentTagPageReq 根据统一标签分页查询内容请求
|
||||
type ContentTagPageReq struct {
|
||||
pubdef.Page
|
||||
Id int64 `json:"id" form:"id"` // 标签ID
|
||||
Slug string `json:"slug" form:"slug"` // 标签slug,非空时优先按slug解析标签ID
|
||||
}
|
||||
|
||||
// AppContentTagListResp app端统一标签列表响应
|
||||
type AppContentTagListResp struct {
|
||||
List []*AppContentTagItem `json:"list"` // 统一标签列表
|
||||
}
|
||||
|
||||
// AppContentTagItem app端统一标签列表项
|
||||
type AppContentTagItem struct {
|
||||
Id int64 `json:"id" bson:"_id"` // 标签ID
|
||||
Name string `json:"name" bson:"name"` // 标签名称
|
||||
Slug string `json:"slug" bson:"slug"` // slug
|
||||
Types []contenttagmod.ContentTagType `json:"types" bson:"types"` // 支持类型 1-视频 5-短剧
|
||||
BindCount int64 `json:"bindCount" bson:"bindCount"` // 当前查询类型绑定数量,不传type时为各类型绑定数量合计
|
||||
VideoCount int64 `json:"videoCount" bson:"videoCount"` // 绑定视频数量
|
||||
PlayletCount int64 `json:"playletCount" bson:"playletCount"` // 绑定短剧数量
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Rank int64 `json:"rank,omitempty" bson:"rank,omitempty"` // 排序
|
||||
}
|
||||
|
||||
// type AdvitiseData struct {
|
||||
// DialogList []AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告
|
||||
// HomeList []AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表
|
||||
// VideoTopList []AdvitiseInfo `json:"videoTopList" bson:"videoTopList"` // 帖子详情(上)
|
||||
// VideoDownList []AdvitiseInfo `json:"videoDownList" bson:"videoDownList"` // 帖子详情(下)
|
||||
// HotAppList []AdvitiseInfo `json:"hotAppList" bson:"hotAppList"` // 热门应用
|
||||
// NewAppList []AdvitiseInfo `json:"newAppList" bson:"newAppList"` // 最新上架
|
||||
// MustAppList []AdvitiseInfo `json:"mustAppList" bson:"mustAppList"` // 必备精品
|
||||
// TitleAppList []AdvitiseInfo `json:"titleAppList" bson:"titleAppList"` // 标题广告(APP名称)
|
||||
// }
|
||||
@@ -0,0 +1,6 @@
|
||||
package proto
|
||||
|
||||
type ContributeData struct {
|
||||
Path string `json:"path"`
|
||||
RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package proto
|
||||
|
||||
// HotNavItem 热门导航
|
||||
type HotNavItem struct {
|
||||
Id int64 `json:"id"` // ID
|
||||
Title string `json:"title"` // 标题
|
||||
Intro string `json:"intro"` // 介绍
|
||||
Url string `json:"url"` // 链接
|
||||
Icon string `json:"icon"` // 图标
|
||||
}
|
||||
|
||||
// DHHtmlData 导航数据
|
||||
type DHHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotNavs []HotNavItem `json:"hotNavs"` // 热门导航
|
||||
}
|
||||
|
||||
// DHConsultingHtmlData 招募
|
||||
type DHConsultingHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
|
||||
// DHConsultingInfoHtmlData 招募
|
||||
type DHConsultingInfoHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package proto
|
||||
|
||||
type FindVideoData struct {
|
||||
RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// DataResp 首页数据响应
|
||||
type DataResp struct {
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 公告
|
||||
Announcement AnnouncementInfo `json:"announcement"`
|
||||
// 首页banner信息列表
|
||||
HomeBannerList []HomeBannerInfo `json:"homeBannerList"`
|
||||
// 首页短剧数据
|
||||
PlayletData HomePlayletDataResp `json:"playletData"`
|
||||
// 首页视频数据
|
||||
VideoData HomeVideoDataResp `json:"videoData"`
|
||||
// 女优信息
|
||||
ActressInfoList []ActressInfo `json:"actressInfoList"`
|
||||
}
|
||||
|
||||
// HomeBannerInfo 首页banner信息
|
||||
type HomeBannerInfo struct {
|
||||
Id int64 `json:"id"` // ID
|
||||
Name string `json:"name"` // 名称
|
||||
Image string `json:"image"` // 图片,已拼接图片域名
|
||||
VerticalImage string `json:"verticalImage"` // 竖版图片,已拼接图片域名
|
||||
Url string `json:"url"` // URL
|
||||
Rank int64 `json:"rank"` // 排序
|
||||
Status bool `json:"status"` // 状态 true-启用 false-停用
|
||||
}
|
||||
|
||||
// HomePlayletDataResp 首页短剧数据响应
|
||||
type HomePlayletDataResp struct {
|
||||
RecommendList []PlayletData `json:"recommendList"` // 推荐短剧列表
|
||||
NewestList []PlayletData `json:"newestList"` // 最新短剧列表
|
||||
HotList []PlayletData `json:"hotList"` // 最热短剧列表
|
||||
}
|
||||
|
||||
// HomeVideoDataResp 首页视频数据响应
|
||||
type HomeVideoDataResp struct {
|
||||
RecommendList []VideoInfo `json:"recommendList"` // 推荐视频列表
|
||||
NewestList []VideoInfo `json:"newestList"` // 最新视频列表
|
||||
HotList []VideoInfo `json:"hotList"` // 最热视频列表
|
||||
}
|
||||
|
||||
// HomeBaseResp 首页公共数据响应
|
||||
type HomeBaseResp struct {
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 公告
|
||||
Announcement AnnouncementInfo `json:"announcement"`
|
||||
}
|
||||
|
||||
// HomeTagsResp 首页标签数据响应
|
||||
type HomeTagsResp struct {
|
||||
HomeBaseResp
|
||||
// 视频标签
|
||||
VideoTagsList []TagsInfoData `json:"videoTagsList"`
|
||||
// 短剧标签
|
||||
PlayletTagsList []TagsInfoData `json:"playletTagsList"`
|
||||
}
|
||||
|
||||
// VideoCategoryDataInfo 视频分类数据
|
||||
type VideoCategoryDataInfo struct {
|
||||
CommonData
|
||||
// Seo相关信息
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
// 描述
|
||||
Desc string `json:"desc"`
|
||||
// 分类关联的视频标签
|
||||
Tags []int64 `json:"tags"`
|
||||
// 视频信息集合
|
||||
VideoInfoList []VideoInfo `json:"videoInfoList"`
|
||||
}
|
||||
|
||||
// CommonData 分类通用数据
|
||||
type CommonData struct {
|
||||
// Id
|
||||
Id int64 `json:"id"`
|
||||
// 分类名称
|
||||
Name string `json:"name"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// APP显示
|
||||
Title string `json:"title"`
|
||||
// 显示位置 0=仅TAB 1=仅首页 2=两者都显示
|
||||
DisplayPosition pubdef.CategoryDisplayPosition `json:"displayPosition"`
|
||||
// 排序方式 0:上线时间降序 1:观看次数降序 2:点赞(收藏)次数降序 3:上线时间升序 4:观看次数升序 5:点赞(收藏)次数升序
|
||||
SortType pubdef.CategorySortType `json:"sortType"`
|
||||
// 展示方式
|
||||
DisplayMode pubdef.DisplayMode `json:"displayMode"`
|
||||
// URL地址
|
||||
UrlValue string `json:"urlValue"`
|
||||
}
|
||||
|
||||
// VideoInfo 视频信息
|
||||
type VideoInfo struct {
|
||||
ContentData
|
||||
}
|
||||
|
||||
// ContentData 内容数据信息
|
||||
type ContentData struct {
|
||||
// id
|
||||
Id int64 `json:"id"`
|
||||
// 标题
|
||||
Title string `json:"title"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// 上架时间
|
||||
AddedTime time.Time `json:"addedTime"`
|
||||
// 封面
|
||||
CoverImg string `json:"coverImg"`
|
||||
// 标签id
|
||||
Tags []int64 `json:"tags"`
|
||||
// 标签名称
|
||||
TagsName []string `json:"tagsName"`
|
||||
// 标签slug 与tagsName顺序一致
|
||||
TagSlugs []string `json:"tagSlugs"`
|
||||
// 观看展示数
|
||||
WatchNum int64 `json:"watchNum"`
|
||||
// 点赞展示数
|
||||
LikeNum int64 `json:"likeNum"`
|
||||
// 收藏展示数
|
||||
CountCollect int64 `json:"countCollect,omitempty"`
|
||||
// Seo相关信息
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
}
|
||||
|
||||
// RankResp 排行榜通用响应
|
||||
type RankResp struct {
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
RankData HomeRankData `json:"rankData"`
|
||||
}
|
||||
|
||||
// HomeRankData 首页排行榜数据(总榜/周榜/月榜/年榜)
|
||||
type HomeRankData struct {
|
||||
Total []ContentData `json:"total"` // 总榜
|
||||
Week []ContentData `json:"week"` // 周榜
|
||||
Month []ContentData `json:"month"` // 月榜
|
||||
Year []ContentData `json:"year"` // 年榜
|
||||
}
|
||||
|
||||
// ActressInfo 女优信息
|
||||
type ActressInfo struct {
|
||||
// Id
|
||||
Id int64 `json:"id"`
|
||||
// 别名
|
||||
Alias string `json:"alias"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// 头像
|
||||
Avatar string `json:"avatar"`
|
||||
// 作品数
|
||||
Works int64 `json:"works"`
|
||||
// 热度值
|
||||
HotValue int64 `json:"hotValue"`
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package proto
|
||||
|
||||
// LawAndTrustHtmlData
|
||||
type LawAndTrustHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage/proxy"
|
||||
)
|
||||
|
||||
// PlayletRecommendListReq 推荐短剧分页请求
|
||||
type PlayletRecommendListReq struct {
|
||||
pubdef.Page
|
||||
TagId int64 `form:"tagId" json:"tagId"` // 标签ID,不传查询全部标签
|
||||
TagSlug string `form:"tagSlug" json:"tagSlug"` // 标签slug,非空时优先按slug解析标签ID
|
||||
CategoryId int64 `form:"categoryId" json:"categoryId"` // 短剧分类ID,不传查询全部分类
|
||||
CategorySlug string `form:"categorySlug" json:"categorySlug"` // 分类slug,非空时优先按slug解析分类ID
|
||||
SortList int64 `form:"sortList" json:"sortList"` // 排序方式 1-推荐 2-最新 3-最热
|
||||
}
|
||||
|
||||
// PlayletRecommendSort 推荐短剧排序方式
|
||||
type PlayletRecommendSort int64
|
||||
|
||||
const (
|
||||
PlayletRecommendSortRecommend PlayletRecommendSort = 1 // 推荐 后台配置 isRecommend=true
|
||||
PlayletRecommendSortNewest PlayletRecommendSort = 2 // 最新 按短剧最后更新时间从近到远排序
|
||||
PlayletRecommendSortHot PlayletRecommendSort = 3 // 最热 按短剧累计点击量从高到低排序
|
||||
)
|
||||
|
||||
// PlayletRecommendItemType 推荐短剧列表项类型
|
||||
type PlayletRecommendItemType int64
|
||||
|
||||
const (
|
||||
PlayletRecommendItemTypePlaylet PlayletRecommendItemType = 0 // 短剧数据
|
||||
PlayletRecommendItemTypeAdvertise PlayletRecommendItemType = 2 // 广告数据
|
||||
)
|
||||
|
||||
// PlayletRecommendListResp 推荐短剧响应
|
||||
type PlayletRecommendListResp struct {
|
||||
response.BasePageResp
|
||||
}
|
||||
|
||||
// PlayletThemeListResp 已启用短剧题材列表响应
|
||||
type PlayletThemeListResp struct {
|
||||
List []PlayletThemeItem `json:"list"` // 全部已启用题材
|
||||
}
|
||||
|
||||
// PlayletThemeItem 短剧题材列表项
|
||||
type PlayletThemeItem struct {
|
||||
Id int64 `json:"id"` // 题材ID
|
||||
Name string `json:"name"` // 题材名称
|
||||
Slug string `json:"slug"` // 题材slug
|
||||
Icon string `json:"icon"` // 题材图标完整地址
|
||||
Rank int64 `json:"rank"` // 排序值,越大越靠前
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // SEO信息
|
||||
}
|
||||
|
||||
// PlayletGuessListReq 猜你喜欢短剧分页请求
|
||||
type PlayletGuessListReq struct {
|
||||
pubdef.Page
|
||||
Id int64 `form:"id" json:"id"` // 当前短剧ID,可选,传入时排除该短剧
|
||||
}
|
||||
|
||||
// PlayletGuessListResp 猜你喜欢短剧分页响应
|
||||
type PlayletGuessListResp struct {
|
||||
response.BasePageResp
|
||||
}
|
||||
|
||||
// PlayletListReq 短剧列表分页请求
|
||||
type PlayletListReq struct {
|
||||
pubdef.Page
|
||||
ThemeId int64 `form:"themeId" json:"themeId"` // 题材 ID,不传查询全部题材
|
||||
ThemeSlug string `form:"themeSlug" json:"themeSlug"` // 题材 slug,非空时优先于 themeId
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序方式,1 热度优先,2 最近更新,默认 1
|
||||
}
|
||||
|
||||
// PlayletListResp 短剧与广告混排分页响应
|
||||
type PlayletListResp struct {
|
||||
response.BasePageResp
|
||||
Advitise AdvitiseData `json:"advitise"` // 完整广告配置
|
||||
}
|
||||
|
||||
// PlayletContentListReq 短剧或视频分页请求
|
||||
type PlayletContentListReq struct {
|
||||
pubdef.Page
|
||||
DataType int64 `form:"dataType" json:"dataType"` // 数据类型,1 短剧,2 视频,必填
|
||||
}
|
||||
|
||||
// PlayletContentListResp 短剧或视频与广告混排分页响应
|
||||
type PlayletContentListResp struct {
|
||||
response.BasePageResp
|
||||
Advitise AdvitiseData `json:"advitise"` // 完整广告配置,postList 为列表广告
|
||||
}
|
||||
|
||||
// PlayletContentListItem 短剧、视频或广告列表项
|
||||
type PlayletContentListItem struct {
|
||||
Type int64 `json:"type"` // 数据类型,0 内容,2 广告
|
||||
Playlet *PlayletData `json:"playlet,omitempty"` // 短剧数据,dataType 为 1 且 type 为 0 时返回
|
||||
Video *VideoInfo `json:"video,omitempty"` // 视频数据,dataType 为 2 且 type 为 0 时返回
|
||||
Advertise *proxy.AdvitiseInfo `json:"advertise,omitempty"` // 广告数据,type 为 2 时返回
|
||||
}
|
||||
|
||||
// PlayletRecommendListItem 推荐短剧列表项,type=0时返回短剧信息,type=2时返回广告信息
|
||||
type PlayletRecommendListItem struct {
|
||||
Type PlayletRecommendItemType `json:"type"` // 数据类型 0-短剧数据 2-广告
|
||||
AdvertiseId string `json:"advertiseId"` // 广告ID,type=2时返回
|
||||
Link string `json:"link"` // 广告跳转链接,type=2时返回
|
||||
Location int64 `json:"location"` // 广告位置ID,type=2时返回
|
||||
PlayletData
|
||||
}
|
||||
|
||||
// LikePlayletReq 点赞或取消点赞短剧请求
|
||||
type LikePlayletReq struct {
|
||||
// ID
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
// 是否点赞,true-点赞、false-取消点赞
|
||||
IsLike *bool `json:"isLike" binding:"required"`
|
||||
}
|
||||
|
||||
// LikePlayletResp 点赞或取消点赞短剧响应
|
||||
type LikePlayletResp struct {
|
||||
// ID
|
||||
Id int64 `json:"id"`
|
||||
// 是否点赞,true-点赞、false-取消点赞
|
||||
IsLike bool `json:"isLike"`
|
||||
// 点赞展示数
|
||||
LikeNum int64 `json:"likeNum"`
|
||||
}
|
||||
|
||||
// QueryLikePlayletListReq 当前用户点赞短剧分页请求
|
||||
type QueryLikePlayletListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// QueryLikePlayletListResp 当前用户点赞短剧分页响应
|
||||
type QueryLikePlayletListResp struct {
|
||||
response.BasePageResp
|
||||
Translations AppTranslationInfo `json:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
|
||||
}
|
||||
|
||||
// PlayletHistoryListReq 当前用户短剧观看历史分页请求
|
||||
type PlayletHistoryListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletHistoryReportReq 播放成功后上报短剧观看历史
|
||||
type PlayletHistoryReportReq struct {
|
||||
PlayletID int64 `json:"playletId" binding:"required,gt=0"` // 短剧 ID
|
||||
ChapterID int64 `json:"chapterId" binding:"required,gt=0"` // 当前播放剧集 ID
|
||||
}
|
||||
|
||||
// PlayletHistoryReportResp 短剧观看历史上报响应
|
||||
type PlayletHistoryReportResp struct {
|
||||
WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
|
||||
}
|
||||
|
||||
// PlayletHistoryListResp 当前用户短剧观看历史分页响应
|
||||
type PlayletHistoryListResp struct {
|
||||
response.BasePageResp
|
||||
Translations AppTranslationInfo `json:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
|
||||
}
|
||||
|
||||
// PlayletHistoryItem 短剧观看历史卡片
|
||||
type PlayletHistoryItem struct {
|
||||
PlayletData
|
||||
ChapterID int64 `json:"chapterId"` // 最近观看剧集 ID
|
||||
Episode int64 `json:"episode"` // 最近观看集数
|
||||
WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
|
||||
}
|
||||
|
||||
// PlayletHistoryClearResp 清空短剧观看历史响应
|
||||
type PlayletHistoryClearResp struct {
|
||||
DeletedCount int64 `json:"deletedCount"` // 删除记录数
|
||||
}
|
||||
|
||||
// FavoritePlayletReq 收藏或取消收藏短剧请求
|
||||
type FavoritePlayletReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 短剧ID
|
||||
IsFavorite *bool `json:"isFavorite" binding:"required"` // true 收藏 false 取消收藏
|
||||
}
|
||||
|
||||
// FavoritePlayletResp 收藏或取消收藏短剧响应
|
||||
type FavoritePlayletResp struct {
|
||||
Id int64 `json:"id"` // 短剧ID
|
||||
IsFavorite bool `json:"isFavorite"` // 当前收藏状态
|
||||
CountCollect int64 `json:"countCollect"` // 收藏展示数
|
||||
}
|
||||
|
||||
// QueryFavoritePlayletListReq 当前用户收藏短剧分页请求
|
||||
type QueryFavoritePlayletListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// QueryFavoritePlayletListResp 当前用户收藏短剧分页响应
|
||||
type QueryFavoritePlayletListResp struct {
|
||||
response.BasePageResp
|
||||
Translations AppTranslationInfo `json:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
|
||||
}
|
||||
|
||||
// PlayletRankReq 短剧排行榜分页请求
|
||||
type PlayletRankReq struct {
|
||||
pubdef.Page
|
||||
// 榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜
|
||||
RankPeriod int64 `json:"rankPeriod" form:"rankPeriod"`
|
||||
}
|
||||
|
||||
// PlayletRankPeriod 短剧排行榜类型
|
||||
type PlayletRankPeriod int64
|
||||
|
||||
const (
|
||||
PlayletRankPeriodLike PlayletRankPeriod = 0 // 点赞榜,按累计点赞量降序,最多50条,不足随机补位
|
||||
PlayletRankPeriodWeek PlayletRankPeriod = 1 // 周榜,按本周观看量降序,最多50条,不足随机补位
|
||||
PlayletRankPeriodMonth PlayletRankPeriod = 2 // 月榜,按本月观看量降序,最多50条,不足随机补位
|
||||
PlayletRankPeriodYear PlayletRankPeriod = 3 // 年榜,按本年观看量降序,最多50条,不足随机补位
|
||||
)
|
||||
|
||||
// PlayletRankResp 短剧排行榜分页响应
|
||||
type PlayletRankResp struct {
|
||||
response.BasePageResp
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
}
|
||||
|
||||
// PlayletRankData 短剧排行榜数据
|
||||
type PlayletRankData struct {
|
||||
PlayletData
|
||||
// 榜单分数,点赞榜为累计展示点赞数,周/月/年榜为对应周期观看数
|
||||
RankScore int64 `json:"rankScore"`
|
||||
}
|
||||
|
||||
// QueryPlayletInfoReq 短剧详情请求
|
||||
type QueryPlayletInfoReq struct {
|
||||
// 短剧ID
|
||||
Id int64 `json:"id" form:"id"`
|
||||
// 短剧slug,非空时优先按slug解析短剧ID
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
}
|
||||
|
||||
// QueryPlayletInfoResp 短剧详情响应
|
||||
type QueryPlayletInfoResp struct {
|
||||
FreeEpisodeCount int64 `json:"freeEpisodeCount"` // 官网开放集数 1至1000表示开放第1集到第N集 -1表示开放全集 未配置按2集返回
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 公告
|
||||
Announcement AnnouncementInfo `json:"announcement"`
|
||||
// 短剧详情数据
|
||||
PlayletInfo PlayletDetailInfo `json:"playletInfo"`
|
||||
// 剧集列表
|
||||
ChapterList []PlayletChapterInfo `json:"chapterList"`
|
||||
// 当前登录用户是否已点赞该短剧
|
||||
IsLike bool `json:"isLike"`
|
||||
// 当前登录用户是否已收藏该短剧
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
}
|
||||
|
||||
// QueryPlayletChapterInfoReq 短剧剧集详情请求
|
||||
type QueryPlayletChapterInfoReq struct {
|
||||
// 剧集ID
|
||||
Id int64 `json:"id" form:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// QueryPlayletChapterInfoResp 短剧剧集详情响应
|
||||
type QueryPlayletChapterInfoResp struct {
|
||||
FreeEpisodeCount int64 `json:"freeEpisodeCount"` // 官网开放集数 1至1000表示开放第1集到第N集 -1表示开放全集 未配置按2集返回
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 公告
|
||||
Announcement AnnouncementInfo `json:"announcement"`
|
||||
// 当前剧集详情
|
||||
ChapterInfo PlayletChapterInfo `json:"chapterInfo"`
|
||||
// 所属短剧详情
|
||||
PlayletInfo PlayletData `json:"playletInfo"`
|
||||
// 剧集列表
|
||||
ChapterList []PlayletChapterInfo `json:"chapterList"`
|
||||
// 热门短剧列表
|
||||
HotPlayletList []PlayletData `json:"hotPlayletList"`
|
||||
// 当前登录用户是否已点赞该短剧
|
||||
IsLike bool `json:"isLike"`
|
||||
// 当前登录用户是否已收藏该短剧
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
}
|
||||
|
||||
// PlayletDetailInfo 短剧详情数据
|
||||
type PlayletDetailInfo struct {
|
||||
PlayletData
|
||||
// 评论数量
|
||||
CountComment int64 `json:"countComment"`
|
||||
// 当前登录用户是否已点赞该短剧
|
||||
IsLike bool `json:"isLike"`
|
||||
// 当前登录用户是否已收藏该短剧
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
}
|
||||
|
||||
// PlayletChapterInfo 短剧剧集数据
|
||||
type PlayletChapterInfo struct {
|
||||
IsOfficialOpen *bool `json:"isOfficialOpen,omitempty"` // 本集是否在官网开放
|
||||
Id int64 `json:"id"` // 剧集ID
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
MediaContentId uint `json:"mediaContentId"` // 媒资中心章节ID
|
||||
CurrentEpisode int64 `json:"currentEpisode"` // 第几集
|
||||
Title string `json:"title"` // 章节名
|
||||
Md5 string `json:"md5"` // 媒体md5
|
||||
Hash string `json:"hash"` // 媒体hash
|
||||
Height int64 `json:"height"` // 高
|
||||
Width int64 `json:"width"` // 宽
|
||||
Ratio float64 `json:"ratio"` // 宽高比
|
||||
VideoUrl string `json:"videoUrl"` // 视频播放地址,优先返回水印地址
|
||||
MediaSize int64 `json:"mediaSize"` // 资源大小
|
||||
MediaTime int64 `json:"mediaTime"` // 影片长度(秒)
|
||||
CountBrowse int64 `json:"countBrowse"` // 浏览数
|
||||
CountCollect int64 `json:"countCollect"` // 收藏数
|
||||
CountLike int64 `json:"countLike"` // 喜欢数
|
||||
CountComment int64 `json:"countComment"` // 评论数
|
||||
PayType playletchaptermod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
|
||||
SingleCoins int64 `json:"singleCoins"` // 单集金币
|
||||
WatermarkStatus playletchaptermod.WatermarkStatus `json:"watermarkStatus"` // 水印状态 0-未处理 1-已登记 2-处理中 3-已完成
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updatedAt"` // 修改时间
|
||||
}
|
||||
|
||||
// PlayletData 短剧数据
|
||||
type PlayletData struct {
|
||||
Id int64 `json:"id"` // 短剧id
|
||||
Status playletmod.Status `json:"status"` // 状态 0-下架 1-上架
|
||||
IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐 false-不推荐
|
||||
IsNew bool `json:"isNew"` // 是否上新 本周内上架为true
|
||||
IsFeatured bool `json:"isFeatured"` // 是否爆剧 后台设置
|
||||
IsOriginal bool `json:"isOriginal"` // 是否原创 后台新增或无媒资中心ID
|
||||
ShowOriginal bool `json:"showOriginal"` // 页面是否展示原创标识
|
||||
OnlineAt time.Time `json:"onlineAt"` // 上架时间
|
||||
MediaCreatedAt time.Time `json:"mediaCreatedAt"` // 媒资中心创建时间
|
||||
MediaUpdatedAt time.Time `json:"mediaUpdatedAt"` // 媒资中心更新时间
|
||||
Title string `json:"title"` // 标题
|
||||
Slug string `json:"slug"` // slug
|
||||
Desc string `json:"desc"` // 简介
|
||||
Anchor string `json:"anchor"` // 作者或演员
|
||||
Cover string `json:"cover"` // 封面,已拼接图片域名
|
||||
Preview string `json:"preview"` // 预告,已拼接图片域名
|
||||
TagIds []int64 `json:"tagIds"` // 标签ID集合
|
||||
TagNames []string `json:"tagNames"` // 标签名称集合
|
||||
TagSlugs []string `json:"tagSlugs"` // 标签slug集合 与tagNames顺序一致
|
||||
ThemeIds []int64 `json:"themeIds"` // 题材ID集合
|
||||
ThemeNames []string `json:"themeNames"` // 题材名称集合
|
||||
ContentType playletmod.ContentType `json:"contentType"` // 内容类型 0-短剧 1-普通短剧影集 2-漫画短剧 3-动漫 4-漫画
|
||||
PayType playletmod.PayType `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
|
||||
SingleCoins int64 `json:"singleCoins"` // 单集金币,支付方式为金币时生效
|
||||
TotalEpisode int64 `json:"totalEpisode"` // 总集数
|
||||
UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
|
||||
CountBrowse int64 `json:"countBrowse"` // 观看展示数
|
||||
HeatValue int64 `json:"heatValue"` // 热度值
|
||||
CountCollect int64 `json:"countCollect"` // 收藏展示数
|
||||
CountLike int64 `json:"countLike"` // 点赞展示数
|
||||
VirtualCountLike int64 `json:"virtualCountLike"` // 后台虚拟点赞数
|
||||
DisplayCountLike int64 `json:"displayCountLike"` // 点赞展示数
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updatedAt"` // 修改时间
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
// HomePlayletResponse 短剧首页数据。
|
||||
type HomePlayletResponse struct {
|
||||
HomeBannerData []*HomeBannerData `json:"homeBannerData"`
|
||||
Modules []*HomeModuleData `json:"modules"`
|
||||
Announcements []*AnnouncementItem `json:"announcements"`
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo,omitempty"`
|
||||
}
|
||||
|
||||
type HomeBannerData struct {
|
||||
PlayletData
|
||||
ShowOnBanner bool `json:"showOnBanner"`
|
||||
Banner string `json:"banner"`
|
||||
}
|
||||
|
||||
// HomeModuleData.List 根据 dataType 返回短剧或视频列表。
|
||||
type HomeModuleData struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"` // 页面路径标识
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Label string `json:"label"`
|
||||
Style string `json:"style"`
|
||||
CardBackgroundImage string `json:"cardBackgroundImage"`
|
||||
ModuleType int64 `json:"moduleType"`
|
||||
DataType int64 `json:"dataType"`
|
||||
RecommendType int64 `json:"recommendType"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
Rank int64 `json:"rank"`
|
||||
List any `json:"list"`
|
||||
TopicList []*HomeTopicData `json:"topicList"`
|
||||
}
|
||||
|
||||
type HomeTopicData struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Icon string `json:"icon"`
|
||||
Rank int64 `json:"rank"`
|
||||
PlayletCount int64 `json:"playletCount"`
|
||||
VideoCount int64 `json:"videoCount,omitempty"`
|
||||
List any `json:"list"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
}
|
||||
|
||||
type HomeVideoData struct {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Cover string `json:"cover"`
|
||||
Desc string `json:"desc"`
|
||||
Tags []int64 `json:"tags"`
|
||||
WatchNum int64 `json:"watchNum"`
|
||||
LikeNum int64 `json:"likeNum"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
}
|
||||
|
||||
type AnnouncementItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// PlayletHomeConfigListReq 首页配置内容分页请求
|
||||
type PlayletHomeConfigListReq struct {
|
||||
// pageNum 默认 1,pageSize 默认 20、最大 100
|
||||
pubdef.Page
|
||||
Id int64 `form:"id" json:"id"` // 首页配置 ID,与 slug 二选一
|
||||
Slug string `form:"slug" json:"slug"` // 页面路径标识,和 ID 同时传时优先使用
|
||||
}
|
||||
|
||||
// PlayletHomeConfigListResp 首页配置内容分页响应
|
||||
type PlayletHomeConfigListResp struct {
|
||||
Config *HomeModuleData `json:"config"` // 当前首页配置,不包含首页预览内容
|
||||
TotalCount int64 `json:"totalCount"` // 符合配置条件的内容或专题总数
|
||||
PageSize int `json:"pageSize"` // 每页条数
|
||||
PageNum int `json:"pageNum"` // 当前页码
|
||||
List any `json:"list"` // 普通模块内容,dataType 为 1 时是短剧,为 2 时是视频
|
||||
TopicList []*HomeTopicData `json:"topicList"` // 专题模块内容,短剧按题材分组,视频按分类分组
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告配置
|
||||
}
|
||||
|
||||
// PlayletTopicDetailReq 短剧题材或视频分类专题详情请求
|
||||
type PlayletTopicDetailReq struct {
|
||||
// pageNum 默认 1,pageSize 默认 12、最大 100
|
||||
pubdef.Page
|
||||
Id int64 `form:"id" json:"id"` // 专题 ID,dataType 为 1 时是题材 ID,为 2 时是视频分类 ID
|
||||
TopicSlug string `form:"topicSlug" json:"topicSlug"` // 专题 slug,非空时优先于 id
|
||||
DataType int64 `form:"dataType" json:"dataType"` // 数据类型,1 为短剧题材,2 为视频分类,默认 1
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序方式,1 编辑排序,2 最新,3 最热,默认 1
|
||||
}
|
||||
|
||||
// PlayletTopicDetailResp 短剧题材或视频分类专题详情响应
|
||||
type PlayletTopicDetailResp struct {
|
||||
Id int64 `json:"id"` // 专题 ID
|
||||
DataType int64 `json:"dataType"` // 数据类型,1 为短剧题材,2 为视频分类
|
||||
Name string `json:"name"` // 专题名称
|
||||
Slug string `json:"slug"` // 专题 slug
|
||||
Desc string `json:"desc"` // 专题描述
|
||||
Icon string `json:"icon"` // 题材图标地址,视频分类专题为空
|
||||
Rank int64 `json:"rank"` // 专题排序值
|
||||
PlayletCount int64 `json:"playletCount"` // 已上架短剧数量,视频专题为 0
|
||||
VideoCount int64 `json:"videoCount,omitempty"` // 审核通过的视频数量,为 0 时省略
|
||||
Covers []string `json:"covers"` // 顶部封面,最多 3 张
|
||||
TotalCount int64 `json:"totalCount"` // 专题内符合条件的内容总数
|
||||
PageSize int `json:"pageSize"` // 每页条数
|
||||
PageNum int `json:"pageNum"` // 当前页码
|
||||
List any `json:"list"` // 当前页内容,dataType 为 1 时是短剧,为 2 时是视频
|
||||
MoreTopics []*HomeTopicData `json:"moreTopics"` // 同数据类型的其他专题,最多 3 个
|
||||
SeoInfo *pubdef.SeoValue `json:"seoInfo"` // 专题 SEO 信息
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告配置
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/models/questionnairemod"
|
||||
|
||||
// QuestionnaireSubmitReq App端提交问卷请求
|
||||
type QuestionnaireSubmitReq struct {
|
||||
UserId int64 `json:"userId"` // 登录用户ID,0表示匿名(选填)
|
||||
QuestionType string `json:"questionType" binding:"required"` // 问题类型:播放卡顿/缓冲、无法播放、画质模糊、音画不同步、加载失败、闪退/崩溃、登录异常、其他问题
|
||||
Frequency string `json:"frequency" binding:"required"` // 出现频率:每次都会、经常出现、偶尔出现、仅现一次
|
||||
Province string `json:"province" binding:"required"` // 所在省份(必填)
|
||||
City string `json:"city"` // 所在城市(选填)
|
||||
Operator string `json:"operator" binding:"required"` // 运营商:电信、联通、移动、广电
|
||||
NetworkType string `json:"networkType" binding:"required"` // 网络类型:WI-FI、5G、4G、其他
|
||||
DeviceType string `json:"deviceType" binding:"required"` // 设备类型:iPhone、Android、iPad、其他
|
||||
DeviceModel string `json:"deviceModel"` // 设备型号(选填)
|
||||
Description string `json:"description" binding:"required"` // 问题描述(必填)
|
||||
Contact string `json:"contact"` // 联系方式(选填)
|
||||
Attachments []questionnairemod.Attachment `json:"attachments"` // 附件列表,最多3个(选填)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
type RichTextInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"`
|
||||
Title string `json:"title" bson:"title"` // 标题
|
||||
Content string `json:"content" bson:"content"` // 内容
|
||||
Desc string `json:"desc" bson:"desc"` // 标题(app)
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// AppRichTextItemResp APP富文本内容列表项
|
||||
type AppRichTextItemResp struct {
|
||||
Id int64 `json:"id"` // 富文本ID
|
||||
Title string `json:"title"` // 标题
|
||||
Content string `json:"content"` // 内容
|
||||
Desc string `json:"desc"` // 标题(app)
|
||||
}
|
||||
|
||||
// AppRichTextListResp APP富文本内容列表响应
|
||||
type AppRichTextListResp struct {
|
||||
List []AppRichTextItemResp `json:"list"` // 富文本内容列表
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package proto
|
||||
|
||||
type SearchSortType string
|
||||
|
||||
const (
|
||||
SearchSortTypeDefault SearchSortType = "default" // 默认 相似度
|
||||
SearchSortTypeHot SearchSortType = "hot" // 热度排序
|
||||
SearchSortTypeNew SearchSortType = "new" // 最新排序
|
||||
)
|
||||
|
||||
// RankType 排行榜类型
|
||||
type RankType int64
|
||||
|
||||
const (
|
||||
RankType_Video = 1 // 视频
|
||||
RankType_Actress = 2 // 女优
|
||||
RankType_Filmreview = 3 // 影评
|
||||
)
|
||||
|
||||
// RankPeriod 排行榜周期
|
||||
type RankPeriod int64
|
||||
|
||||
const (
|
||||
RankPeriod_Week RankPeriod = 1
|
||||
RankPeriod_Month RankPeriod = 2
|
||||
RankPeriod_Year RankPeriod = 3
|
||||
)
|
||||
|
||||
type SearchRankHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
VideoWeek SearchRankData `json:"videoWeek"`
|
||||
ActressWeek SearchRankData `json:"actressWeek"`
|
||||
FilmreviewWeek SearchRankData `json:"filmreviewWeek"`
|
||||
VideoMonth SearchRankData `json:"videoMonth"`
|
||||
ActressMonth SearchRankData `json:"actressMonth"`
|
||||
FilmreviewMonth SearchRankData `json:"filmreviewMonth"`
|
||||
VideoYear SearchRankData `json:"videoYear"`
|
||||
ActressYear SearchRankData `json:"actressYear"`
|
||||
FilmreviewYear SearchRankData `json:"filmreviewYear"`
|
||||
}
|
||||
type SearchRankData struct {
|
||||
RankName string `json:"rankName" form:"rankName"` // 排行榜名字
|
||||
RankType RankType `json:"rankType" form:"rankType"` // 排行榜类型 1视频, 2女优 ,3影评
|
||||
RankPeriod RankPeriod `json:"rankPeriod" form:"rankPeriod"` // 排行榜周期 1=周 2=月 3=年
|
||||
List []HotSearchItem `json:"list" form:"list"`
|
||||
}
|
||||
|
||||
type HotSearchItem struct {
|
||||
Id int64 `json:"id" form:"id"` // ID
|
||||
Title string `json:"title" form:"title"` //标题
|
||||
HotValue int64 `json:"hotValue" form:"hotValue"` // 热度值
|
||||
}
|
||||
|
||||
type SearchResp struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
InTitle string `json:"inTitle"` //输入标题
|
||||
InType int64 `json:"inType"` // 输入类型 -1=全部 0=视频 6=短剧
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Sort []SearchSortItem `json:"sorts"` // 排序选项
|
||||
SearchRecommend []string `json:"searchRecommend"` // 搜索推荐
|
||||
Media []VideoInfoData `json:"media" form:"media"` // 视频资源
|
||||
Playlets []PlayletData `json:"playlets" form:"playlets"` // 短剧资源
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
}
|
||||
|
||||
type SearchSortItem struct {
|
||||
Key string `json:"key"` // 值
|
||||
Title string `json:"title"` // 标题
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
type AppTranslationInfo struct {
|
||||
CategoryList []TranslationCategory `json:"categoryList" form:"categoryList"`
|
||||
}
|
||||
|
||||
type TranslationCategory struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name" bson:"name"` // 名称
|
||||
Icon string `json:"icon" bson:"icon"` // Icon
|
||||
TranslationList []TranslationInfo `json:"translationList" form:"translationList"`
|
||||
UrlValue string `json:"urlValue" bson:"urlValue"` // URl路径
|
||||
CategoryType pubdef.TranslationType `json:"categoryType"` // 导航类型
|
||||
}
|
||||
|
||||
type TranslationInfo struct {
|
||||
ID int64 `json:"id"` // ID
|
||||
Name string `json:"name" bson:"name"` // 名称
|
||||
IsFriendAds bool `json:"isFriendAds"` // 是否是友链广告位
|
||||
UrlValue string `json:"urlValue" bson:"urlValue"` // URl路径
|
||||
TransCategoryId int64 `json:"-"` // 导航分类
|
||||
DisplayMode pubdef.DisplayMode `json:"displayMode"` // 展示方式
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// latestTime 返回两个时间中较大的那个
|
||||
func latestTime(a, b time.Time) time.Time {
|
||||
if b.After(a) {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
type TagsInfoData struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"` // 帖子标题
|
||||
Slug string `json:"slug"` // slug
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Works int64 `json:"works" bson:"works,omitempty"` // 作品数量
|
||||
Images string `json:"images" bson:"images,omitempty"` // 背景
|
||||
}
|
||||
type CollectionInfoData struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"` // 帖子标题
|
||||
}
|
||||
type VideoType int64
|
||||
|
||||
const (
|
||||
VideoTypeVideo VideoType = 0 // 视频
|
||||
VideoTypeCollection VideoType = 1 // 合集
|
||||
VideoTypeAdvitise VideoType = 2 // 广告
|
||||
VideoType_AV VideoType = 3 // AV视频
|
||||
)
|
||||
|
||||
type VideoInfoData struct {
|
||||
Id int64 `json:"id"`
|
||||
Type VideoType `json:"type"` // 类型
|
||||
Title string `json:"title"` // 帖子标题
|
||||
Slug string `json:"slug"` // slug
|
||||
Keyworld []string `json:"keyworld"` // 关键字
|
||||
AddedTime time.Time `json:"addedTime"` // 帖子上架时间
|
||||
CreateInfo PostCreateInfo `json:"createInfo"` // 作者信息
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
// Nodes []videoinformationmod.Node `json:"nodes"` // 节点
|
||||
Video videoinformationmod.VideoInfo `json:"video"` // 视频
|
||||
Tags []TagsInfoData `json:"tags"` // 标签
|
||||
IsHot bool `json:"isHot"` // 是否热门
|
||||
AdvitiseId string `json:"advitiseId"` // 广告Id
|
||||
CollectionInfo []VideoInfoData `json:"collectionInfo"` // 合集数据
|
||||
Link string `json:"link"` // 跳转链接
|
||||
WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
|
||||
LikeNum int64 `json:"likeNum,omitempty"` // 点赞展示数
|
||||
CountCollect int64 `json:"countCollect,omitempty"` // 收藏展示数
|
||||
ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
|
||||
Location int64 `json:"location" bson:"location"` // 广告位置id
|
||||
ActressInfo []ActressInfoData `json:"actressInfo" bson:"actressInfo"` // 女优信息
|
||||
IssuedAt time.Time `json:"issuedAt" bson:"issuedAt"` // 发行时间
|
||||
IssuedBrand string `json:"issuedBrand" bson:"issuedBrand"` // 发行商
|
||||
Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
|
||||
Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
|
||||
UpdatedAt time.Time `json:"updatedAt"` // 页面更新时间
|
||||
PreviewVtt string `json:"previewVtt,omitempty"` // 雪碧图 vtt
|
||||
}
|
||||
|
||||
// 面包屑
|
||||
type BreadcrumbItem struct {
|
||||
Name string `json:"name"` // 名称
|
||||
Url string `json:"url"` // 跳转链接
|
||||
}
|
||||
|
||||
// 详情页导航
|
||||
type DetailNavItem struct {
|
||||
Name string `json:"name"` // 导航名称
|
||||
Url string `json:"url"` // 导航链接
|
||||
}
|
||||
|
||||
type VideoHistoryInfo struct {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"` // 帖子标题
|
||||
AddedTime time.Time `json:"addedTime"` // 上架时间
|
||||
}
|
||||
|
||||
type VideoCategoryInfo struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"` // slug
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Title string `json:"title" bson:"title"` // APP显示
|
||||
Desc string `json:"desc" bson:"desc"` // 描述
|
||||
}
|
||||
|
||||
// VideoCategoryListResp 已启用视频分类列表响应
|
||||
type VideoCategoryListResp struct {
|
||||
List []VideoCategoryListItem `json:"list"` // 全部已启用视频分类
|
||||
}
|
||||
|
||||
// VideoCategoryListItem 视频分类列表项
|
||||
type VideoCategoryListItem struct {
|
||||
Id int64 `json:"id"` // 分类ID
|
||||
Name string `json:"name"` // 分类名称
|
||||
Slug string `json:"slug"` // 分类slug
|
||||
Title string `json:"title"` // APP展示标题
|
||||
Desc string `json:"desc"` // 分类描述
|
||||
Rank int64 `json:"rank"` // 排序值,越大越靠前
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // SEO信息
|
||||
}
|
||||
|
||||
type VideoPlayReq struct {
|
||||
VideoId int64 `json:"videoId" form:"videoId"` // 视频ID
|
||||
}
|
||||
|
||||
type VideoPlayResp struct {
|
||||
AuthKey string `json:"authKey"`
|
||||
}
|
||||
|
||||
type VideoCreaterInfo struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"` // slug
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// QueryVideoReq 根据分类id查询视频列表请求
|
||||
type QueryVideoReq struct {
|
||||
pubdef.Page
|
||||
// 视频分类id
|
||||
CategoryId int64 `json:"categoryId" form:"categoryId"`
|
||||
// 分类slug,非空时优先按slug解析分类ID
|
||||
CategorySlug string `json:"categorySlug" form:"categorySlug"`
|
||||
}
|
||||
|
||||
// QueryVideoItemType 视频更多列表项类型
|
||||
type QueryVideoItemType int64
|
||||
|
||||
const (
|
||||
// QueryVideoItemTypeVideo 表示视频数据
|
||||
QueryVideoItemTypeVideo QueryVideoItemType = 0
|
||||
// QueryVideoItemTypeAdvertise 表示广告数据
|
||||
QueryVideoItemTypeAdvertise QueryVideoItemType = 2
|
||||
)
|
||||
|
||||
// QueryVideoResp 视频更多分页响应
|
||||
type QueryVideoResp struct {
|
||||
response.BasePageResp
|
||||
// 当前分类信息(含 SEO)
|
||||
CategoryInfo VideoCategoryInfo `json:"categoryInfo"`
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
}
|
||||
|
||||
// QueryVideoListItem 视频更多列表项,type=0时返回视频信息,type=2时返回广告信息。
|
||||
type QueryVideoListItem struct {
|
||||
// 数据类型:0-视频数据、2-广告
|
||||
Type QueryVideoItemType `json:"type"`
|
||||
// 广告ID,type=2时返回
|
||||
AdvertiseId string `json:"advertiseId"`
|
||||
// 广告跳转链接,type=2时返回
|
||||
Link string `json:"link"`
|
||||
// 广告位置ID,type=2时返回
|
||||
Location int64 `json:"location"`
|
||||
VideoInfo
|
||||
}
|
||||
|
||||
// VideoRecommendListReq 推荐视频分页请求
|
||||
type VideoRecommendListReq struct {
|
||||
pubdef.Page
|
||||
// 分类id,不传查询全部分类id
|
||||
CategoryId int64 `form:"categoryId" json:"categoryId"`
|
||||
// 分类slug,非空时优先按slug解析分类ID
|
||||
CategorySlug string `form:"categorySlug" json:"categorySlug"`
|
||||
// 标签ID,不传查询全部标签
|
||||
TagId int64 `form:"tagId" json:"tagId"`
|
||||
// 标签slug,非空时优先按slug解析标签ID
|
||||
TagSlug string `form:"tagSlug" json:"tagSlug"`
|
||||
// 排序方式 1-推荐 2-最新 3-最热
|
||||
SortList int64 `form:"sortList" json:"sortList"`
|
||||
}
|
||||
|
||||
// VideoListReq 上架视频分页请求
|
||||
type VideoListReq struct {
|
||||
pubdef.Page
|
||||
CategoryId int64 `form:"categoryId" json:"categoryId"` // 视频分类 ID,不传查询全部分类
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序方式,1 热度优先,2 最近更新,默认 1
|
||||
}
|
||||
|
||||
// VideoListResp 视频与广告混排分页响应
|
||||
type VideoListResp struct {
|
||||
response.BasePageResp
|
||||
Advitise AdvitiseData `json:"advitise"` // 完整广告配置
|
||||
}
|
||||
|
||||
// VideoRecommendSort 推荐视频排序方式
|
||||
type VideoRecommendSort int64
|
||||
|
||||
const (
|
||||
VideoRecommendSortRecommend VideoRecommendSort = 1 // 推荐 后台配置 isRecommend=true
|
||||
VideoRecommendSortNewest VideoRecommendSort = 2 // 最新 按视频最后更新时间从近到远排序
|
||||
VideoRecommendSortHot VideoRecommendSort = 3 // 最热 按视频累计播放量从高到低排序
|
||||
)
|
||||
|
||||
// VideoRecommendItemType 推荐视频列表项类型
|
||||
type VideoRecommendItemType int64
|
||||
|
||||
const (
|
||||
VideoRecommendItemTypeVideo VideoRecommendItemType = 0 // 视频数据
|
||||
VideoRecommendItemTypeAdvertise VideoRecommendItemType = 2 // 广告数据
|
||||
)
|
||||
|
||||
// VideoRecommendListResp 推荐视频分页响应
|
||||
type VideoRecommendListResp struct {
|
||||
response.BasePageResp
|
||||
}
|
||||
|
||||
// VideoGuessListReq 猜你喜欢视频分页请求
|
||||
type VideoGuessListReq struct {
|
||||
pubdef.Page
|
||||
Id int64 `form:"id" json:"id"` // 当前视频ID,可选,传入时排除该视频
|
||||
}
|
||||
|
||||
// VideoGuessListResp 猜你喜欢视频分页响应
|
||||
type VideoGuessListResp struct {
|
||||
response.BasePageResp
|
||||
}
|
||||
|
||||
// VideoRecommendListItem 推荐视频列表项
|
||||
type VideoRecommendListItem struct {
|
||||
Type VideoRecommendItemType `json:"type"` // 数据类型 0-视频数据 2-广告
|
||||
AdvertiseId string `json:"advertiseId"` // 广告ID,type=2时返回
|
||||
Link string `json:"link"` // 广告跳转链接,type=2时返回
|
||||
Location int64 `json:"location"` // 广告位置ID,type=2时返回
|
||||
VideoInfo
|
||||
}
|
||||
|
||||
// FavoriteVideoReq 收藏或取消收藏视频请求
|
||||
type FavoriteVideoReq struct {
|
||||
// ID
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
// 是否收藏,true-收藏、false-取消收藏
|
||||
IsFavorite *bool `json:"isFavorite" binding:"required"`
|
||||
}
|
||||
|
||||
// FavoriteVideoResp 收藏或取消收藏视频响应
|
||||
type FavoriteVideoResp struct {
|
||||
// ID
|
||||
Id int64 `json:"id"`
|
||||
// 是否收藏,true-收藏、false-取消收藏
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
}
|
||||
|
||||
// LikeVideoReq 点赞或取消点赞视频请求
|
||||
type LikeVideoReq struct {
|
||||
// ID
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
// 是否点赞,true-点赞、false-取消点赞
|
||||
IsLike *bool `json:"isLike" binding:"required"`
|
||||
}
|
||||
|
||||
// LikeVideoResp 点赞或取消点赞视频响应
|
||||
type LikeVideoResp struct {
|
||||
// ID
|
||||
Id int64 `json:"id"`
|
||||
// 是否点赞,true-点赞、false-取消点赞
|
||||
IsLike bool `json:"isLike"`
|
||||
// 点赞展示数
|
||||
LikeNum int64 `json:"likeNum"`
|
||||
}
|
||||
|
||||
// QueryLikeVideoListReq 当前用户点赞视频分页请求
|
||||
type QueryLikeVideoListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// QueryLikeVideoListResp 当前用户点赞视频分页响应
|
||||
type QueryLikeVideoListResp struct {
|
||||
response.BasePageResp
|
||||
Translations AppTranslationInfo `json:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
|
||||
}
|
||||
|
||||
// VideoHistoryListReq 当前用户视频观看历史分页请求
|
||||
type VideoHistoryListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// VideoHistoryReportReq 播放成功后上报视频观看历史
|
||||
type VideoHistoryReportReq struct {
|
||||
Id int64 `json:"id" binding:"required,gt=0"` // 视频 ID
|
||||
}
|
||||
|
||||
// VideoHistoryReportResp 视频观看历史上报响应
|
||||
type VideoHistoryReportResp struct {
|
||||
WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
|
||||
}
|
||||
|
||||
// VideoHistoryListResp 当前用户视频观看历史分页响应
|
||||
type VideoHistoryListResp struct {
|
||||
response.BasePageResp
|
||||
Translations AppTranslationInfo `json:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"` // 底部翻译
|
||||
}
|
||||
|
||||
// VideoHistoryItem 视频观看历史卡片
|
||||
type VideoHistoryItem struct {
|
||||
FavoriteVideoListItem
|
||||
Desc string `json:"desc"` // 视频简介
|
||||
WatchedAt time.Time `json:"watchedAt"` // 最近观看时间
|
||||
}
|
||||
|
||||
// VideoHistoryClearResp 清空视频观看历史响应
|
||||
type VideoHistoryClearResp struct {
|
||||
DeletedCount int64 `json:"deletedCount"` // 删除记录数
|
||||
}
|
||||
|
||||
// QueryFavoriteVideoListReq 当前登录用户收藏视频分页请求
|
||||
type QueryFavoriteVideoListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// QueryFavoriteVideoListResp 当前登录用户收藏视频分页响应
|
||||
type QueryFavoriteVideoListResp struct {
|
||||
response.BasePageResp
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
}
|
||||
|
||||
// FavoriteVideoListItem 当前登录用户收藏视频列表项
|
||||
type FavoriteVideoListItem struct {
|
||||
// id
|
||||
Id int64 `json:"id"`
|
||||
// 标题
|
||||
Title string `json:"title"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// 上架时间
|
||||
AddedTime time.Time `json:"addedTime"`
|
||||
// 封面
|
||||
CoverImg string `json:"coverImg"`
|
||||
// 标签id
|
||||
Tags []int64 `json:"tags"`
|
||||
// 标签名称
|
||||
TagsName []string `json:"tagsName"`
|
||||
// 标签slug 与tagsName顺序一致
|
||||
TagSlugs []string `json:"tagSlugs"`
|
||||
// 观看展示数
|
||||
WatchNum int64 `json:"watchNum"`
|
||||
// 点赞展示数
|
||||
LikeNum int64 `json:"likeNum"`
|
||||
// 收藏展示数
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
}
|
||||
|
||||
// VideoRankReq 视频排行榜分页请求
|
||||
type VideoRankReq struct {
|
||||
pubdef.Page
|
||||
// 榜单类型 0-点赞榜 1-周榜 2-月榜 3-年榜
|
||||
RankPeriod int64 `json:"rankPeriod" form:"rankPeriod"`
|
||||
}
|
||||
|
||||
// VideoRankPeriod 视频排行榜类型
|
||||
type VideoRankPeriod int64
|
||||
|
||||
const (
|
||||
VideoRankPeriodLike VideoRankPeriod = 0 // 点赞榜,按累计点赞量降序,最多50条,不足随机补位
|
||||
VideoRankPeriodWeek VideoRankPeriod = 1 // 周榜,按本周观看量降序,最多50条,不足随机补位
|
||||
VideoRankPeriodMonth VideoRankPeriod = 2 // 月榜,按本月观看量降序,最多50条,不足随机补位
|
||||
VideoRankPeriodYear VideoRankPeriod = 3 // 年榜,按本年观看量降序,最多50条,不足随机补位
|
||||
)
|
||||
|
||||
// VideoRankResp 视频排行榜分页响应
|
||||
type VideoRankResp struct {
|
||||
response.BasePageResp
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
}
|
||||
|
||||
// QueryVideoInfoReq 视频详情请求
|
||||
type QueryVideoInfoReq struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
// 视频slug,非空时优先按slug解析视频ID
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
}
|
||||
|
||||
// QueryVideoInfoResp 视频详情响应
|
||||
type QueryVideoInfoResp struct {
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 导航
|
||||
Translations AppTranslationInfo `json:"translations"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 底部翻译
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 公告
|
||||
Announcement AnnouncementInfo `json:"announcement"`
|
||||
|
||||
// 视频详情数据
|
||||
VideoInfo VideoDetailInfo `json:"videoInfo"`
|
||||
// 当前登录用户是否已收藏该视频
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
// 当前登录用户是否已点赞该视频
|
||||
IsLike bool `json:"isLike"`
|
||||
}
|
||||
|
||||
// VideoDetailInfo 视频详情数据
|
||||
type VideoDetailInfo struct {
|
||||
// id
|
||||
Id int64 `json:"id"`
|
||||
// 标题
|
||||
Title string `json:"title"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// 封面
|
||||
CoverImg string `json:"coverImg"`
|
||||
// 视频播放信息
|
||||
Video videoinformationmod.VideoInfo `json:"video"`
|
||||
// 标签
|
||||
Tags []TagsInfoData `json:"tags"`
|
||||
// 观看展示数
|
||||
WatchNum int64 `json:"watchNum"`
|
||||
// 点赞展示数
|
||||
LikeNum int64 `json:"likeNum"`
|
||||
// 收藏展示数
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
// 上架时间
|
||||
AddedTime time.Time `json:"addedTime"`
|
||||
// 作品简介
|
||||
Desc string `json:"desc"`
|
||||
// 当前登录用户是否已收藏该视频
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
// 当前登录用户是否已点赞该视频
|
||||
IsLike bool `json:"isLike"`
|
||||
}
|
||||
|
||||
func VideoInfoToData(videoInfo *videoinformationmod.VideoInformation, imgCdn string, creatorInfo PostCreateInfo) VideoInfoData {
|
||||
t := VideoTypeVideo
|
||||
switch videoInfo.MediaType {
|
||||
case videoinformationmod.MediaType_Video:
|
||||
t = VideoTypeVideo
|
||||
case videoinformationmod.MediaType_Collection:
|
||||
t = VideoTypeCollection
|
||||
case videoinformationmod.MediaType_Advitise:
|
||||
t = VideoTypeAdvitise
|
||||
case videoinformationmod.MediaType_AV:
|
||||
t = VideoType_AV
|
||||
}
|
||||
videotemp := VideoInfoData{
|
||||
Id: videoInfo.Id,
|
||||
Type: t,
|
||||
Title: videoInfo.Title,
|
||||
Slug: videoInfo.Slug,
|
||||
Keyworld: videoInfo.Keyworld,
|
||||
AddedTime: videoInfo.AddedTime,
|
||||
CreateInfo: creatorInfo,
|
||||
CoverImg: netutil.MergeURL(imgCdn, videoInfo.CoverImg),
|
||||
// Nodes: make([]videoinformationmod.Node, 0),
|
||||
Video: videoInfo.Video,
|
||||
IsHot: videoInfo.IsHot,
|
||||
Tags: make([]TagsInfoData, 0),
|
||||
ShowTimes: videoInfo.Real.ShowTimes*100 + 6241,
|
||||
WatchTimes: videoInfo.Real.WatchTimes*100 + 3152,
|
||||
ActressInfo: make([]ActressInfoData, 0),
|
||||
Desc: videoInfo.Desc,
|
||||
Bango: videoInfo.Bango,
|
||||
IssuedBrand: videoInfo.IssuedBrand,
|
||||
IssuedAt: videoInfo.IssuedAt,
|
||||
UpdatedAt: latestTime(videoInfo.UpdatedAt, videoInfo.PageUpdatedAt),
|
||||
PreviewVtt: videoInfo.PreviewVtt,
|
||||
}
|
||||
if videoInfo.Video.Cover != "" {
|
||||
videotemp.Video.Cover = netutil.MergeURL(imgCdn, videotemp.Video.Cover)
|
||||
}
|
||||
//for _, nodeInfo := range videoInfo.Nodes {
|
||||
// nodeTmp := videoinformationmod.Node{
|
||||
// Images: make([]string, 0),
|
||||
// Url: nodeInfo.Url,
|
||||
// Text: nodeInfo.Text,
|
||||
// Videos: make([]videoinformationmod.VideoInfo, 0),
|
||||
// Title: nodeInfo.Title,
|
||||
// }
|
||||
// for _, image := range nodeInfo.Images {
|
||||
// nodeTmp.Images = append(nodeTmp.Images, netutil.MergeURL(imgCdn, image))
|
||||
// }
|
||||
// if nodeInfo.Videos != nil && len(nodeInfo.Videos) > 0 {
|
||||
// for _, video := range nodeInfo.Videos {
|
||||
// videoTmp := videoinformationmod.VideoInfo{
|
||||
// TaskId: video.TaskId,
|
||||
// FileId: video.FileId,
|
||||
// Url: netutil.MergeURL(imgCdn, video.Url),
|
||||
// Cover: video.Cover,
|
||||
// Height: video.Height,
|
||||
// Width: video.Width,
|
||||
// VideoTime: video.VideoTime,
|
||||
// PreMp4FileNameFs: video.PreMp4FileNameFs,
|
||||
// }
|
||||
// nodeTmp.Videos = append(nodeTmp.Videos, videoTmp)
|
||||
// }
|
||||
// }
|
||||
// videotemp. = append(videotemp.Nodes, nodeTmp)
|
||||
//}
|
||||
return videotemp
|
||||
}
|
||||
|
||||
// StripH265Url 清空主视频 H265 地址。
|
||||
func StripH265Url(data *VideoInfoData) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
data.Video.H265Url = ""
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// HomeHtmlData 首页
|
||||
type HomeHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"` // 请求路径
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
}
|
||||
|
||||
type VideoCategoryData struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Slug string `json:"slug"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"`
|
||||
}
|
||||
|
||||
type VideoCategoryHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
VideoCategory VideoCategoryData `json:"videoCategory" form:"videoCategory"` // 当前分类数据
|
||||
}
|
||||
|
||||
type CategoryVideoListHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
|
||||
CategoryList []VideoCategoryData `json:"categoryList" form:"categoryList"`
|
||||
}
|
||||
|
||||
type VideoCategoryListHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
|
||||
CategoryList []VideoCategoryInfo `json:"categoryList" form:"categoryList"`
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
|
||||
type VideoDetailsHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Video VideoInfoData `json:"video" form:"video"` // 视频详情
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
|
||||
RecommendVideos []VideoInfoData `json:"recommendVideos" form:"recommendVideos"` // 推荐视频
|
||||
ChoiceVideos []VideoInfoData `json:"choiceVideos" form:"choiceVideos"` // 精选视频
|
||||
Breadcrumbs []BreadcrumbItem `json:"breadcrumbs" form:"breadcrumbs"` // 面包屑
|
||||
DetailNavs []DetailNavItem `json:"detailNavs" form:"detailNavs"` // 详情页导航
|
||||
}
|
||||
|
||||
// CreateVideoListHtmlData 创作者视频列表
|
||||
type CreateVideoListHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
CreateInfo PostCreateInfo `json:"createInfo" bson:"createInfo"` // 创作者信息
|
||||
VideoList []VideoInfoData `json:"videoList" form:"videoList"`
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
type PostCreateInfo struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Slug string `json:"slug"` // slug
|
||||
Image string `json:"image" form:"image"` //头像
|
||||
Works int64 `json:"works" form:"works"`
|
||||
CreateType int64 `json:"createType"` // 创作者类型 0=普通 1=官方
|
||||
Desc string `json:"desc" form:"desc"` // 作者描述
|
||||
Background string `json:"background"` // 背景
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// TagHtmlData 首页
|
||||
type TagHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Media []VideoInfoData `json:"media" form:"media"` // 视频资源
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Tag TagsInfoData `json:"tag" form:"tag"` // 标签
|
||||
SortItem []videoinformationmod.VideoSortItem `json:"sortItem" form:"sortItem"` // 排序项
|
||||
SortValue videoinformationmod.VideoSortItem `json:"sortValue" form:"sortValue"` // 当前排序项
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
type VideoTagHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Tags []TagsInfoData `json:"tags" form:"tags"`
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
|
||||
type VideoHistoryHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
Tags []TagsInfoData `json:"tags" form:"tags"` // 标签
|
||||
Media []VideoHistoryInfo `json:"media" form:"media"` // 视频
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
|
||||
type RichTextHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
type NotFoundHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
|
||||
type UrlRedirectData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"` // 公告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Category VideoCategoryInfo `json:"category" form:"category"` // 分类
|
||||
UrlRedirect string `json:"urlRedirect" form:"urlRedirect"`
|
||||
}
|
||||
|
||||
type ContactUsReq struct {
|
||||
Name string `json:"name" form:"name"` // 姓名
|
||||
Email string `json:"email" form:"email"` // 邮件
|
||||
Content string `json:"content" form:"content"` // 内容
|
||||
CategoryName string `json:"categoryName" form:"categoryName"` // 主旨分类
|
||||
}
|
||||
|
||||
// QuestionnaireHtmlData 问卷调查页
|
||||
type QuestionnaireHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
Translation AppTranslationInfo `json:"translation" form:"translation"`
|
||||
Config ConfigInfo `json:"config" form:"config"`
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Announcement AnnouncementInfo `json:"announcement" form:"announcement"`
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package questionnaireapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/searchser"
|
||||
"videoweb/app/service/translationser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Page 问卷调查页
|
||||
func Page(c *gin.Context) {
|
||||
hotSearch := searchser.GetHotSearchItem(c)
|
||||
data := proto.QuestionnaireHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translation: translationser.GetTranslationData(),
|
||||
Config: configser.GetRequestConfig(c),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
Announcement: configser.GetAnnouncement(),
|
||||
HotSearch: hotSearch,
|
||||
}
|
||||
configser.RewriteImageCdn(c, &data)
|
||||
c.HTML(200, "questionnaire", gin.H{
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package questionnaireapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/questionnaireser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Submit 提交问卷
|
||||
// @Tags 问卷调查APP
|
||||
// @Summary 提交问卷
|
||||
// @Description 用户提交问题反馈问卷
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.QuestionnaireSubmitReq true "提交参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/questionnaire/submit [post]
|
||||
func Submit(ctx *gin.Context) {
|
||||
var req proto.QuestionnaireSubmitReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
serr := questionnaireser.Submit(ctx, ctx.ClientIP(), &req)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, nil)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package richtextapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/richtextser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type getByTitleReq struct {
|
||||
Title string `form:"title" binding:"required"`
|
||||
}
|
||||
|
||||
// GetByTitle 按 title 查询富文本
|
||||
// @Tags 富文本
|
||||
// @Summary 按 title 查询富文本
|
||||
// @Description 从缓存中查询启用状态的富文本信息
|
||||
// @Produce json
|
||||
// @Param title query string true "富文本内部标题"
|
||||
// @Success 200 {object} proto.RichTextInfo "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/richtext [get]
|
||||
func GetByTitle(ctx *gin.Context) {
|
||||
var req getByTitleReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
data, serr := richtextser.GetByTitle(req.Title)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, data)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package searchapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/searchser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SearchApi 搜索
|
||||
// @Tags 搜索
|
||||
// @Summary 搜索
|
||||
// @Description 搜索视频和短剧,省略 type 查询全部,type 为 0 查询视频,为 6 查询短剧,sort 支持 default/hot/new
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param title query string false "搜索关键词"
|
||||
// @Param page query int false "页码,默认1"
|
||||
// @Param type query int false "媒体类型,不传=全部,0=视频,6=短剧"
|
||||
// @Param sort query string false "排序方式 default/hot/new,默认default"
|
||||
// @Success 200 {object} proto.SearchResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /search [get]
|
||||
func SearchApi(c *gin.Context) {
|
||||
title := c.Query("title")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
sort := c.DefaultQuery("sort", string(proto.SearchSortTypeDefault))
|
||||
mediaType := int64(-1)
|
||||
if typeStr, ok := c.GetQuery("type"); ok {
|
||||
var err error
|
||||
mediaType, err = strconv.ParseInt(typeStr, 10, 64)
|
||||
if err != nil || (mediaType != 0 && mediaType != 6) {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "type 只支持 0 或 6")
|
||||
return
|
||||
}
|
||||
}
|
||||
page, err := strconv.ParseInt(pageStr, 10, 64)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
data := searchser.Search(c, title, mediaType, page, sort)
|
||||
data.Path = c.Request.URL.Path
|
||||
data.CompletePath = fmt.Sprintf("/search")
|
||||
data.Advitise = configser.GetAdvitise()
|
||||
data.BottonTranslations = translationser.GetTranslationBottomList()
|
||||
data.Translations = translationser.GetTranslationData()
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package searchapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestSearchApiRejectsUnsupportedTypes(t *testing.T) {
|
||||
for _, query := range []string{"type=1", "type=3", "type=2", "type=-1", "type=abc", "type="} {
|
||||
writer := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(writer)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/search/?"+query, nil)
|
||||
SearchApi(ctx)
|
||||
var resp struct {
|
||||
Code int64 `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(writer.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("参数 %s 的响应无法解析: %v", query, err)
|
||||
}
|
||||
if resp.Code != int64(srvdef.ErrParamError) {
|
||||
t.Errorf("参数 %s 返回 code %d, want %d", query, resp.Code, srvdef.ErrParamError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package uploadapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"videoweb/app/service/uploadser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// VideoBody 分片上传视频
|
||||
// @Tags 上传
|
||||
// @Summary 视频分片上传
|
||||
// @Description 将视频分片转发至存储服务 /media/web/autopublish/videoBody
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param taskId formData string true "视频任务ID"
|
||||
// @Param pos formData string true "当前分片序号,从0开始"
|
||||
// @Param uploadUserName formData string false "上传用户名,为空默认 questionnaires"
|
||||
// @Param file formData file true "分片文件数据"
|
||||
// @Success 200 {object} uploadser.VideoBodyResp "成功"
|
||||
// @Failure 400 {string} string "参数错误"
|
||||
// @Router /api/app/upload/videoBody [post]
|
||||
func VideoBody(ctx *gin.Context) {
|
||||
form, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
taskIds := form.Value["taskId"]
|
||||
posList := form.Value["pos"]
|
||||
uploadUserNames := form.Value["uploadUserName"]
|
||||
fileList := form.File["file"]
|
||||
|
||||
if len(taskIds) == 0 || taskIds[0] == "" {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "taskId 不能为空")
|
||||
return
|
||||
}
|
||||
if len(posList) == 0 || posList[0] == "" {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "pos 不能为空")
|
||||
return
|
||||
}
|
||||
if len(fileList) == 0 {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "file 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
param := &uploadser.VideoBodyParam{
|
||||
TaskId: taskIds[0],
|
||||
Pos: posList[0],
|
||||
FileHeader: fileList[0],
|
||||
}
|
||||
if len(uploadUserNames) > 0 {
|
||||
param.UploadUserName = uploadUserNames[0]
|
||||
}
|
||||
|
||||
respBytes, err := uploadser.ForwardVideoBody(param)
|
||||
if err != nil {
|
||||
log.Error("VideoBody forward failed", log.E(err))
|
||||
common.ServerJSON(ctx, srvdef.ServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
|
||||
}
|
||||
|
||||
// VideoOk 通知视频分片上传完成
|
||||
// @Tags 上传
|
||||
// @Summary 视频分片上传完成通知
|
||||
// @Description 所有分片上传完毕后调用,转发至存储服务 /media/web/autopublish/videoOk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body object true "{ \"taskId\": \"...\" }"
|
||||
// @Success 200 {object} uploadser.VideoOkResp "成功"
|
||||
// @Failure 400 {string} string "参数错误"
|
||||
// @Router /api/app/upload/videoOk [post]
|
||||
func VideoOk(ctx *gin.Context) {
|
||||
var req struct {
|
||||
TaskId string `json:"taskId" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := uploadser.ForwardVideoOk(req.TaskId)
|
||||
if err != nil {
|
||||
log.Error("VideoOk forward failed", log.E(err))
|
||||
common.ServerJSON(ctx, srvdef.ServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
|
||||
}
|
||||
|
||||
// UploadImage 上传图片
|
||||
// @Tags 上传
|
||||
// @Summary 图片上传
|
||||
// @Description 将图片转发至存储服务 /media/web/autopublish/image
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "图片文件"
|
||||
// @Success 200 {object} uploadser.ImageUploadResp "成功"
|
||||
// @Failure 400 {string} string "参数错误"
|
||||
// @Router /api/app/upload/image [post]
|
||||
func UploadImage(ctx *gin.Context) {
|
||||
f, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "file 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
param := &uploadser.ImageParam{
|
||||
FileHeader: f,
|
||||
}
|
||||
respBytes, err := uploadser.ForwardImage(param)
|
||||
if err != nil {
|
||||
log.Error("UploadImage forward failed", log.E(err))
|
||||
common.ServerJSON(ctx, srvdef.ServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
package videoapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/slugser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/app/service/videoser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List app端上架视频列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端上架视频列表 -- by 王鸣
|
||||
// @Description 按分类和排序分页查询上架视频,最近更新按更新时间降序,每五条视频后穿插一条广告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoListReq true "视频列表请求"
|
||||
// @Success 200 {object} proto.VideoListResp{list=[]proto.VideoRecommendListItem} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/list [get]
|
||||
func List(c *gin.Context) {
|
||||
var req proto.VideoListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.VideoList(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// CategoryList 查询全部已启用视频分类
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端视频分类列表
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.VideoCategoryListResp "成功"
|
||||
// @Router /api/app/video/category/list [get]
|
||||
func CategoryList(c *gin.Context) {
|
||||
resp, err := videoser.VideoCategoryList(c)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// RecommendList app端推荐视频列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端推荐视频列表 -- by 王鸣
|
||||
// @Description 分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoRecommendListReq true "推荐视频列表请求"
|
||||
// @Success 200 {object} proto.VideoRecommendListResp{list=[]proto.VideoRecommendListItem} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/recommend/list [get]
|
||||
func RecommendList(c *gin.Context) {
|
||||
var req proto.VideoRecommendListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.VideoRecommendList(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// GuessList 分页查询猜你喜欢视频
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 猜你喜欢视频列表 -- by 王鸣
|
||||
// @Description id可选,传入时排除当前视频,按展示观看量降序查询上架视频
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoGuessListReq true "猜你喜欢视频请求"
|
||||
// @Success 200 {object} proto.VideoGuessListResp{list=[]proto.VideoRecommendListItem} "成功"
|
||||
// @Router /api/app/video/guess/list [get]
|
||||
func GuessList(c *gin.Context) {
|
||||
var req proto.VideoGuessListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.VideoGuessList(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// TagList app端根据标签ID查询视频列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端根据标签ID查询视频列表 -- by 王鸣
|
||||
// @Description 根据统一标签ID分页查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Param pageNum query int false "页码,默认1"
|
||||
// @Param pageSize query int false "每页数量,默认20,最大100"
|
||||
// @Success 200 {object} proto.VideoRecommendListResp{list=[]proto.VideoRecommendListItem} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/tag/{id} [get]
|
||||
func TagList(c *gin.Context) {
|
||||
var req proto.ContentTagPageReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
pathID, pathSlug := slugser.ParseIDOrSlug(c.Param("id"))
|
||||
if req.Id <= 0 {
|
||||
req.Id = pathID
|
||||
}
|
||||
if req.Slug == "" {
|
||||
req.Slug = pathSlug
|
||||
}
|
||||
data, err := videoser.VideoTagList(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// QueryVideoMore
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 根据分类id查询视频列表 --by 王鸣
|
||||
// @Description 根据分类id查询视频列表,带穿插列表广告,每间隔8条视频数据插入一个广告
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryVideoReq true "视频更多查询请求"
|
||||
// @Success 200 {object} proto.QueryVideoResp{list=[]proto.QueryVideoListItem{VideoInfo=proto.VideoInfo}} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/more [get]
|
||||
func QueryVideoMore(c *gin.Context) {
|
||||
var req proto.QueryVideoReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.QueryVideoMore(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// QueryVideoInfo 视频详情
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 视频详情 -- by 王鸣
|
||||
// @Description 查询视频详情、收藏状态和点赞状态
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryVideoInfoReq true "视频详情查询请求"
|
||||
// @Success 200 {object} proto.QueryVideoInfoResp{videoInfo=proto.VideoDetailInfo} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/info [get]
|
||||
func QueryVideoInfo(c *gin.Context) {
|
||||
var req proto.QueryVideoInfoReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.QueryVideoInfo(c, common.GetUserId(c), videoser.H5M3U8RequestIP(c), c.Request.UserAgent(), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// FavoriteVideo 收藏或取消收藏视频
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 收藏或取消收藏视频 --by 旺鸣
|
||||
// @Description 当前登录用户收藏或取消收藏指定视频,isFavorite=true表示收藏,isFavorite=false表示取消收藏
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.FavoriteVideoReq true "收藏或取消收藏请求"
|
||||
// @Success 200 {object} proto.FavoriteVideoResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/favorite [post]
|
||||
func FavoriteVideo(c *gin.Context) {
|
||||
var req proto.FavoriteVideoReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(c)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
ua, _ := common.GetUA(c)
|
||||
if ua.UserAgent == "" {
|
||||
ua.UserAgent = c.Request.UserAgent()
|
||||
}
|
||||
data, err := videoser.FavoriteVideo(c, userId, c.ClientIP(), ua, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// LikeVideo 点赞或取消点赞视频
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 点赞或取消点赞视频 -- by 王鸣
|
||||
// @Description 当前登录用户点赞或取消点赞指定视频,isLike=true表示点赞,isLike=false表示取消点赞
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.LikeVideoReq true "点赞或取消点赞请求"
|
||||
// @Success 200 {object} proto.LikeVideoResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/like [post]
|
||||
func LikeVideo(c *gin.Context) {
|
||||
var req proto.LikeVideoReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(c)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
ua, _ := common.GetUA(c)
|
||||
if ua.UserAgent == "" {
|
||||
ua.UserAgent = c.Request.UserAgent()
|
||||
}
|
||||
data, err := videoser.LikeVideo(c, userId, c.ClientIP(), ua, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// QueryVideoRank 视频收藏排行榜
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 视频收藏排行榜
|
||||
// @Description 视频收藏排行榜
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.RankResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/rank [get]
|
||||
func QueryVideoRank(c *gin.Context) {
|
||||
rank := proto.HomeRankData{
|
||||
Total: configser.GetVideoRankList(configser.RankPeriodTotal, 0),
|
||||
Week: configser.GetVideoRankList(configser.RankPeriodWeek, 0),
|
||||
Month: configser.GetVideoRankList(configser.RankPeriodMonth, 0),
|
||||
Year: configser.GetVideoRankList(configser.RankPeriodYear, 0),
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, proto.RankResp{
|
||||
RankData: rank,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
})
|
||||
}
|
||||
|
||||
// QueryVideoRankList 视频排行榜分页列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 视频排行榜分页列表 -- by 王鸣
|
||||
// @Description 分页查询视频排行榜,rankPeriod=0点赞榜(累计点赞)、1周榜/2月榜/3年榜(周期观看量),独立缓存10分钟,每榜最多50条,pageSize不是50时强制为50
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoRankReq true "视频排行榜分页请求"
|
||||
// @Success 200 {object} proto.VideoRankResp{list=[]proto.ContentData} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/video/rank/list [get]
|
||||
func QueryVideoRankList(c *gin.Context) {
|
||||
var req proto.VideoRankReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.QueryVideoRank(c, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// QueryFavoriteVideoList 当前登录用户收藏视频分页列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 当前登录用户收藏视频分页列表 --by 旺鸣
|
||||
// @Description 当前登录用户收藏视频分页列表,按收藏时间倒序排序
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryFavoriteVideoListReq true "收藏视频分页请求"
|
||||
// @Success 200 {object} proto.QueryFavoriteVideoListResp{list=[]proto.FavoriteVideoListItem} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/favorite/list [get]
|
||||
func QueryFavoriteVideoList(c *gin.Context) {
|
||||
var req proto.QueryFavoriteVideoListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(c)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
data, err := videoser.QueryFavoriteVideoList(c, userId, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, "")
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// QueryLikeVideoList 当前用户点赞视频分页列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 当前用户点赞视频分页列表
|
||||
// @Description 按点赞时间倒序返回当前用户点赞的上架视频
|
||||
// @Produce json
|
||||
// @Param params query proto.QueryLikeVideoListReq true "点赞视频分页请求"
|
||||
// @Success 200 {object} proto.QueryLikeVideoListResp{list=[]proto.FavoriteVideoListItem} "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/like/list [get]
|
||||
func QueryLikeVideoList(c *gin.Context) {
|
||||
var req proto.QueryLikeVideoListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
userID := common.GetUserId(c)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
resp, err := videoser.QueryLikeVideoList(c, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ReportVideoHistory 播放成功后记录当前用户的视频
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 上报视频观看历史
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.VideoHistoryReportReq true "当前播放视频"
|
||||
// @Success 200 {object} proto.VideoHistoryReportResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/history [post]
|
||||
func ReportVideoHistory(c *gin.Context) {
|
||||
userID := common.GetUserId(c)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
var req proto.VideoHistoryReportReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := videoser.ReportVideoHistory(c, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// QueryVideoHistoryList 当前用户视频观看历史分页列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 视频观看历史列表
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoHistoryListReq true "分页请求"
|
||||
// @Success 200 {object} proto.VideoHistoryListResp{list=[]proto.VideoHistoryItem} "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/history/list [get]
|
||||
func QueryVideoHistoryList(c *gin.Context) {
|
||||
userID := common.GetUserId(c)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
var req proto.VideoHistoryListReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(c, srvdef.ErrParamError, "")
|
||||
return
|
||||
}
|
||||
resp, err := videoser.QueryVideoHistoryList(c, userID, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ClearVideoHistory 清空当前用户视频观看历史
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 清空视频观看历史
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.VideoHistoryClearResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/video/history [delete]
|
||||
func ClearVideoHistory(c *gin.Context) {
|
||||
userID := common.GetUserId(c)
|
||||
if userID <= 0 {
|
||||
common.ServerJSON(c, srvdef.InvalidToken, "")
|
||||
return
|
||||
}
|
||||
resp, err := videoser.ClearVideoHistory(c, userID)
|
||||
if err != nil {
|
||||
common.ServerJSON(c, err, err.GetMsg())
|
||||
return
|
||||
}
|
||||
common.ServerJSON(c, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package appg
|
||||
|
||||
import (
|
||||
"videoweb/pub/common/ipaddr"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
const EsIndex = "sz"
|
||||
|
||||
var (
|
||||
Conf GlobalCfgEx
|
||||
Cache *cache.Cache
|
||||
Name NameSource
|
||||
DetectorV6 *ipaddr.XDBDetector
|
||||
DetectorV4 *ipaddr.XDBDetector
|
||||
)
|
||||
|
||||
type NameSource struct {
|
||||
FamilyNames []string `yaml:"familyNames"`
|
||||
MiddleNames []string `yaml:"middleNames"`
|
||||
AfterNames []string `yaml:"afterNames"`
|
||||
}
|
||||
type PictureSource struct {
|
||||
Portrait []string `yaml:"portrait"`
|
||||
UserbackGround []string `yaml:"userbackground"`
|
||||
ActressBackground []string `yaml:"actressbackground"`
|
||||
}
|
||||
type GlobalCfgEx struct {
|
||||
Redis struct {
|
||||
Addr string `yaml:"addr"`
|
||||
Db int `yaml:"db"`
|
||||
Passwd string `yaml:"passwd"`
|
||||
Subscribe bool `yaml:"subscribe"`
|
||||
}
|
||||
|
||||
Mongodb struct {
|
||||
Addr string `yaml:"addr"`
|
||||
LogAddr string `yaml:"logaddr"`
|
||||
RecomAddr string `yaml:"recomaddr"`
|
||||
AIOrderCenter string `yaml:"aiordercenter"`
|
||||
}
|
||||
Es struct {
|
||||
Addr string `yaml:"addr"`
|
||||
User string `yaml:"user"`
|
||||
Pass string `yaml:"pass"`
|
||||
} `yaml:"es"`
|
||||
RecomSys struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Addr string `yaml:"addr"`
|
||||
ApiKey string `yaml:"apikey"`
|
||||
} `yaml:"recomsys"`
|
||||
HttpOptions struct {
|
||||
DcCtxTimeOut uint64 `yaml:"dcCtxTimeOut"` //代充http请求 超时ctx取消时间
|
||||
} `yaml:"httpOptions"`
|
||||
Kafka struct {
|
||||
Url string `yaml:"url"`
|
||||
Group string `yaml:"group"`
|
||||
} `yaml:"kafka"`
|
||||
JwtKey string `yaml:"jwtKey"`
|
||||
RunMod string `yaml:"runMod"`
|
||||
ResKey string `yaml:"resKey"`
|
||||
CdnKey string `yaml:"cdnKey"`
|
||||
Pprof bool `yaml:"pprof"`
|
||||
Master bool `yaml:"master"` //是否是主节点
|
||||
Host string `yaml:"host"`
|
||||
ListenAddr string `yaml:"listenAddr"`
|
||||
DHListenAddr string `yaml:"dhListenAddr"`
|
||||
KfAppId string `yaml:"kfAppId"`
|
||||
KfSecret string `yaml:"kfSecret"`
|
||||
KfApiUrlPrefix string `yaml:"kfApiUrlPrefix"`
|
||||
VersionUrl string `yaml:"versionUrl"`
|
||||
KafkaUrl string `yaml:"kafkaUrl"`
|
||||
CenterDomain string `yaml:"centerDomain"`
|
||||
TgToken string `yaml:"tgToken"`
|
||||
ReplayAttackConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
AppKey string `yaml:"appkey"`
|
||||
GuestKey string `yaml:"guestKey"`
|
||||
} `yaml:"attack"`
|
||||
ReplayH5AttackConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
AppKey string `yaml:"appkey"`
|
||||
GuestKey string `yaml:"guestKey"`
|
||||
} `yaml:"h5attack"`
|
||||
PayCenter struct {
|
||||
Addr string `yaml:"addr"`
|
||||
AppId int64 `yaml:"appId"`
|
||||
Serect string `yaml:"serect"`
|
||||
} `yaml:"payCenter"`
|
||||
AiCenterAppId int64 `yaml:"aiCenterAppId"`
|
||||
AiUndressingCallbackUrl string `yaml:"aiUndressingCallbackUrl"`
|
||||
NavigateUrl string `yaml:"navigateUrl"`
|
||||
NewProductUrl string `yaml:"newProductUrl"`
|
||||
TransactionUrl string `yaml:"transactionUrl"` // 支付下单地址
|
||||
LocalWebUrl string `yaml:"localWebUrl"`
|
||||
JituanProductUrl string `yaml:"jituanProductUrl"`
|
||||
AiImageToVideoUrl string `yaml:"aiImageToVideoUrl"` // AI图生视频请求地址
|
||||
AiTextToImageUrl string `yaml:"aiTextToImageUrl"` // Ai文生图请求地址
|
||||
AiMateH5 string `yaml:"aiMateH5"`
|
||||
OrigDomain string `yaml:"origDomain"` // M3U8下载地址
|
||||
CdnAuthKey string `yaml:"cdnAuthKey"`
|
||||
TsAuthKey string `yaml:"tsAuthKey"`
|
||||
LsjMediaConf struct {
|
||||
AppId string `yaml:"appId"`
|
||||
ApiKey string `yaml:"apiKey"`
|
||||
ApiUrl string `yaml:"apiUrl"`
|
||||
M3u8Yuan string `yaml:"m3u8Yuan"`
|
||||
TsYuan string `yaml:"tsYuan"`
|
||||
ImageYuan string `yaml:"imageYuan"`
|
||||
DomainsRefreshHr int64 `yaml:"domainsRefreshHr"`
|
||||
} `yaml:"lsjMediaConf"`
|
||||
TrustedProxies string `yaml:"trustedProxies"` // 可信反向代理IP,多个用逗号分隔,空则不信任任何代理
|
||||
BucketVideoAddressV2 string `yaml:"bucketVideoAddressV2"`
|
||||
BucketVideoAddress string `yaml:"bucketVideoAddress"`
|
||||
AdCenter struct {
|
||||
ApiDomain string `yaml:"apiDomain"` // api地址 如:https://adopenapi.adcs01.top
|
||||
MerchantCode string `yaml:"merchantCode"` // 商户 code
|
||||
AppCode string `yaml:"appCode"` // 应用 code
|
||||
AesKey string `yaml:"aesKey"` // 数据解密KEY
|
||||
} `yaml:"adCenter"`
|
||||
DataReport struct {
|
||||
Url string `yaml:"url"`
|
||||
} `yaml:"dataReport"`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package appg
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
func InitCache() {
|
||||
Cache = cache.New(5*time.Minute, 10*time.Minute)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package appg
|
||||
@@ -0,0 +1,39 @@
|
||||
redis:
|
||||
addr: "172.104.181.99:16789"
|
||||
db: 3
|
||||
passwd: "B6vJHlYiTseLj77z"
|
||||
subscribe: true
|
||||
mongodb:
|
||||
addr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_mdcom?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
|
||||
logaddr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_mdcom_log?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
|
||||
es:
|
||||
addr: "http://172.104.181.99:9200"
|
||||
user: "elastic"
|
||||
pass: "admin123456"
|
||||
httpOptions:
|
||||
dcCtxTimeOut: 10
|
||||
jwtKey: "2311b15a4da1c80dc541b99abb622233"
|
||||
runMod: "debug"
|
||||
resKey: "vEukA&w15z4VAD3kAY#fkL#rBnU!WDhN"
|
||||
cdnKey: "vEukAw15z4VAD3kAYfkLrBnUWDhN123"
|
||||
tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
|
||||
tgToken: ""
|
||||
attack:
|
||||
enable: false
|
||||
appkey: "kaFtkDJRcchRMTI9"
|
||||
h5key: "kaFttDJRcahRMTI7"
|
||||
windowDurationSeconds: 60
|
||||
pprof: false
|
||||
host: "172.104.181.99"
|
||||
listenAddr: "25221"
|
||||
origDomain: "https://yuan.bbaazz.me/"
|
||||
# trustedProxies: "172.104.181.99"
|
||||
trustedProxies: ""
|
||||
bucketVideoAddressV2: "http://139.162.22.207:24996"
|
||||
adCenter:
|
||||
apiDomain: "https://adopenapi.adcs01.top"
|
||||
merchantCode: "1991129808640122880"
|
||||
appCode: "JHA-1009"
|
||||
aesKey: "gD4+rmbX5TS5cLbb3wAN5uEAYp/1j4hkQrS9SzuSjc8"
|
||||
dataReport:
|
||||
url: "https://api.shuifeng.cc"
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
—Џ€И’љБCЏ„
|
||||
+2605
File diff suppressed because it is too large
Load Diff
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
echo "开始"
|
||||
rm -rf ./web_hghl_app ./web_hghl_app.tar.gz
|
||||
GOOS=linux GOARCH=amd64 go build -o web_hghl_app
|
||||
tar zvcf web_hghl_app.tar.gz web_hghl_app
|
||||
|
||||
|
||||
scp web_hghl_app.tar.gz root@172.104.166.87:/data/uzip
|
||||
ssh root@172.104.166.87 "/server/web_hghl/app/up_app.sh"
|
||||
|
||||
rm -rf ./web_hghl_app ./web_hghl_app.tar.gz
|
||||
+6740
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+55
@@ -0,0 +1,55 @@
|
||||
//go:generate swag init --parseDependency --parseDepth 4
|
||||
//go:generate mtool replicate-module -m contest
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"videoweb/app/server"
|
||||
)
|
||||
|
||||
// @title 视频站APP接口
|
||||
// @version 1.0
|
||||
// @description 这是视频站APP接口服务.
|
||||
// @termsOfService
|
||||
|
||||
// @contact.name
|
||||
// @contact.url
|
||||
// @contact.email
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host 视频站-api.lcncctn.com
|
||||
// @BasePath /
|
||||
// @Schemes http https
|
||||
|
||||
// @securityDefinitions.basic BasicAuth
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @securitydefinitions.oauth2.application OAuth2Application
|
||||
// @tokenUrl https://example.com/oauth/token
|
||||
// @scope.write Grants write access
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
// @securitydefinitions.oauth2.implicit OAuth2Implicit
|
||||
// @authorizationUrl https://example.com/oauth/authorize
|
||||
// @scope.write Grants write access
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
// @securitydefinitions.oauth2.password OAuth2Password
|
||||
// @tokenUrl https://example.com/oauth/token
|
||||
// @scope.read Grants read access
|
||||
// @scope.write Grants write access
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
|
||||
// @tokenUrl https://example.com/oauth/token
|
||||
// @authorizationUrl https://example.com/oauth/authorize
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
func main() {
|
||||
server.Run(os.Args)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// gin 验证中间件
|
||||
|
||||
var (
|
||||
// 白名单,不需要验证token的api
|
||||
whiteList = []string{
|
||||
"/swagger",
|
||||
"/api/app/ping/check",
|
||||
"/api/app/ping/config",
|
||||
"/api/app/health/ping",
|
||||
"/api/app/login/guest",
|
||||
"/api/app/auth/register",
|
||||
"/api/app/auth/login",
|
||||
"/api/app/user/qrcode/info",
|
||||
"/api/app/upload/images",
|
||||
"/api/app/home",
|
||||
"/api/app/richtext",
|
||||
"/api/app/content/tag/list",
|
||||
"/api/app/config/footer/info",
|
||||
"/api/app/config/richtext/list",
|
||||
"/api/app/navigation/header",
|
||||
"/api/app/comment/list",
|
||||
"/api/app/video/more",
|
||||
"/api/app/video/list",
|
||||
"/api/app/video/category/list",
|
||||
"/api/app/video/info",
|
||||
"/api/app/video/recommend/list",
|
||||
"/api/app/video/guess/list",
|
||||
"/api/app/video/rank/list",
|
||||
"/api/app/playlet/recommend/list",
|
||||
"/api/app/playlet/guess/list",
|
||||
"/api/app/playlet/home",
|
||||
"/api/app/playlet/home-config/list",
|
||||
"/api/app/playlet/topic/detail",
|
||||
"/api/app/playlet/theme/list",
|
||||
"/api/app/playlet/list",
|
||||
"/api/app/playlet/content/list",
|
||||
"/api/app/playlet/tag/",
|
||||
"/api/app/playlet/info",
|
||||
"/api/app/playlet/chapter/info",
|
||||
"/api/app/playlet/rank/list",
|
||||
"/api/app/actress",
|
||||
"/api/app/upload/videoBody",
|
||||
"/api/app/upload/videoOk",
|
||||
"/api/app/upload/image",
|
||||
"/search",
|
||||
"/api/app/applist",
|
||||
"/h5/m3u8",
|
||||
"/enkey",
|
||||
"/newenkey",
|
||||
"/lsjenkey",
|
||||
"/dh",
|
||||
"/businessCooperation/",
|
||||
"/servicesProject/",
|
||||
"/consulting/",
|
||||
"/consultinginfo/",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
tokener = utils.NewTokenRedis()
|
||||
)
|
||||
|
||||
func Auth(ctx *gin.Context) {
|
||||
dotoken := func() srvdef.CodeType {
|
||||
token := ctx.Request.Header.Get("Authorization")
|
||||
// 去掉Bearer,如果有
|
||||
token = utils.StripBearerPrefixFromTokenString(token)
|
||||
|
||||
if token == "" {
|
||||
for _, w := range whiteList {
|
||||
if strings.Contains(ctx.Request.URL.Path, w) {
|
||||
return srvdef.Success
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("Auth:token is empty", log.Any("sysType", ctx.GetString(consts.CtxSysType)))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
|
||||
userId, newToken, err := utils.ParseUserToken(token, appg.Conf.JwtKey)
|
||||
if err != nil {
|
||||
for _, w := range whiteList {
|
||||
if strings.Contains(ctx.Request.URL.Path, w) {
|
||||
return srvdef.Success
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("Auth:ParseUserToken", log.Any("token", token), log.E(err))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
|
||||
has, err := tokener.Exists(userId, token)
|
||||
if err != nil {
|
||||
log.Error("Auth:Exists", log.Any("userId", userId), log.Any("token", token), log.E(err))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
if !has {
|
||||
log.Error("Auth:Exists:token not exists", log.Any("userId", userId), log.Any("token", token), log.E(err))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
|
||||
if len(newToken) > 0 {
|
||||
err = tokener.Save(userId, newToken)
|
||||
if err != nil {
|
||||
log.Error("Auth:Save", log.Any("userId", userId), log.Any("token", newToken), log.E(err))
|
||||
return srvdef.StoreLevel
|
||||
}
|
||||
ctx.Writer.Header().Set("Refresh-Authorization", newToken)
|
||||
}
|
||||
|
||||
ctx.Set(consts.CtxUserID, userId)
|
||||
ctx.Set(consts.CtxToken, token)
|
||||
|
||||
return srvdef.Success
|
||||
}
|
||||
|
||||
errType := dotoken()
|
||||
if errType != srvdef.Success {
|
||||
// log.Info("Auth bad request", log.Any("Path", ctx.Request.URL.Path), log.Any("ip", ctx.ClientIP()))
|
||||
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
|
||||
"code": errType,
|
||||
"msg": errType.Msg(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ipblacklist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"videoweb/app/service/configser"
|
||||
)
|
||||
|
||||
func IpBlackListMiddleware(ctx *gin.Context) {
|
||||
ip := ctx.ClientIP()
|
||||
if configser.CheckIpBlack(ip) {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package ipstat
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// IpStatMiddleware 记录访客真实 IP,按天存入 Redis SET 实现去重计数。
|
||||
// key 格式:ipstat:{appId}:{yyyymmdd},TTL 49 小时(保证昨日数据次日仍可读)。
|
||||
func IpStatMiddleware(ctx *gin.Context) {
|
||||
ctx.Next()
|
||||
|
||||
normalized := normalizeIP(ipaddr.ResolveClientIP(ctx))
|
||||
if normalized == "" {
|
||||
return
|
||||
}
|
||||
|
||||
date := time.Now().In(utils.LocBJ).Format("20060102")
|
||||
key := redisconst.IpStatKey(pubdef.AppId, date)
|
||||
|
||||
isNew, err := redisSAddNX(key, normalized)
|
||||
if err != nil {
|
||||
log.Warn("IpStatMiddleware SAdd failed", log.Any("key", key), log.E(err))
|
||||
return
|
||||
}
|
||||
if isNew {
|
||||
// 仅新增成员时刷新 TTL,避免每次请求都执行 EXPIRE
|
||||
if err := redis.Expire(key, redisconst.IpStatExpire); err != nil {
|
||||
log.Warn("IpStatMiddleware Expire failed", log.Any("key", key), log.E(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redisSAddNX 添加成员并返回是否为新增(count > 0)
|
||||
func redisSAddNX(key string, member string) (bool, error) {
|
||||
err := redis.SAdd(key, member)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// normalizeIP 将原始 IP 标准化:
|
||||
// - 去掉 IPv4-mapped IPv6 前缀(::ffff:)
|
||||
// - 过滤私有/回环/链路本地地址
|
||||
// - 非法或私有 IP 返回空字符串
|
||||
func normalizeIP(raw string) string {
|
||||
ip := net.ParseIP(raw)
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
ip = v4
|
||||
}
|
||||
if isPrivateIP(ip) {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
var privateRanges = []net.IPNet{
|
||||
parseCIDR("10.0.0.0/8"),
|
||||
parseCIDR("172.16.0.0/12"),
|
||||
parseCIDR("192.168.0.0/16"),
|
||||
parseCIDR("127.0.0.0/8"),
|
||||
parseCIDR("::1/128"),
|
||||
parseCIDR("fe80::/10"),
|
||||
parseCIDR("fc00::/7"),
|
||||
}
|
||||
|
||||
func parseCIDR(s string) net.IPNet {
|
||||
_, network, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *network
|
||||
}
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
for _, r := range privateRanges {
|
||||
if r.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -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
|
||||
//退出
|
||||
)
|
||||
@@ -0,0 +1,262 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/tgbot"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Check 运行监控检查
|
||||
func Check(ctx *gin.Context) {
|
||||
//查看是否有限制登陆
|
||||
ip := ctx.ClientIP()
|
||||
ifKey := redisconst.IpForbitKey(ip)
|
||||
retValue, _ := redis.Get(ifKey)
|
||||
if retValue != nil {
|
||||
//log.Info("Test Check", log.Any("ip", ip), log.Any("retValue", *retValue))
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"code": srvdef.ErrIpForbid,
|
||||
"msg": srvdef.ErrIpForbid.Msg(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := ctx.Request.URL.Path
|
||||
raw := ctx.Request.URL.RawQuery
|
||||
rawData, err := ctx.GetRawData()
|
||||
if err != nil {
|
||||
log.Info("Test get raw data error", log.E(err))
|
||||
}
|
||||
// 将取出来的执行条件放回ctx中
|
||||
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData))
|
||||
//recover
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
buf := make([]byte, 2048)
|
||||
length := runtime.Stack(buf, false)
|
||||
info := string(buf[:length])
|
||||
log.Error("catch a unkown excetion", log.Any("excetion :", info))
|
||||
//发送tg消息
|
||||
msg := "monitor check crash,the stack:\n" + info
|
||||
tgbot.SendToProgram("API接口处理宕了", msg)
|
||||
}
|
||||
}()
|
||||
// Process request
|
||||
ctx.Next()
|
||||
}()
|
||||
// Stop timer
|
||||
stopTime := time.Now()
|
||||
latency := stopTime.Sub(start).Milliseconds()
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
//添加记录
|
||||
//log.Info("Test", log.Any("rawData", rawData))
|
||||
rawString := string(rawData)
|
||||
addApiCall(path, int64(latency), rawString)
|
||||
//如果时间大于1秒发送tg消息
|
||||
if latency > 4000 {
|
||||
userId := ctx.GetInt64(consts.CtxUserID)
|
||||
msg := ""
|
||||
msg = msg + fmt.Sprintf("path:%s", path) + "\n"
|
||||
msg = msg + fmt.Sprintf("userId:%d", userId) + "\n"
|
||||
msg = msg + fmt.Sprintf("userIp:%s", ip) + "\n"
|
||||
msg = msg + fmt.Sprintf("rawData:%s", string(rawData)) + "\n"
|
||||
msg = msg + fmt.Sprintf("time:%d", latency) + "\n"
|
||||
tgbot.SendToProgram("API接口调用超时", msg)
|
||||
}
|
||||
//log.Info("monitor:Check", log.Any("latency", latency), log.Any("path", path))
|
||||
}
|
||||
|
||||
func init() {
|
||||
hourStatises = make(map[string]*LogicStatis)
|
||||
muniteStatises = make(map[string]*LogicStatis)
|
||||
apiCallChan = make(chan *StatisParam, 1024)
|
||||
godef.Go(loop)
|
||||
log.Info("(monitor:init) init success.")
|
||||
}
|
||||
|
||||
func loop() {
|
||||
log.Info("(monitor:loop) start...")
|
||||
defer log.Info("monitor:loop) end")
|
||||
var param *StatisParam
|
||||
for {
|
||||
select {
|
||||
case param = <-apiCallChan:
|
||||
onStatis(param)
|
||||
default:
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onStatis(param *StatisParam) {
|
||||
if param == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
hourStr := now.Format("2006-01-02 15")
|
||||
hourStatis, hourOk := hourStatises[hourStr]
|
||||
if !hourOk {
|
||||
hourStatis = &LogicStatis{
|
||||
ApisStatis: make(map[string]*APIStatis),
|
||||
}
|
||||
hourStatises[hourStr] = hourStatis
|
||||
}
|
||||
muniteStr := now.Format("2006-01-02 15:04")
|
||||
minuteStatis, minuteOk := muniteStatises[muniteStr]
|
||||
if !minuteOk {
|
||||
minuteStatis = &LogicStatis{
|
||||
ApisStatis: make(map[string]*APIStatis),
|
||||
}
|
||||
muniteStatises[muniteStr] = minuteStatis
|
||||
}
|
||||
onStatisApiCall(hourStr, param, hourStatis, 10000000)
|
||||
onStatisApiCall(muniteStr, param, minuteStatis, 20000)
|
||||
//如果新的一小时,发送上一小时数据
|
||||
if !hourOk {
|
||||
//清空分钟数据
|
||||
muniteStatises = make(map[string]*LogicStatis)
|
||||
lastHour := now.Add(-time.Hour).Format("2006-01-02 15")
|
||||
hourStatis, hourOk = hourStatises[lastHour]
|
||||
if hourOk {
|
||||
msg := fmt.Sprintf("统计时间:%s\n", lastHour)
|
||||
msg += formatStatis(hourStatis)
|
||||
tgbot.SendToProgram("API接口调用统计", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatStatis(hourStatis *LogicStatis) string {
|
||||
apiStatises := make([]*APIStatis, 0)
|
||||
for _, static := range hourStatis.ApisStatis {
|
||||
apiStatises = append(apiStatises, static)
|
||||
}
|
||||
sort.Slice(apiStatises, func(i, j int) bool {
|
||||
return apiStatises[i].CallNum > apiStatises[j].CallNum
|
||||
})
|
||||
//发送数据
|
||||
msg := ""
|
||||
maxlen := len(apiStatises)
|
||||
if maxlen > 10 {
|
||||
maxlen = 10
|
||||
}
|
||||
for i := 0; i < maxlen; i++ {
|
||||
apiStatis := apiStatises[i]
|
||||
msg += formatApiStatis(apiStatis)
|
||||
}
|
||||
timePerCall := int64(0)
|
||||
callPerSec := int64(0)
|
||||
if hourStatis.ApiCallNum > 0 {
|
||||
timePerCall = hourStatis.ApiTotalTime / hourStatis.ApiCallNum
|
||||
diffSecdonds := int64(time.Now().Sub(hourStatis.Start).Seconds())
|
||||
if diffSecdonds > 0 {
|
||||
callPerSec = hourStatis.ApiCallNum / diffSecdonds
|
||||
}
|
||||
}
|
||||
msg += fmt.Sprintf("ApiCallNum:%d \n", hourStatis.ApiCallNum)
|
||||
msg += fmt.Sprintf("ApiTotalTime:%d \n", hourStatis.ApiTotalTime)
|
||||
msg += fmt.Sprintf("ApiTimePerCall:%d \n", timePerCall)
|
||||
msg += fmt.Sprintf("ApiCallPerSec:%d \n", callPerSec)
|
||||
return msg
|
||||
}
|
||||
|
||||
func formatApiStatis(apiStatis *APIStatis) string {
|
||||
msg := ""
|
||||
if apiStatis.CallNum > 0 {
|
||||
apiStatis.TimePerCall = apiStatis.TotalTime / apiStatis.CallNum
|
||||
diffSeconds := int64(time.Now().Sub(apiStatis.Start).Seconds())
|
||||
if diffSeconds > 0 {
|
||||
apiStatis.CallPerSec = apiStatis.CallNum / diffSeconds
|
||||
}
|
||||
}
|
||||
|
||||
statisStr := fmt.Sprintf(`path %s:
|
||||
MaxRawData:%s
|
||||
CallNum:%d;MinTime:%d;MaxTime:%d;TotalTime:%d;TimePerCall:%d;CallPerSec:%d`,
|
||||
apiStatis.Path,
|
||||
apiStatis.MaxTimeRawData,
|
||||
apiStatis.CallNum,
|
||||
apiStatis.MinTime,
|
||||
apiStatis.MaxTime,
|
||||
apiStatis.TotalTime,
|
||||
apiStatis.TimePerCall,
|
||||
apiStatis.CallPerSec)
|
||||
msg += statisStr
|
||||
msg += "\n---------------------------------------------------\n"
|
||||
return msg
|
||||
}
|
||||
|
||||
// api统计
|
||||
func onStatisApiCall(dateStr string, param *StatisParam,
|
||||
logicStatis *LogicStatis, maxCallNum int64) {
|
||||
logicStatis.ApiCallNum++
|
||||
logicStatis.ApiTotalTime += param.Param2
|
||||
apiStatis, ok := logicStatis.ApisStatis[param.Param1]
|
||||
if !ok {
|
||||
apiStatis = &APIStatis{
|
||||
Path: param.Param1,
|
||||
MaxTimeRawData: param.Param3,
|
||||
CallNum: 0,
|
||||
MinTime: math.MaxInt64,
|
||||
MaxTime: 0,
|
||||
TotalTime: 0,
|
||||
Start: time.Now(),
|
||||
Send: 0,
|
||||
}
|
||||
logicStatis.ApisStatis[param.Param1] = apiStatis
|
||||
}
|
||||
apiStatis.CallNum++
|
||||
apiStatis.TotalTime += param.Param2
|
||||
if param.Param2 > apiStatis.MaxTime {
|
||||
apiStatis.MaxTime = param.Param2
|
||||
apiStatis.MaxTimeRawData = param.Param3
|
||||
}
|
||||
if param.Param2 < apiStatis.MinTime {
|
||||
apiStatis.MinTime = param.Param2
|
||||
}
|
||||
if apiStatis.CallNum > maxCallNum && apiStatis.Send < 4 {
|
||||
apiStatis.Send++
|
||||
msg := fmt.Sprintf("接口%s每分钟调用超过最大次数%d\n", apiStatis.Path, maxCallNum)
|
||||
msg += formatApiStatis(apiStatis)
|
||||
tgbot.SendToProgram("API调用警告", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func addApiCall(path string, latency int64, rawData string) {
|
||||
param := &StatisParam{
|
||||
Type: SP_ApiCall,
|
||||
Param1: path,
|
||||
Param2: latency,
|
||||
Param3: rawData,
|
||||
}
|
||||
pushParam(param)
|
||||
}
|
||||
|
||||
func pushParam(param *StatisParam) {
|
||||
//no block
|
||||
select {
|
||||
case apiCallChan <- param:
|
||||
default:
|
||||
info := "(monitor:pushParam) failed,chan is full."
|
||||
log.Info(info)
|
||||
tgbot.SendToProgram("monitor pushParam failed", info)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package reptilerecord
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"strings"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
const ctxKeyIsBot = "isBot"
|
||||
|
||||
var (
|
||||
botPatterns = map[string]string{
|
||||
"Googlebot": "Googlebot",
|
||||
"Bingbot": "Bingbot",
|
||||
"Slurp": "Slurp",
|
||||
"DuckDuckBot": "DuckDuckBot",
|
||||
"Baiduspider": "Baiduspider",
|
||||
"YandexBot": "YandexBot",
|
||||
"Sogou": "Sogou",
|
||||
"Exabot": "Exabot",
|
||||
"facebot": "facebot",
|
||||
"facebookexternalhit": "facebookexternalhit",
|
||||
}
|
||||
)
|
||||
|
||||
func ReptileRecord(c *gin.Context) {
|
||||
ua := c.Request.UserAgent()
|
||||
botName := identifyBot(ua)
|
||||
if botName != "" {
|
||||
c.Set(ctxKeyIsBot, true)
|
||||
log.Info("爬虫访问:", log.Any("botName", botName), log.Any("ua", ua), log.Any("path", c.Request.URL.Path), log.Any("ip", c.ClientIP()))
|
||||
}
|
||||
}
|
||||
|
||||
// IsBot 返回当前请求是否来自已知搜索引擎爬虫
|
||||
func IsBot(c *gin.Context) bool {
|
||||
v, exists := c.Get(ctxKeyIsBot)
|
||||
return exists && v.(bool)
|
||||
}
|
||||
|
||||
func identifyBot(userAgent string) string {
|
||||
for name, pattern := range botPatterns {
|
||||
if strings.Contains(userAgent, pattern) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package templatecache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
const (
|
||||
maxCacheItems = 2000
|
||||
maxCacheBytes = 64 << 20
|
||||
)
|
||||
|
||||
var (
|
||||
Cache *cache.Cache
|
||||
cacheBytes int64
|
||||
)
|
||||
|
||||
// 章节详情、资讯详情等 ID 空间过大,禁止进进程缓存。
|
||||
var highCardinalityContains = []string{
|
||||
"/chapter/info",
|
||||
"/consultinginfo/",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cache = cache.New(5*time.Minute, 5*time.Minute)
|
||||
Cache.OnEvicted(func(_ string, v interface{}) {
|
||||
if b, ok := v.([]byte); ok {
|
||||
atomic.AddInt64(&cacheBytes, -int64(len(b)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func shouldBypassCache(path string) bool {
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
if i := strings.IndexByte(path, '?'); i >= 0 {
|
||||
path = path[:i]
|
||||
}
|
||||
for _, part := range highCardinalityContains {
|
||||
if strings.Contains(path, part) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cacheBudgetAllows(n int) bool {
|
||||
if n <= 0 {
|
||||
return false
|
||||
}
|
||||
if Cache.ItemCount() >= maxCacheItems {
|
||||
return false
|
||||
}
|
||||
return atomic.LoadInt64(&cacheBytes)+int64(n) <= maxCacheBytes
|
||||
}
|
||||
|
||||
type templateResponseWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *templateResponseWriter) Write(b []byte) (int, error) {
|
||||
w.body.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func WithCache(duration time.Duration) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
if ctx.Request.Method != "GET" {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
if shouldBypassCache(ctx.Request.URL.Path) {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := ctx.Request.URL.String()
|
||||
if cacheData, exists := Cache.Get(cacheKey); exists {
|
||||
if item, ok := cacheData.([]byte); ok && len(item) > 0 {
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", item)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writer := &templateResponseWriter{
|
||||
body: bytes.NewBuffer(nil),
|
||||
ResponseWriter: ctx.Writer,
|
||||
}
|
||||
ctx.Writer = writer
|
||||
ctx.Next()
|
||||
|
||||
if ctx.Writer.Status() != 200 {
|
||||
return
|
||||
}
|
||||
raw := writer.body.Bytes()
|
||||
if !cacheBudgetAllows(len(raw)) {
|
||||
return
|
||||
}
|
||||
copied := append([]byte(nil), raw...)
|
||||
atomic.AddInt64(&cacheBytes, int64(len(copied)))
|
||||
cacheDuration := duration
|
||||
if cacheDuration == 0 {
|
||||
cacheDuration = time.Minute
|
||||
}
|
||||
Cache.Set(cacheKey, copied, cacheDuration)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
// 替换gin 默认v8验证器
|
||||
// 改为v9
|
||||
func init() {
|
||||
binding.Validator = new(defaultValidator)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package visit
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 用户访问中间件
|
||||
// 记录用户在线量
|
||||
// 按5分钟纬度记录到redis中
|
||||
func Visit(ctx *gin.Context) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/actressapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func actressRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/actress/list", actressapi.ActressList)
|
||||
r.GET("/api/app/actress/work/list", actressapi.QueryActressWorkList)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/authapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func authRouter(r *gin.Engine) {
|
||||
// 游客登录(无需 token)
|
||||
login := r.Group("/api/app/login")
|
||||
{
|
||||
login.POST("/guest", authapi.GuestLogin)
|
||||
}
|
||||
|
||||
// 账号注册 / 登录 / 改资料
|
||||
auth := r.Group("/api/app/auth")
|
||||
{
|
||||
auth.POST("/register", authapi.Register)
|
||||
auth.POST("/login", authapi.Login)
|
||||
auth.GET("/getInfo", authapi.GetInfo)
|
||||
auth.POST("/profile", authapi.UpdateProfile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/commentapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func commentRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/comment/list", commentapi.List)
|
||||
r.POST("/api/app/comment/send", commentapi.SendComment)
|
||||
r.POST("/api/app/comment/like/set", commentapi.SetLike)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
configapi "videoweb/app/api/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func configRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/config/footer/info", configapi.FooterInfo)
|
||||
r.GET("/api/app/config/richtext/list", configapi.RichTextList)
|
||||
r.GET("/api/app/content/tag/list", configapi.ContentTagList)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/app/api/dhapi"
|
||||
"videoweb/app/middleware/templatecache"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func dhRouter(r *gin.Engine) {
|
||||
r.GET("/dh/", templatecache.WithCache(time.Minute*1), dhapi.DHApi)
|
||||
r.GET("/businessCooperation/", templatecache.WithCache(time.Minute*1), dhapi.BusinessCooperationApi)
|
||||
r.GET("/servicesProject/", templatecache.WithCache(time.Minute*1), dhapi.ServicesProjectApi)
|
||||
r.GET("/consulting/", templatecache.WithCache(time.Minute*1), dhapi.ConsultingApi)
|
||||
r.GET("/consultinginfo/:id/", templatecache.WithCache(time.Minute*1), dhapi.ConsultingInfoApi)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/app/middleware/auth"
|
||||
"videoweb/app/middleware/cors"
|
||||
"videoweb/app/middleware/ipblacklist"
|
||||
"videoweb/app/middleware/ipstat"
|
||||
"videoweb/app/middleware/reptilerecord"
|
||||
"videoweb/app/service/displaycount"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/DeanThompson/ginpprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
_ "videoweb/app/docs"
|
||||
)
|
||||
|
||||
func Init() (engine *gin.Engine) {
|
||||
// 设置启动模式
|
||||
switch appg.Conf.RunMod {
|
||||
case "dev":
|
||||
gin.SetMode(gin.DebugMode)
|
||||
case "test":
|
||||
gin.SetMode(gin.TestMode)
|
||||
case "prod":
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
default:
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
// 初始化gin引擎
|
||||
engine = gin.Default()
|
||||
engine.HandleMethodNotAllowed = true
|
||||
engine.MaxMultipartMemory = 60 << 20
|
||||
|
||||
// 从配置文件读取可信代理列表(字符串,多个 IP 用逗号分隔)
|
||||
// 配置了则只信任这些 IP 的 X-Forwarded-For,未配置则不信任任何代理(直接用 TCP 连接 IP)
|
||||
// 配置示例(app.yaml)
|
||||
// trustedProxies: "10.0.0.1"
|
||||
// trustedProxies: "10.0.0.1,10.0.0.2"
|
||||
if appg.Conf.TrustedProxies != "" {
|
||||
proxies := strings.Split(appg.Conf.TrustedProxies, ",")
|
||||
for i, p := range proxies {
|
||||
proxies[i] = strings.TrimSpace(p)
|
||||
}
|
||||
_ = engine.SetTrustedProxies(proxies)
|
||||
log.Printf("SetTrustedProxies: %v", proxies)
|
||||
} else {
|
||||
_ = engine.SetTrustedProxies([]string{})
|
||||
log.Printf("SetTrustedProxies: none (direct TCP IP mode)")
|
||||
}
|
||||
|
||||
common.InitGinSecret(appg.Conf.ResKey, consts.RunMod(appg.Conf.RunMod))
|
||||
common.SetResponseDataTransform(displaycount.Transform)
|
||||
// 注册中间件
|
||||
registeMiddleware(engine)
|
||||
// 注册路由
|
||||
registeRouter(engine)
|
||||
|
||||
if appg.Conf.RunMod != "prod" {
|
||||
setGinSwagger(engine)
|
||||
}
|
||||
// 是否开启监控服务
|
||||
if appg.Conf.Pprof {
|
||||
ginpprof.Wrap(engine)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func registeMiddleware(engine *gin.Engine) {
|
||||
engine.Use(
|
||||
// gin.Recovery(),
|
||||
cors.Cors(&cors.Options{
|
||||
Origin: nil, // "*"
|
||||
Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete},
|
||||
AllowedHeaders: []string{"Content-Length", "Content-Type", "Authorization", "X-User-Agent", "device", "api_version", "x-api-key", useragent.SDKCommonHeader},
|
||||
ExposedHeaders: []string{"Content-Disposition", "Refresh-Authorization"},
|
||||
OptionsSuccessStatus: http.StatusOK,
|
||||
}),
|
||||
// 必须是此顺序, happens before原则
|
||||
//monitor.Check,
|
||||
ipblacklist.IpBlackListMiddleware,
|
||||
reptilerecord.ReptileRecord,
|
||||
ipstat.IpStatMiddleware,
|
||||
useragent.UserAgent,
|
||||
auth.Auth,
|
||||
// calllimit.Check,
|
||||
// visit.Visit,
|
||||
)
|
||||
}
|
||||
|
||||
// 注册路由
|
||||
func registeRouter(r *gin.Engine) {
|
||||
homeRouter(r)
|
||||
videoRouter(r)
|
||||
commentRouter(r)
|
||||
searchRouter(r)
|
||||
dhRouter(r)
|
||||
lawAndTrustRouter(r)
|
||||
actressRouter(r)
|
||||
configRouter(r)
|
||||
navigationRouter(r)
|
||||
playletRouter(r)
|
||||
authRouter(r)
|
||||
uploadRouter(r)
|
||||
questionnaireRouter(r)
|
||||
appInfoRouter(r)
|
||||
healthRouter(r)
|
||||
richTextRouter(r)
|
||||
}
|
||||
|
||||
// 设置swag 接口信息
|
||||
func setGinSwagger(router *gin.Engine) {
|
||||
// 使用相对路径,无需依赖 Host 配置,本地/测试环境均可直接访问
|
||||
url := ginSwagger.URL("/swagger/doc.json")
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/app/api/home"
|
||||
"videoweb/app/middleware/templatecache"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func homeRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/home", home.Home)
|
||||
r.GET("/api/app/home/base", templatecache.WithCache(time.Minute*1), home.HomeBase)
|
||||
r.GET("/api/app/home/tags", templatecache.WithCache(time.Minute*1), home.HomeTags)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/app/api/lawandtrust"
|
||||
"videoweb/app/middleware/templatecache"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func lawAndTrustRouter(r *gin.Engine) {
|
||||
r.GET("/useragreement/", templatecache.WithCache(time.Minute*1), lawandtrust.UserAgreementApi)
|
||||
r.GET("/privacyPolicy/", templatecache.WithCache(time.Minute*1), lawandtrust.PrivacyPolicyApi)
|
||||
r.GET("/dmca/", templatecache.WithCache(time.Minute*1), lawandtrust.DmcaApi)
|
||||
r.GET("/statement/", templatecache.WithCache(time.Minute*1), lawandtrust.StatementApi)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/navigationapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func navigationRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/navigation/header", navigationapi.HeaderNavigation)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/playletapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func playletRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/playlet/home", playletapi.HomePlaylet)
|
||||
r.GET("/api/app/playlet/home-config/list", playletapi.HomeConfigList)
|
||||
r.GET("/api/app/playlet/topic/detail", playletapi.TopicDetail)
|
||||
r.GET("/api/app/playlet/theme/list", playletapi.ThemeList)
|
||||
r.GET("/api/app/playlet/list", playletapi.List)
|
||||
r.GET("/api/app/playlet/content/list", playletapi.ContentList)
|
||||
r.GET("/api/app/playlet/recommend/list", playletapi.RecommendList)
|
||||
r.GET("/api/app/playlet/guess/list", playletapi.GuessList)
|
||||
r.GET("/api/app/playlet/tag/:id", playletapi.TagList)
|
||||
r.GET("/api/app/playlet/info", playletapi.QueryPlayletInfo)
|
||||
r.GET("/api/app/playlet/chapter/info", playletapi.QueryPlayletChapterInfo)
|
||||
r.POST("/api/app/playlet/like", playletapi.LikePlaylet)
|
||||
r.GET("/api/app/playlet/like/list", playletapi.QueryLikePlayletList)
|
||||
r.POST("/api/app/playlet/favorite", playletapi.FavoritePlaylet)
|
||||
r.GET("/api/app/playlet/favorite/list", playletapi.QueryFavoritePlayletList)
|
||||
r.GET("/api/app/playlet/history/list", playletapi.QueryPlayletHistoryList)
|
||||
r.POST("/api/app/playlet/history", playletapi.ReportPlayletHistory)
|
||||
r.DELETE("/api/app/playlet/history", playletapi.ClearPlayletHistory)
|
||||
r.GET("/api/app/playlet/rank/list", playletapi.QueryPlayletRankList)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/questionnaireapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func questionnaireRouter(r *gin.Engine) {
|
||||
r.GET("/questionnaire", questionnaireapi.Page)
|
||||
r.POST("/api/app/questionnaire/submit", questionnaireapi.Submit)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/richtextapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func richTextRouter(router *gin.Engine) {
|
||||
router.GET("/api/app/richtext", richtextapi.GetByTitle)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/app/api/searchapi"
|
||||
"videoweb/app/middleware/templatecache"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func searchRouter(r *gin.Engine) {
|
||||
|
||||
r.GET("/search/", templatecache.WithCache(time.Minute*1), searchapi.SearchApi)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/uploadapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func uploadRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/upload")
|
||||
{
|
||||
group.POST("/videoBody", uploadapi.VideoBody)
|
||||
group.POST("/videoOk", uploadapi.VideoOk)
|
||||
group.POST("/image", uploadapi.UploadImage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/videoapi"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
_ "videoweb/app/docs"
|
||||
)
|
||||
|
||||
func videoRouter(r *gin.Engine) {
|
||||
|
||||
r.GET("/api/app/video/list", videoapi.List)
|
||||
r.GET("/api/app/video/category/list", videoapi.CategoryList)
|
||||
r.GET("/api/app/video/recommend/list", videoapi.RecommendList)
|
||||
r.GET("/api/app/video/guess/list", videoapi.GuessList)
|
||||
r.GET("/api/app/video/tag/:id", videoapi.TagList)
|
||||
r.GET("/api/app/video/more", videoapi.QueryVideoMore)
|
||||
r.GET("/api/app/video/info", videoapi.QueryVideoInfo)
|
||||
r.POST("/api/app/video/favorite", videoapi.FavoriteVideo)
|
||||
r.POST("/api/app/video/like", videoapi.LikeVideo)
|
||||
r.GET("/api/app/video/like/list", videoapi.QueryLikeVideoList)
|
||||
r.GET("/api/app/video/favorite/list", videoapi.QueryFavoriteVideoList)
|
||||
r.GET("/api/app/video/history/list", videoapi.QueryVideoHistoryList)
|
||||
r.POST("/api/app/video/history", videoapi.ReportVideoHistory)
|
||||
r.DELETE("/api/app/video/history", videoapi.ClearVideoHistory)
|
||||
r.GET("/api/app/video/rank", videoapi.QueryVideoRank)
|
||||
r.GET("/api/app/video/rank/list", videoapi.QueryVideoRankList)
|
||||
r.GET("/h5/m3u8/*key", videoser.GetM3U8Ex)
|
||||
r.GET("/enkey", videoser.Enkey)
|
||||
r.GET("/newenkey", videoser.NewEnkey)
|
||||
r.GET("/lsjenkey", videoser.LsjEnkey)
|
||||
r.GET("/vtt/*key", videoser.GetVTT)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/app/router"
|
||||
"videoweb/app/service/appser"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/richtextser"
|
||||
"videoweb/app/service/translationser"
|
||||
ipstatsync "videoweb/app/skd/ipstat"
|
||||
seoskd "videoweb/app/skd/seo"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvversion"
|
||||
"videoweb/pub/common/tgbot"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/redis"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
time.Local, _ = time.LoadLocation("Asia/Shanghai")
|
||||
}
|
||||
func Run(args []string) {
|
||||
showVersion()
|
||||
if err := initConfig(); err != nil {
|
||||
log.Error("init config failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := initMongoDB(); err != nil {
|
||||
log.Error("init mongodb failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//初始化tg机器人
|
||||
if appg.Conf.TgToken != "" {
|
||||
// tgbot.InitTgBot(appg.Conf.TgToken, "[汁乎App]")
|
||||
//tgbot.SendToProgram("Test", "Test")
|
||||
}
|
||||
detectorV6, err := ipaddr.NewXDBDetectorV6("conf/ip2region_v6.xdb")
|
||||
if err != nil {
|
||||
fmt.Printf("创建检测器失败: %v\n", err)
|
||||
fmt.Println("正在尝试下载数据库文件...")
|
||||
}
|
||||
appg.DetectorV6 = detectorV6
|
||||
|
||||
detectorV4, err := ipaddr.NewXDBDetectorV4("conf/ip2region_v4.xdb")
|
||||
if err != nil {
|
||||
fmt.Printf("创建检测器失败: %v\n", err)
|
||||
fmt.Println("正在尝试下载数据库文件...")
|
||||
}
|
||||
appg.DetectorV4 = detectorV4
|
||||
proxy.InitBucketVideoAddress(appg.Conf.BucketVideoAddress, appg.Conf.BucketVideoAddressV2)
|
||||
mongos.StartLid()
|
||||
initRedis()
|
||||
initData()
|
||||
ipstatsync.StartSync()
|
||||
dataReport.Init(dataReport.Config{
|
||||
ApiUrl: appg.Conf.DataReport.Url,
|
||||
RunMod: appg.Conf.RunMod,
|
||||
})
|
||||
if err := initElasticSearch(); err != nil {
|
||||
log.Error("init ES failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
initHttpServer()
|
||||
seoskd.Start()
|
||||
//tgbot.SendToProgram("启动", "app启动完毕")
|
||||
shutdownHandler()
|
||||
}
|
||||
|
||||
func initConfig() error {
|
||||
|
||||
buf, err := ioutil.ReadFile("conf/app.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = yaml.Unmarshal(buf, &appg.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
nameBuf, err := ioutil.ReadFile("conf/names.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = yaml.Unmarshal(nameBuf, &appg.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// pictureBuf, err := ioutil.ReadFile("conf/picture.yaml")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// if err = yaml.Unmarshal(pictureBuf, &appg.Picture); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initMongoDB() error {
|
||||
shopDb := mongos.NewManager(appg.Conf.Mongodb.Addr)
|
||||
if err := shopDb.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
mongos.Shop = shopDb
|
||||
|
||||
ycShopDb := mongos.NewManager(appg.Conf.Mongodb.LogAddr)
|
||||
if err := ycShopDb.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
mongos.ShopLog = ycShopDb
|
||||
return nil
|
||||
}
|
||||
|
||||
func initElasticSearch() error {
|
||||
es, err := esmanager.NewManager(appg.Conf.Es.Addr,
|
||||
appg.Conf.Es.User, appg.Conf.Es.Pass)
|
||||
if err != nil {
|
||||
log.Info("new es failed", log.Any("err", err))
|
||||
return err
|
||||
}
|
||||
if err = es.ClientConnect(); err != nil {
|
||||
log.Info("connect es failed", log.Any("err", err))
|
||||
return err
|
||||
}
|
||||
esmanager.Instance = es
|
||||
log.Info("es:Init Ok!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 数据
|
||||
func initData() {
|
||||
models.Init()
|
||||
// service.Start()
|
||||
appg.InitCache()
|
||||
translationser.Start()
|
||||
configser.Start()
|
||||
richtextser.Start()
|
||||
appser.Start()
|
||||
}
|
||||
|
||||
func initRedis() error {
|
||||
if err := redis.Init(redis.Options{
|
||||
Addr: appg.Conf.Redis.Addr,
|
||||
Psw: appg.Conf.Redis.Passwd,
|
||||
Db: appg.Conf.Redis.Db,
|
||||
}); err != nil {
|
||||
log.Error("redis connect err", log.E(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func showVersion() {
|
||||
log.Info(fmt.Sprintf("sevice version:%s", srvversion.SERVICE_VERSION))
|
||||
log.Info(fmt.Sprintf("git version:%s", srvversion.GIT_VERSION))
|
||||
log.Info(fmt.Sprintf("build date:%s", srvversion.BUILD_DATE))
|
||||
log.Info(fmt.Sprintf("build ip:%s", srvversion.BUILD_IP))
|
||||
}
|
||||
|
||||
func initHttpServer() {
|
||||
addr := fmt.Sprintf(":%s", appg.Conf.ListenAddr)
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router.Init(),
|
||||
}
|
||||
go func() {
|
||||
// service connections
|
||||
log.Info("服务已启动", log.Any("地址", addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("listen:", log.E(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func shutdownHandler() {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Kill)
|
||||
<-quit
|
||||
log.Warn("Shutdown... server")
|
||||
|
||||
done := make(chan int)
|
||||
go func() {
|
||||
seoskd.Stop()
|
||||
richtextser.Stop()
|
||||
configser.Stop()
|
||||
mongos.Shop.DisConnect()
|
||||
mongos.ShopLog.DisConnect()
|
||||
redis.Disconnect()
|
||||
tgbot.ReleaseTgBot()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Warn("shutdown timeout....")
|
||||
case <-done:
|
||||
log.Info("graceful shutdown")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package actressser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/moreser"
|
||||
"videoweb/app/service/slugser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/actress"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const homeActressPageSize int64 = 9
|
||||
const videoHomeActressPageSize int64 = 16
|
||||
|
||||
const (
|
||||
actressWorkDefaultPageSize int64 = 10
|
||||
actressWorkMaxPageSize int64 = 100
|
||||
actressWorkAdInterval int64 = 8
|
||||
)
|
||||
|
||||
// ActressListData 查询女优榜单列表,按热度值降序返回全部可用数据
|
||||
func ActressListData(ctx context.Context, path string) (*proto.QueryActressListResp, *srvdef.SrvErr) {
|
||||
list := configser.GetActressWorks()
|
||||
if len(list) == 0 {
|
||||
var err *srvdef.SrvErr
|
||||
list, err = queryActressListDataFromDB(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resp := &proto.QueryActressListResp{
|
||||
Path: path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
ActressInfoList: list,
|
||||
}
|
||||
configser.RewriteImageCdn(ctx, resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// HomeActressData 查询首页女优数据,按热度值降序取前 9 条
|
||||
func HomeActressData(ctx context.Context) ([]proto.ActressInfo, *srvdef.SrvErr) {
|
||||
return homeActressDataByCdn(ctx, homeActressPageSize, configser.GetConfigInfo().ImgCdn)
|
||||
}
|
||||
|
||||
// VideoHomeActressData 查询视频首页热门女优数据,按热度值降序取前 16 条
|
||||
func VideoHomeActressData(ctx context.Context) ([]proto.ActressInfo, *srvdef.SrvErr) {
|
||||
return homeActressDataByCdn(ctx, videoHomeActressPageSize, configser.ImageCdnFromCtx(ctx))
|
||||
}
|
||||
|
||||
// HomeActressDataByLimit 按热度值降序查询指定数量的女优数据
|
||||
func HomeActressDataByLimit(ctx context.Context, limit int64) ([]proto.ActressInfo, *srvdef.SrvErr) {
|
||||
return homeActressDataByCdn(ctx, limit, configser.ImageCdnFromCtx(ctx))
|
||||
}
|
||||
|
||||
func homeActressDataByCdn(ctx context.Context, limit int64, imgCdn string) ([]proto.ActressInfo, *srvdef.SrvErr) {
|
||||
if limit <= 0 {
|
||||
limit = homeActressPageSize
|
||||
}
|
||||
list, err := actress.FindManyActresss(ctx, bson.M{
|
||||
actress.FN.Actress.Status: true,
|
||||
}, homeActressFindOptions(limit))
|
||||
if err != nil {
|
||||
log.Warn("查询首页女优数据失败", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
data := make([]proto.ActressInfo, 0, len(list))
|
||||
for _, item := range list {
|
||||
data = append(data, homeActressInfo(item, imgCdn))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// queryActressListDataFromDB 回源查询女优榜单,使用字段投影减少 MongoDB 返回体积
|
||||
func queryActressListDataFromDB(ctx context.Context) ([]proto.ActressInfoData, *srvdef.SrvErr) {
|
||||
config := configser.GetConfigInfo()
|
||||
list, err := actress.FindManyActresss(ctx, bson.M{
|
||||
actress.FN.Actress.Status: true,
|
||||
}, options.Find().
|
||||
SetSort(bson.D{{Key: actress.FN.Actress.HotValue, Value: -1}, {Key: actress.FN.Actress.Id, Value: -1}}).
|
||||
SetProjection(bson.M{
|
||||
actress.FN.Actress.Id: 1,
|
||||
actress.FN.Actress.Alias: 1,
|
||||
actress.FN.Actress.Slug: 1,
|
||||
actress.FN.Actress.Avatar: 1,
|
||||
actress.FN.Actress.Cover: 1,
|
||||
actress.FN.Actress.Desc: 1,
|
||||
actress.FN.Actress.Office: 1,
|
||||
actress.FN.Actress.DebutDate: 1,
|
||||
actress.FN.Actress.BornDate: 1,
|
||||
actress.FN.Actress.Region: 1,
|
||||
actress.FN.Actress.Score: 1,
|
||||
actress.FN.Actress.Works: 1,
|
||||
actress.FN.Actress.HotValue: 1,
|
||||
actress.FN.Actress.SeoInfo: 1,
|
||||
actress.FN.Actress.Meas.Gender: 1,
|
||||
actress.FN.Actress.Meas.Height: 1,
|
||||
actress.FN.Actress.Meas.Weight: 1,
|
||||
actress.FN.Actress.Meas.Bust: 1,
|
||||
actress.FN.Actress.Meas.Waist: 1,
|
||||
actress.FN.Actress.Meas.Hip: 1,
|
||||
actress.FN.Actress.Meas.Cup: 1,
|
||||
actress.FN.Actress.Meas.Len: 1,
|
||||
actress.FN.Actress.Meas.Dia: 1,
|
||||
actress.FN.Actress.Meas.Blood: 1,
|
||||
actress.FN.Actress.Real.WatchTimes: 1,
|
||||
actress.FN.Actress.Real.ShowTimes: 1,
|
||||
actress.FN.Actress.Real.Comments: 1,
|
||||
}))
|
||||
if err != nil {
|
||||
log.Warn("查询女优榜单数据失败", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
data := make([]proto.ActressInfoData, 0, len(list))
|
||||
for _, item := range list {
|
||||
data = append(data, proto.ActressInfoToData(*item, config.ImgCdn))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// homeActressFindOptions 限定首页女优查询数量、排序和字段投影,减少 MongoDB 返回数据量
|
||||
func homeActressFindOptions(limit int64) *options.FindOptions {
|
||||
return options.Find().
|
||||
SetLimit(limit).
|
||||
SetSort(bson.D{{Key: actress.FN.Actress.HotValue, Value: -1}, {Key: actress.FN.Actress.Id, Value: -1}}).
|
||||
SetProjection(bson.M{
|
||||
actress.FN.Actress.Id: 1,
|
||||
actress.FN.Actress.Alias: 1,
|
||||
actress.FN.Actress.Slug: 1,
|
||||
actress.FN.Actress.Avatar: 1,
|
||||
actress.FN.Actress.Works: 1,
|
||||
actress.FN.Actress.HotValue: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// homeActressInfo 将女优模型转换为首页轻量女优结构
|
||||
func homeActressInfo(item *actress.Actress, imgCdn string) proto.ActressInfo {
|
||||
return proto.ActressInfo{
|
||||
Id: item.Id,
|
||||
Alias: item.Alias,
|
||||
Slug: item.Slug,
|
||||
Avatar: netutil.MergeURL(imgCdn, item.Avatar),
|
||||
Works: item.Works,
|
||||
HotValue: item.HotValue,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryActressWorkList 查询指定女优详情和作品混排列表
|
||||
func QueryActressWorkList(ctx context.Context, req *proto.QueryActressWorkListReq) (*proto.QueryActressWorkListResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
actressId, err := slugser.ActressID(ctx, req.Id, req.Slug, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Id = actressId
|
||||
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, actressWorkDefaultPageSize, actressWorkMaxPageSize)
|
||||
config := configser.GetRequestConfig(ctx)
|
||||
actressInfo := configser.GetActressInfo(req.Id)
|
||||
if actressInfo == nil {
|
||||
log.Warn("女优详情不存在", log.Any("actressId", req.Id))
|
||||
return nil, srvdef.New(srvdef.AtressNotExist)
|
||||
}
|
||||
filter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.ActressIds: bson.M{"$in": []int64{req.Id}},
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
}
|
||||
totalCount, err := videoinformationmod.CountVideoInformations(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("统计女优作品总数失败", log.Any("actressId", req.Id), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
ads := configser.GetAdvitise().PostList
|
||||
adsTotalCount := moreser.InsertedAdsCount(totalCount, int64(len(ads)), actressWorkAdInterval)
|
||||
mixedTotalCount := totalCount + adsTotalCount
|
||||
resp := &proto.QueryActressWorkListResp{
|
||||
BasePageResp: response.BasePageResp{
|
||||
TotalCount: mixedTotalCount,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]*proto.QueryVideoListItem, 0),
|
||||
},
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
ActressInfo: proto.ActressInfoToData(*actressInfo, config.ImgCdn),
|
||||
}
|
||||
if totalCount == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
start := (pageNum - 1) * pageSize
|
||||
if start >= mixedTotalCount {
|
||||
return resp, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > mixedTotalCount {
|
||||
end = mixedTotalCount
|
||||
}
|
||||
contentStart, contentLimit := moreser.ContentRange(start, end, totalCount, adsTotalCount, actressWorkAdInterval)
|
||||
list := videoinformationmod.VideoInformationList{}
|
||||
if contentLimit > 0 {
|
||||
list, err = videoinformationmod.FindManyVideoInformations(ctx, filter, options.Find().
|
||||
SetSort(actressWorkSort(req)).
|
||||
SetProjection(actressWorkProjection()).
|
||||
SetSkip(contentStart).
|
||||
SetLimit(contentLimit))
|
||||
if err != nil {
|
||||
log.Warn("查询女优作品列表失败", log.Any("actressId", req.Id), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
mediaList := buildActressWorkMixedListWithoutTotal(list, ads, config.ImgCdn, start, end, contentStart, adsTotalCount)
|
||||
resp.List = mediaList
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// actressWorkSort 观看量和收藏量按真实数据 real.watchTimes、real.likes 排序,不使用 fake 数据
|
||||
func actressWorkSort(req *proto.QueryActressWorkListReq) bson.D {
|
||||
sort := make(bson.D, 0, 4)
|
||||
if dir := actressWorkSortDir(req.AddedTimeSort); dir != 0 {
|
||||
sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: dir})
|
||||
}
|
||||
if dir := actressWorkSortDir(req.WatchTimesSort); dir != 0 {
|
||||
sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Real.WatchTimes, Value: dir})
|
||||
}
|
||||
if dir := actressWorkSortDir(req.LikesSort); dir != 0 {
|
||||
sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Real.Likes, Value: dir})
|
||||
}
|
||||
if len(sort) > 0 {
|
||||
sort = append(sort, bson.E{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1})
|
||||
return sort
|
||||
}
|
||||
return bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.VideoCount, Value: -1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.AddedTime, Value: -1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: -1},
|
||||
}
|
||||
}
|
||||
|
||||
// actressWorkProjection 限定作品列表卡片字段,避免读取详情页大字段和视频节点。
|
||||
func actressWorkProjection() bson.M {
|
||||
return bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: 1,
|
||||
videoinformationmod.FN.VideoInformation.Title: 1,
|
||||
videoinformationmod.FN.VideoInformation.Slug: 1,
|
||||
videoinformationmod.FN.VideoInformation.AddedTime: 1,
|
||||
videoinformationmod.FN.VideoInformation.CoverImg: 1,
|
||||
videoinformationmod.FN.VideoInformation.Tags: 1,
|
||||
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
|
||||
videoinformationmod.FN.VideoInformation.Fake.WatchTimes: 1,
|
||||
videoinformationmod.FN.VideoInformation.Real.Likes: 1,
|
||||
videoinformationmod.FN.VideoInformation.Fake.Likes: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// actressWorkSortDir 将接口排序值转换为 MongoDB 排序方向
|
||||
func actressWorkSortDir(sortType int64) int {
|
||||
switch sortType {
|
||||
case 1:
|
||||
return 1
|
||||
case 2:
|
||||
return -1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// buildActressWorkMixedListWithoutTotal 根据无总数混排分页窗口组装当前页列表
|
||||
func buildActressWorkMixedListWithoutTotal(list videoinformationmod.VideoInformationList, ads []proxy.AdvitiseInfo, cdn string, start, end, contentStart, adsTotal int64) []*proto.QueryVideoListItem {
|
||||
resp := make([]*proto.QueryVideoListItem, 0, end-start)
|
||||
contentByIndex := make(map[int64]*videoinformationmod.VideoInformation, len(list))
|
||||
for i, item := range list {
|
||||
contentByIndex[contentStart+int64(i)] = item
|
||||
}
|
||||
for pos := start; pos < end; pos++ {
|
||||
adIndex := moreser.AdIndexAtPositionWithoutTotal(pos, adsTotal, actressWorkAdInterval)
|
||||
if adIndex >= 0 && adIndex < adsTotal {
|
||||
resp = append(resp, actressWorkAdToListItem(ads[adIndex], cdn))
|
||||
continue
|
||||
}
|
||||
contentIndex := moreser.ContentIndexAtPositionWithoutTotal(pos, adsTotal, actressWorkAdInterval)
|
||||
if item := contentByIndex[contentIndex]; item != nil {
|
||||
resp = append(resp, actressWorkVideoToListItem(item, cdn))
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// actressWorkVideoToListItem 将视频模型转换为女优作品列表中的视频项
|
||||
func actressWorkVideoToListItem(item *videoinformationmod.VideoInformation, cdn string) *proto.QueryVideoListItem {
|
||||
tagNames, tagSlugs := configser.VideoTagNamesAndSlugs(item.Tags)
|
||||
return &proto.QueryVideoListItem{
|
||||
Type: proto.QueryVideoItemTypeVideo,
|
||||
VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Slug: item.Slug,
|
||||
AddedTime: item.AddedTime,
|
||||
CoverImg: netutil.MergeURL(cdn, item.CoverImg),
|
||||
Tags: item.Tags,
|
||||
TagsName: tagNames,
|
||||
TagSlugs: tagSlugs,
|
||||
WatchNum: item.Real.WatchTimes + item.Fake.WatchTimes,
|
||||
LikeNum: item.Real.Likes + item.Fake.Likes,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// actressWorkAdToListItem 将列表广告转换为女优作品列表中的广告项
|
||||
func actressWorkAdToListItem(ad proxy.AdvitiseInfo, cdn string) *proto.QueryVideoListItem {
|
||||
return &proto.QueryVideoListItem{
|
||||
Type: proto.QueryVideoItemTypeAdvertise,
|
||||
AdvertiseId: ad.Id,
|
||||
Link: ad.Link,
|
||||
Location: ad.Location,
|
||||
VideoInfo: proto.VideoInfo{ContentData: proto.ContentData{
|
||||
Title: ad.Title,
|
||||
CoverImg: netutil.MergeURL(cdn, ad.Images),
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package appser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common/srvdef"
|
||||
)
|
||||
|
||||
// AppCategoryData 查询应用分类列表及当前分类数据
|
||||
func AppCategoryData(ctx context.Context, categoryId int64) ([]proto.AppCategoryInfo, proto.AppCategoryInfo, *srvdef.SrvErr) {
|
||||
list := GetAppListCache()
|
||||
category := GetCategoryCache(categoryId)
|
||||
configser.RewriteImageCdn(ctx, &list)
|
||||
configser.RewriteImageCdn(ctx, &category)
|
||||
return list, category, nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package appser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common/http"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
const appListAPIPath = "/media/web/appinfo/applist"
|
||||
|
||||
var jsonHeader = map[string]string{"Content-Type": "application/json"}
|
||||
|
||||
var (
|
||||
appListCron *cron.Cron
|
||||
appListCache struct {
|
||||
lock sync.RWMutex
|
||||
appList []proto.AppCategoryInfo
|
||||
categoryMaps map[int64]proto.AppCategoryInfo
|
||||
}
|
||||
)
|
||||
|
||||
type remoteAppListResp struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
List []proto.AppCategoryInfo `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Start 启动定时刷新应用列表缓存(每10分钟一次)
|
||||
func Start() {
|
||||
appListCron = cron.New(cron.WithSeconds())
|
||||
UpdateAppListCache(context.Background())
|
||||
_, _ = appListCron.AddFunc("0 */10 * * * ?", func() {
|
||||
UpdateAppListCache(context.Background())
|
||||
})
|
||||
appListCron.Start()
|
||||
}
|
||||
|
||||
// GetAppListCache 返回全部应用分类列表(带各分类下应用)
|
||||
func GetAppListCache() []proto.AppCategoryInfo {
|
||||
appListCache.lock.RLock()
|
||||
defer appListCache.lock.RUnlock()
|
||||
if len(appListCache.appList) == 0 {
|
||||
return make([]proto.AppCategoryInfo, 0)
|
||||
}
|
||||
result := make([]proto.AppCategoryInfo, len(appListCache.appList))
|
||||
copy(result, appListCache.appList)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCategoryCache 按分类ID返回单个分类(含应用列表);categoryId=0 返回第一个分类
|
||||
func GetCategoryCache(categoryId int64) proto.AppCategoryInfo {
|
||||
appListCache.lock.RLock()
|
||||
defer appListCache.lock.RUnlock()
|
||||
if categoryId == 0 && len(appListCache.appList) > 0 {
|
||||
return appListCache.appList[0]
|
||||
}
|
||||
if v, ok := appListCache.categoryMaps[categoryId]; ok {
|
||||
return v
|
||||
}
|
||||
return proto.AppCategoryInfo{AppList: make([]proto.AppInfoData, 0)}
|
||||
}
|
||||
|
||||
// UpdateAppListCache 从远端拉取应用列表并刷新内存缓存
|
||||
func UpdateAppListCache(ctx context.Context) {
|
||||
domain := proxy.DomainAddressRepair("", proxy.BucketVideoAddressV2)
|
||||
apiURL := strings.TrimSuffix(domain, "/") + appListAPIPath
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]string{"dc": "dc"})
|
||||
resp, err := http.Post(apiURL, jsonHeader, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
log.Warn("UpdateAppListCache http.Post failed", log.Any("url", apiURL), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
var remote remoteAppListResp
|
||||
if err = json.Unmarshal(resp.Data, &remote); err != nil {
|
||||
log.Warn("UpdateAppListCache unmarshal failed", log.Any("url", apiURL), log.E(err))
|
||||
return
|
||||
}
|
||||
if remote.Code != 200 {
|
||||
log.Warn("UpdateAppListCache bad code", log.Any("url", apiURL), log.Any("code", remote.Code))
|
||||
return
|
||||
}
|
||||
|
||||
config := configser.GetConfigInfo()
|
||||
advitise := configser.GetAdvitise()
|
||||
channelCode := config.ChannelCode
|
||||
|
||||
categoryMaps := make(map[int64]proto.AppCategoryInfo)
|
||||
var appList []proto.AppCategoryInfo
|
||||
|
||||
for _, category := range remote.Data.List {
|
||||
var apps []proto.AppInfoData
|
||||
for _, app := range category.AppList {
|
||||
// 过滤掉本应用自身
|
||||
if strings.HasPrefix(app.Name, pubdef.AppName) {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, proto.AppInfoData{
|
||||
Id: app.Id,
|
||||
CategoryId: app.CategoryId,
|
||||
Name: app.Name,
|
||||
Intro: app.Intro,
|
||||
Description: app.Description,
|
||||
Logo: netutil.MergeURL(config.ImgCdn, app.Logo),
|
||||
Url: appendChannelCode(app.Url, channelCode),
|
||||
Sort: app.Sort,
|
||||
})
|
||||
}
|
||||
// 广告位 A(code=12)注入图标广告
|
||||
if category.Code == 12 {
|
||||
for _, ad := range advitise.AList {
|
||||
apps = append(apps, proto.AppInfoData{
|
||||
AdvitiseId: ad.Id,
|
||||
CategoryId: category.Id,
|
||||
Name: ad.Title,
|
||||
Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
|
||||
Url: ad.Link,
|
||||
Location: ad.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 广告位 B(code=13)注入图标文字广告
|
||||
if category.Code == 13 {
|
||||
for _, ad := range advitise.BList {
|
||||
apps = append(apps, proto.AppInfoData{
|
||||
AdvitiseId: ad.Id,
|
||||
CategoryId: category.Id,
|
||||
Name: ad.Title,
|
||||
Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
|
||||
Url: ad.Link,
|
||||
Location: ad.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 空分类不展示
|
||||
if len(apps) == 0 {
|
||||
continue
|
||||
}
|
||||
category.AppList = apps
|
||||
categoryMaps[category.Id] = proto.AppCategoryInfo{
|
||||
Id: category.Id,
|
||||
Title: category.Title,
|
||||
Intro: category.Intro,
|
||||
Sort: category.Sort,
|
||||
Code: category.Code,
|
||||
}
|
||||
appList = append(appList, category)
|
||||
}
|
||||
|
||||
appListCache.lock.Lock()
|
||||
appListCache.appList = appList
|
||||
appListCache.categoryMaps = categoryMaps
|
||||
appListCache.lock.Unlock()
|
||||
}
|
||||
|
||||
// appendChannelCode 为应用 URL 拼接渠道码参数 dc=xxx;已含 dc 参数则不再拼接
|
||||
func appendChannelCode(rawURL, channelCode string) string {
|
||||
if len(channelCode) == 0 {
|
||||
channelCode = pubdef.DefaultChannelCode
|
||||
}
|
||||
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
channelCode = strings.TrimSpace(channelCode)
|
||||
if rawURL == "" || channelCode == "" {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
query := parsed.Query()
|
||||
if query.Has("dc") {
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
query.Set("dc", channelCode)
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var tokener = utils.NewTokenRedis()
|
||||
|
||||
var defaultAvatars = []string{
|
||||
"v3/image/bo/ty/1016/girl1.jpeg",
|
||||
"v3/image/re/9y/1016/girl2.jpeg",
|
||||
"v3/image/5f/k4/1016/girl3.jpeg",
|
||||
"v3/image/ej/ss/1016/girl4.jpeg",
|
||||
"v3/image/yq/ys/1016/girl5.jpeg",
|
||||
"v3/image/9e/d3/1016/girl6.jpeg",
|
||||
}
|
||||
|
||||
func randomAvatar() string {
|
||||
return defaultAvatars[time.Now().UnixNano()%int64(len(defaultAvatars))]
|
||||
}
|
||||
|
||||
// generateUniqueInviteCode 生成 8 位唯一邀请码(含大小写字母),最多重试 10 次
|
||||
func generateUniqueInviteCode(ctx context.Context) string {
|
||||
for i := 0; i < 10; i++ {
|
||||
code := utils.GetRandomLetters(8)
|
||||
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.InviteCode: code})
|
||||
if exist == nil {
|
||||
return code
|
||||
}
|
||||
log.Warn("authser:generateUniqueInviteCode collision, retrying", log.Any("code", code))
|
||||
}
|
||||
// 极小概率走到这里:用更长的随机串兜底
|
||||
return utils.GetRandomLetters(12)
|
||||
}
|
||||
|
||||
// LoginResp 登录/注册成功后的响应数据
|
||||
type LoginResp struct {
|
||||
Token string `json:"token"`
|
||||
UserId int64 `json:"userId"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
NickName string `json:"nickName"`
|
||||
Status pubdef.UserStatus `json:"status"`
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
InviteCode string `json:"inviteCode"` // 邀请码
|
||||
LoginType pubdef.LoginType `json:"loginType"` // 登录方式 0=设备 3=账号
|
||||
IsRegister bool `json:"isRegister"` // 是否已完成账号注册
|
||||
IsNewUser bool `json:"isNewUser"` // 是否本次请求新创建的用户
|
||||
Recharge int64 `json:"recharge"` // 当前金币个数
|
||||
}
|
||||
|
||||
// UserInfoResp 当前登录用户信息
|
||||
type UserInfoResp struct {
|
||||
UserId int64 `json:"userId"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
NickName string `json:"nickName"`
|
||||
Status pubdef.UserStatus `json:"status"`
|
||||
Avatar string `json:"avatar"`
|
||||
InviteCode string `json:"inviteCode"`
|
||||
LoginType pubdef.LoginType `json:"loginType"`
|
||||
IsRegister bool `json:"isRegister"`
|
||||
Recharge int64 `json:"recharge"`
|
||||
}
|
||||
|
||||
// UAInfo 从 UA 中间件提取的设备/系统信息
|
||||
type UAInfo struct {
|
||||
Device string // 设备型号(DevType,如 "iPhone")
|
||||
SysType pubdef.SysType // 系统类型(ios / android / h5 等)
|
||||
AppVer string // App 版本号
|
||||
IP string // 客户端 IP
|
||||
}
|
||||
|
||||
// GuestLoginReq 游客登录请求
|
||||
type GuestLoginReq struct {
|
||||
DevID string // 设备 ID(首选)
|
||||
Mac string // 设备 MAC 地址(备用)
|
||||
DeviceId string // 设备 ID 第二版本(备用)
|
||||
UA UAInfo // 设备/系统信息
|
||||
}
|
||||
|
||||
// hashPassword 使用 bcrypt 对明文密码进行哈希
|
||||
func hashPassword(plain string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// checkPassword 验证明文密码与 bcrypt 哈希是否匹配
|
||||
func checkPassword(plain, hash string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
|
||||
}
|
||||
|
||||
// generateAndSaveToken 生成 JWT 并存入 Redis
|
||||
func generateAndSaveToken(userId int64) (string, *srvdef.SrvErr) {
|
||||
token, err := utils.GenerateUserToken(utils.UserClaims{
|
||||
UserID: userId,
|
||||
Expire: consts.UserJwtTokenExpire,
|
||||
}, appg.Conf.JwtKey)
|
||||
if err != nil {
|
||||
return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
|
||||
}
|
||||
if err = tokener.Save(userId, token); err != nil {
|
||||
return "", srvdef.New(srvdef.StoreLevel, "token存储失败", "")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GuestLogin 游客登录:按设备 ID 查找已有用户,找不到则自动注册
|
||||
// 若请求携带有效 token(currentUserId != 0),优先复用该账号
|
||||
func GuestLogin(ctx context.Context, req *GuestLoginReq, currentUserId int64) (*LoginResp, *srvdef.SrvErr) {
|
||||
var user *usermod.User
|
||||
|
||||
// 优先用 token 对应的已有用户
|
||||
if currentUserId > 0 {
|
||||
u, _ := usermod.FindOneUserById(ctx, currentUserId)
|
||||
if u != nil {
|
||||
user = u
|
||||
}
|
||||
}
|
||||
|
||||
// 按设备 ID 查找
|
||||
if user == nil {
|
||||
user, _ = findUserByDevice(ctx, req.DevID, req.Mac, req.DeviceId)
|
||||
}
|
||||
|
||||
isNewUser := false
|
||||
if user == nil {
|
||||
// 未找到 → 自动注册游客
|
||||
var serr *srvdef.SrvErr
|
||||
user, serr = registerGuest(ctx, req)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
isNewUser = true
|
||||
} else {
|
||||
// 已有用户:更新设备信息和 UA(静默,不影响主流程)
|
||||
updateSet := bson.M{
|
||||
usermod.FN.User.LastLoginAt: time.Now(),
|
||||
usermod.FN.User.UpdatedAt: time.Now(),
|
||||
}
|
||||
if req.DevID != "" {
|
||||
updateSet[usermod.FN.User.DevID] = req.DevID
|
||||
}
|
||||
if req.Mac != "" && req.Mac != "00:00:00:00:00:00" {
|
||||
updateSet[usermod.FN.User.Mac] = req.Mac
|
||||
}
|
||||
if req.DeviceId != "" {
|
||||
updateSet[usermod.FN.User.DevV2] = req.DeviceId
|
||||
}
|
||||
if req.UA.Device != "" {
|
||||
updateSet[usermod.FN.User.Device] = req.UA.Device
|
||||
}
|
||||
if req.UA.SysType != "" {
|
||||
updateSet[usermod.FN.User.SysType] = req.UA.SysType.String()
|
||||
}
|
||||
if req.UA.AppVer != "" {
|
||||
updateSet[usermod.FN.User.AppVer] = req.UA.AppVer
|
||||
}
|
||||
if req.UA.IP != "" {
|
||||
updateSet[usermod.FN.User.LastIp] = req.UA.IP
|
||||
}
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": updateSet},
|
||||
)
|
||||
}
|
||||
|
||||
if user.Status == pubdef.UserStatusDisable {
|
||||
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
|
||||
token, serr := generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
cdn := configser.ImageCdnFromCtx(ctx)
|
||||
return &LoginResp{
|
||||
Token: token,
|
||||
UserId: user.Id,
|
||||
AccountNo: user.AccountNo,
|
||||
NickName: user.NickName,
|
||||
Status: user.Status,
|
||||
Avatar: netutil.MergeURL(cdn, user.Avatar),
|
||||
InviteCode: user.InviteCode,
|
||||
LoginType: user.LoginType,
|
||||
IsRegister: user.IsRegister,
|
||||
IsNewUser: isNewUser,
|
||||
Recharge: user.Recharge,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 根据当前登录 token 解析出的用户 ID 查询用户信息
|
||||
func GetInfo(ctx context.Context, userId int64) (*UserInfoResp, *srvdef.SrvErr) {
|
||||
if userId <= 0 {
|
||||
return nil, srvdef.New(srvdef.InvalidToken, srvdef.InvalidToken.Msg(), "")
|
||||
}
|
||||
user, serr := usermod.FindOneUserById(ctx, userId, options.FindOne().SetProjection(userInfoProjection()))
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if user == nil {
|
||||
return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
|
||||
}
|
||||
if user.Status == pubdef.UserStatusDisable {
|
||||
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
cdn := configser.ImageCdnFromCtx(ctx)
|
||||
return &UserInfoResp{
|
||||
UserId: user.Id,
|
||||
AccountNo: user.AccountNo,
|
||||
NickName: user.NickName,
|
||||
Status: user.Status,
|
||||
Avatar: netutil.MergeURL(cdn, user.Avatar),
|
||||
InviteCode: user.InviteCode,
|
||||
LoginType: user.LoginType,
|
||||
IsRegister: user.IsRegister,
|
||||
Recharge: user.Recharge,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func userInfoProjection() bson.M {
|
||||
return bson.M{
|
||||
usermod.FN.User.Id: 1,
|
||||
usermod.FN.User.AccountNo: 1,
|
||||
usermod.FN.User.NickName: 1,
|
||||
usermod.FN.User.Status: 1,
|
||||
usermod.FN.User.Avatar: 1,
|
||||
usermod.FN.User.InviteCode: 1,
|
||||
usermod.FN.User.LoginType: 1,
|
||||
usermod.FN.User.IsRegister: 1,
|
||||
usermod.FN.User.Recharge: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// findUserByDevice 按设备 ID 三级回退查找用户(DevID → Mac → DeviceId)
|
||||
func findUserByDevice(ctx context.Context, devID, mac, deviceId string) (*usermod.User, *srvdef.SrvErr) {
|
||||
invalidMac := "00:00:00:00:00:00"
|
||||
if devID != "" {
|
||||
u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: devID})
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
if mac != "" && mac != invalidMac {
|
||||
u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac})
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
if deviceId != "" && deviceId != invalidMac {
|
||||
u, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId})
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// registerGuest 自动创建游客用户
|
||||
func registerGuest(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) {
|
||||
now := time.Now()
|
||||
user := &usermod.User{
|
||||
Id: mongos.Lid.User.Get(),
|
||||
NickName: getName(),
|
||||
Avatar: randomAvatar(),
|
||||
InviteCode: generateUniqueInviteCode(ctx),
|
||||
Status: pubdef.UserStatusNormal,
|
||||
LoginType: pubdef.LoginTypeDevice,
|
||||
IsRegister: false,
|
||||
DevID: req.DevID,
|
||||
Mac: req.Mac,
|
||||
DevV2: req.DeviceId,
|
||||
Device: req.UA.Device,
|
||||
SysType: req.UA.SysType.String(),
|
||||
AppVer: req.UA.AppVer,
|
||||
OriVer: req.UA.AppVer,
|
||||
RegIp: req.UA.IP,
|
||||
LastIp: req.UA.IP,
|
||||
RegisterTime: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Register 账号注册
|
||||
// currentUserId != 0 时为游客升级模式:在已有游客账号上绑定账号密码
|
||||
// currentUserId == 0 时为直接注册新账号
|
||||
func Register(ctx context.Context, accountNo, password, agentInviteCode string, currentUserId int64, ua UAInfo) (*LoginResp, *srvdef.SrvErr) {
|
||||
// 检查 accountNo 是否已被其他用户占用
|
||||
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo})
|
||||
if exist != nil && exist.Id != currentUserId {
|
||||
return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "")
|
||||
}
|
||||
|
||||
hashed, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.Failure, "密码处理失败", "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 游客升级:更新已有记录
|
||||
if currentUserId > 0 {
|
||||
user, _ := usermod.FindOneUserById(ctx, currentUserId)
|
||||
if user == nil {
|
||||
return nil, srvdef.New(srvdef.ErrUserNotExsit, "用户不存在", "")
|
||||
}
|
||||
if user.IsRegister {
|
||||
return nil, srvdef.New(srvdef.ErrDbInputExist, "已完成账号注册,无需重复操作", "")
|
||||
}
|
||||
setDoc := bson.M{
|
||||
usermod.FN.User.AccountNo: accountNo,
|
||||
usermod.FN.User.Password: hashed,
|
||||
usermod.FN.User.IsRegister: true,
|
||||
usermod.FN.User.LoginType: pubdef.LoginTypeAccount,
|
||||
usermod.FN.User.UpdatedAt: now,
|
||||
}
|
||||
if agentInviteCode != "" && user.AgentInviteCode == "" {
|
||||
setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode
|
||||
}
|
||||
if ua.Device != "" {
|
||||
setDoc[usermod.FN.User.Device] = ua.Device
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
setDoc[usermod.FN.User.SysType] = ua.SysType.String()
|
||||
}
|
||||
if ua.AppVer != "" {
|
||||
setDoc[usermod.FN.User.AppVer] = ua.AppVer
|
||||
}
|
||||
if ua.IP != "" {
|
||||
setDoc[usermod.FN.User.LastIp] = ua.IP
|
||||
}
|
||||
if serr := usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: currentUserId},
|
||||
bson.M{"$set": setDoc},
|
||||
); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
// 给邀请人 +1
|
||||
if agentInviteCode != "" {
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.InviteCode: agentInviteCode},
|
||||
bson.M{"$inc": bson.M{usermod.FN.User.Invites: 1},
|
||||
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()}},
|
||||
)
|
||||
}
|
||||
token, serr := generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
cdn := configser.ImageCdnFromCtx(ctx)
|
||||
return &LoginResp{
|
||||
Token: token,
|
||||
UserId: user.Id,
|
||||
AccountNo: accountNo,
|
||||
NickName: user.NickName,
|
||||
Status: user.Status,
|
||||
Avatar: netutil.MergeURL(cdn, user.Avatar),
|
||||
InviteCode: user.InviteCode,
|
||||
LoginType: pubdef.LoginTypeAccount,
|
||||
IsRegister: true,
|
||||
Recharge: user.Recharge,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 未登录:直接注册新账号
|
||||
user := &usermod.User{
|
||||
Id: mongos.Lid.User.Get(),
|
||||
AccountNo: accountNo,
|
||||
Password: hashed,
|
||||
NickName: getName(),
|
||||
Avatar: randomAvatar(),
|
||||
InviteCode: generateUniqueInviteCode(ctx),
|
||||
AgentInviteCode: agentInviteCode,
|
||||
Status: pubdef.UserStatusNormal,
|
||||
IsRegister: true,
|
||||
LoginType: pubdef.LoginTypeAccount,
|
||||
Device: ua.Device,
|
||||
SysType: ua.SysType.String(),
|
||||
AppVer: ua.AppVer,
|
||||
OriVer: ua.AppVer,
|
||||
RegIp: ua.IP,
|
||||
LastIp: ua.IP,
|
||||
RegisterTime: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if agentInviteCode != "" {
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.InviteCode: agentInviteCode},
|
||||
bson.M{
|
||||
"$inc": bson.M{usermod.FN.User.Invites: 1},
|
||||
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
|
||||
},
|
||||
)
|
||||
}
|
||||
token, serr := generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
cdn := configser.ImageCdnFromCtx(ctx)
|
||||
return &LoginResp{
|
||||
Token: token,
|
||||
UserId: user.Id,
|
||||
AccountNo: user.AccountNo,
|
||||
NickName: user.NickName,
|
||||
Status: user.Status,
|
||||
Avatar: netutil.MergeURL(cdn, user.Avatar),
|
||||
InviteCode: user.InviteCode,
|
||||
LoginType: pubdef.LoginTypeAccount,
|
||||
IsRegister: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Login 用户登录(账号均可)
|
||||
func Login(ctx context.Context, account, password string, ua UAInfo) (*LoginResp, *srvdef.SrvErr) {
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: account})
|
||||
if user == nil {
|
||||
return nil, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "")
|
||||
}
|
||||
|
||||
if user.Status == pubdef.UserStatusDisable {
|
||||
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
|
||||
if !checkPassword(password, user.Password) {
|
||||
return nil, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
|
||||
}
|
||||
|
||||
token, serr := generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
// 更新最后登录时间、IP 及 UA(fire-and-forget)
|
||||
loginSet := bson.M{
|
||||
usermod.FN.User.LastLoginAt: time.Now(),
|
||||
usermod.FN.User.UpdatedAt: time.Now(),
|
||||
}
|
||||
if ua.IP != "" {
|
||||
loginSet[usermod.FN.User.LastIp] = ua.IP
|
||||
}
|
||||
if ua.Device != "" {
|
||||
loginSet[usermod.FN.User.Device] = ua.Device
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
loginSet[usermod.FN.User.SysType] = ua.SysType.String()
|
||||
}
|
||||
if ua.AppVer != "" {
|
||||
loginSet[usermod.FN.User.AppVer] = ua.AppVer
|
||||
}
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": loginSet},
|
||||
)
|
||||
cdn := configser.ImageCdnFromCtx(ctx)
|
||||
return &LoginResp{
|
||||
Token: token,
|
||||
UserId: user.Id,
|
||||
AccountNo: user.AccountNo,
|
||||
NickName: user.NickName,
|
||||
Status: user.Status,
|
||||
Avatar: netutil.MergeURL(cdn, user.Avatar),
|
||||
InviteCode: user.InviteCode,
|
||||
LoginType: user.LoginType,
|
||||
IsRegister: user.IsRegister,
|
||||
Recharge: user.Recharge,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
|
||||
type UpdateProfileReq struct {
|
||||
AccountNo string // 用户账号(用于定位用户,必传)
|
||||
Avatar *string // 新头像 URL(可选)
|
||||
NikeName *string // 昵称
|
||||
OldPassword *string // 旧密码明文(修改密码时必传)
|
||||
NewPassword *string // 新密码明文(修改密码时必传)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改用户资料:支持修改头像、密码,或两者同时修改
|
||||
func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr {
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: req.AccountNo})
|
||||
if user == nil {
|
||||
return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
|
||||
}
|
||||
|
||||
setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()}
|
||||
|
||||
// 修改头像
|
||||
if req.Avatar != nil && *req.Avatar != "" {
|
||||
setDoc[usermod.FN.User.Avatar] = *req.Avatar
|
||||
}
|
||||
if req.NikeName != nil && *req.NikeName != "" {
|
||||
setDoc[usermod.FN.User.NickName] = *req.NikeName
|
||||
}
|
||||
|
||||
// 修改密码:两者必须同时提供
|
||||
if req.OldPassword != nil || req.NewPassword != nil {
|
||||
if req.OldPassword == nil || req.NewPassword == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "")
|
||||
}
|
||||
if !checkPassword(*req.OldPassword, user.Password) {
|
||||
return srvdef.New(srvdef.AccountErrPasswordError, "旧密码错误", "")
|
||||
}
|
||||
hashed, err := hashPassword(*req.NewPassword)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.Failure, "密码处理失败", "")
|
||||
}
|
||||
setDoc[usermod.FN.User.Password] = hashed
|
||||
}
|
||||
|
||||
// 没有任何需要更新的字段
|
||||
if len(setDoc) == 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "")
|
||||
}
|
||||
|
||||
return usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
)
|
||||
}
|
||||
|
||||
func getName() string {
|
||||
const limit = 300
|
||||
totalCnt, err := redis.SCard(redis.NameSource)
|
||||
if err != nil || totalCnt < limit {
|
||||
if len(appg.Name.FamilyNames) == 0 || len(appg.Name.AfterNames) == 0 {
|
||||
// fmt.Println(totalCnt, err, appg.Name.FamilyNames)
|
||||
return "游客" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
}
|
||||
for i := 0; i < limit/100; i++ {
|
||||
go func() {
|
||||
var names []string
|
||||
for j := 0; j < limit/3; j++ {
|
||||
length := 2 + rand.Intn(2)
|
||||
if length == 2 {
|
||||
name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] +
|
||||
appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))]
|
||||
names = append(names, name)
|
||||
|
||||
}
|
||||
if length == 3 {
|
||||
name := appg.Name.FamilyNames[utils.RandInt(0, len(appg.Name.FamilyNames))] +
|
||||
appg.Name.MiddleNames[utils.RandInt(0, len(appg.Name.MiddleNames))] +
|
||||
appg.Name.AfterNames[utils.RandInt(0, len(appg.Name.AfterNames))]
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
_ = redis.SAdd(redis.NameSource, names)
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
_ = redis.Expire(redis.NameSource, redis.NameSourceExpire)
|
||||
}
|
||||
|
||||
name, err := redis.SPop(redis.NameSource)
|
||||
if name == "" || err != nil {
|
||||
return "游客" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/moreser"
|
||||
"videoweb/app/service/reportser"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/models/commentlikemod"
|
||||
"videoweb/pub/models/commentmod"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
commentListLimit int64 = 200
|
||||
subCommentListLimit int64 = 400
|
||||
commentTreeDefaultPageSize int64 = 10
|
||||
commentTreeMaxPageSize int64 = 100
|
||||
commentTreeMaxDepth int = 5
|
||||
)
|
||||
|
||||
type commentTreeNode struct {
|
||||
info proto.CommentInfo
|
||||
children []*commentTreeNode
|
||||
}
|
||||
|
||||
// ListCommentTree 分页查询指定对象评论列表,并按父子关系组装树形结构
|
||||
func ListCommentTree(ctx context.Context, userId int64, req *proto.CommentListReq) (*proto.CommentListResp, *srvdef.SrvErr) {
|
||||
if req.ObjectId <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "对象ID不能为空")
|
||||
}
|
||||
if !pubdef.ValidCommentObjectType(req.ObjectType) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "评论类型有误")
|
||||
}
|
||||
if req.SortOrder != 0 && req.SortOrder != 1 && req.SortOrder != 2 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "排序方式有误")
|
||||
}
|
||||
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, commentTreeDefaultPageSize, commentTreeMaxPageSize)
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.ObjectId: req.ObjectId,
|
||||
commentmod.FN.Comment.ObjectType: req.ObjectType,
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
commentmod.FN.Comment.Level: 1,
|
||||
}
|
||||
total, err := commentmod.CountComments(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("ListCommentTree.CountComments", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.CommentListResp{BasePageResp: response.BasePageResp{
|
||||
TotalCount: total,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]proto.CommentInfo, 0),
|
||||
}}
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
imgCdn := configser.ImageCdnFromCtx(ctx)
|
||||
topList, err := commentmod.FindManyComment(ctx, filter, options.Find().
|
||||
SetProjection(commentListProjection()).
|
||||
SetSort(commentListSort(req.SortOrder)).
|
||||
SetSkip((pageNum-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err != nil {
|
||||
log.Warn("ListCommentTree.FindManyComment", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
children, err := commentTreeChildren(ctx, req.ObjectId, req.ObjectType, collectCommentIds(topList))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := append(collectCommentIds(topList), collectCommentIds(children)...)
|
||||
likes, err := commentlikemod.FindByUserAndComments(ctx, userId, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
liked := make(map[int64]bool, len(likes))
|
||||
for _, like := range likes {
|
||||
liked[like.CommentId] = true
|
||||
}
|
||||
resp.List = buildCommentTree(topList, children, imgCdn, liked)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// commentListSort 热门按点赞数排序,默认按发布时间排序
|
||||
func commentListSort(sortOrder int64) bson.D {
|
||||
if sortOrder == 1 {
|
||||
return bson.D{{Key: commentmod.FN.Comment.CountLike, Value: -1}, {Key: commentmod.FN.Comment.Id, Value: -1}}
|
||||
}
|
||||
return bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}, {Key: commentmod.FN.Comment.Id, Value: -1}}
|
||||
}
|
||||
|
||||
// GetCommentList 查询指定对象的首屏评论列表,并缓存短时间内重复读取
|
||||
func GetCommentList(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
||||
cacheKey := redisconst.CommentListCacheKey(objectId, objectType)
|
||||
cacheData, cacheErr := redis.Get(cacheKey)
|
||||
if cacheErr != nil {
|
||||
log.Warn("获取缓存失败", log.Any("cacheKey", cacheKey), log.E(cacheErr))
|
||||
}
|
||||
if cacheData != nil {
|
||||
var commentListResp *proto.GetCommentListResp
|
||||
if err := json.Unmarshal([]byte(*cacheData), &commentListResp); err != nil {
|
||||
log.Warn("解析缓存失败", log.Any("cacheKey", cacheKey), log.E(err))
|
||||
} else if commentListResp != nil {
|
||||
configser.RewriteImageCdn(ctx, commentListResp)
|
||||
return commentListResp, nil
|
||||
}
|
||||
}
|
||||
data, err := commentList(ctx, objectId, objectType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, marshalErr := json.Marshal(data)
|
||||
if marshalErr != nil {
|
||||
log.Warn("解析缓存失败", log.Any("cacheKey", cacheKey), log.E(marshalErr))
|
||||
configser.RewriteImageCdn(ctx, data)
|
||||
return data, nil
|
||||
}
|
||||
if setErr := redis.Set(cacheKey, string(bytes), redisconst.CommentListKeyExpire); setErr != nil {
|
||||
log.Warn("设置缓存失败", log.Any("cacheKey", cacheKey), log.E(setErr))
|
||||
}
|
||||
configser.RewriteImageCdn(ctx, data)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// commentList 查询顶级评论,并批量补齐二级回复,避免逐条查询子评论
|
||||
func commentList(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.ObjectId: objectId,
|
||||
commentmod.FN.Comment.ObjectType: objectType,
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
commentmod.FN.Comment.Level: 1,
|
||||
}
|
||||
total, err := commentmod.CountComments(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("commentList.CountComments", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
resp := &proto.GetCommentListResp{
|
||||
ObjectId: objectId,
|
||||
Total: total,
|
||||
List: make([]proto.CommentInfo, 0),
|
||||
}
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
imgCdn := configser.GetConfigInfo().ImgCdn
|
||||
list, err := commentmod.FindManyComment(ctx, filter, options.Find().
|
||||
SetProjection(commentListProjection()).
|
||||
SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}).
|
||||
SetLimit(commentListLimit))
|
||||
if err != nil {
|
||||
log.Warn("commentList.FindManyComment", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
parentIds := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
parentIds = append(parentIds, item.Id)
|
||||
}
|
||||
subCommentMap, err := subCommentList(ctx, parentIds, imgCdn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
info := commentInfoData(item, imgCdn)
|
||||
info.SubComment = subCommentMap[item.Id]
|
||||
resp.List = append(resp.List, info)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// subCommentList 批量查询二级评论,并按父评论ID分组
|
||||
func subCommentList(ctx context.Context, parentIds []int64, imgCdn string) (map[int64][]proto.CommentInfo, *srvdef.SrvErr) {
|
||||
data := make(map[int64][]proto.CommentInfo, len(parentIds))
|
||||
if len(parentIds) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
list, err := commentmod.FindManyComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.ParentsId: bson.M{"$in": parentIds},
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
}, options.Find().
|
||||
SetProjection(commentListProjection()).
|
||||
SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}).
|
||||
SetLimit(subCommentListLimit))
|
||||
if err != nil {
|
||||
log.Warn("subCommentList.FindManyComment", log.Any("parentIds", parentIds), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
data[item.ParentsId] = append(data[item.ParentsId], commentInfoData(item, imgCdn))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// commentTreeChildren 按层批量查询当前页一级评论的子孙评论,避免 N+1 查询
|
||||
func commentTreeChildren(ctx context.Context, objectId int64, objectType pubdef.CommentObjectType, parentIds []int64) (commentmod.CommentModelList, *srvdef.SrvErr) {
|
||||
data := make(commentmod.CommentModelList, 0)
|
||||
currentParentIds := uniqueCommentIds(parentIds)
|
||||
if len(currentParentIds) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
seenIds := make(map[int64]struct{}, len(currentParentIds))
|
||||
for _, id := range currentParentIds {
|
||||
seenIds[id] = struct{}{}
|
||||
}
|
||||
for depth := 0; len(currentParentIds) > 0 && depth < commentTreeMaxDepth; depth++ {
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.ObjectId: objectId,
|
||||
commentmod.FN.Comment.ObjectType: objectType,
|
||||
commentmod.FN.Comment.ParentsId: bson.M{"$in": currentParentIds},
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
}
|
||||
list, err := commentmod.FindManyComment(ctx, filter, options.Find().
|
||||
SetProjection(commentListProjection()).
|
||||
SetSort(bson.D{{Key: commentmod.FN.Comment.CreatedAt, Value: -1}}))
|
||||
if err != nil {
|
||||
log.Warn("commentTreeChildren.FindManyComment", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
nextParentIds := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenIds[item.Id]; ok {
|
||||
continue
|
||||
}
|
||||
seenIds[item.Id] = struct{}{}
|
||||
data = append(data, item)
|
||||
nextParentIds = append(nextParentIds, item.Id)
|
||||
}
|
||||
currentParentIds = nextParentIds
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// buildCommentTree 将一级评论和子孙评论按 parentsId 组装为树形结构
|
||||
func buildCommentTree(topList commentmod.CommentModelList, children commentmod.CommentModelList, imgCdn string, liked map[int64]bool) []proto.CommentInfo {
|
||||
nodes := make(map[int64]*commentTreeNode, len(topList)+len(children))
|
||||
roots := make([]*commentTreeNode, 0, len(topList))
|
||||
for _, item := range topList {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
info := commentInfoData(item, imgCdn)
|
||||
info.IsLike = liked[item.Id]
|
||||
node := &commentTreeNode{info: info}
|
||||
nodes[item.Id] = node
|
||||
roots = append(roots, node)
|
||||
}
|
||||
for _, item := range children {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
info := commentInfoData(item, imgCdn)
|
||||
info.IsLike = liked[item.Id]
|
||||
nodes[item.Id] = &commentTreeNode{info: info}
|
||||
}
|
||||
for _, item := range children {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
parentNode, ok := nodes[item.ParentsId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if node, ok := nodes[item.Id]; ok {
|
||||
parentNode.children = append(parentNode.children, node)
|
||||
}
|
||||
}
|
||||
return commentTreeNodesToInfos(roots)
|
||||
}
|
||||
|
||||
// commentTreeNodesToInfos 递归输出评论树响应
|
||||
func commentTreeNodesToInfos(nodes []*commentTreeNode) []proto.CommentInfo {
|
||||
list := make([]proto.CommentInfo, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
info := node.info
|
||||
info.SubComment = commentTreeNodesToInfos(node.children)
|
||||
list = append(list, info)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// collectCommentIds 收集评论ID集合
|
||||
func collectCommentIds(list commentmod.CommentModelList) []int64 {
|
||||
ids := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.Id <= 0 {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, item.Id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// uniqueCommentIds 去重评论ID集合
|
||||
func uniqueCommentIds(ids []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
result := make([]int64, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// commentInfoData 将评论模型转换为 APP 评论响应结构
|
||||
func commentInfoData(item *commentmod.CommentModel, imgCdn string) proto.CommentInfo {
|
||||
return proto.CommentInfo{
|
||||
Id: item.Id,
|
||||
ObjectId: item.ObjectId,
|
||||
UserName: item.UserName,
|
||||
Avatar: netutil.MergeURL(imgCdn, item.Avatar),
|
||||
Level: item.Level,
|
||||
ParentsId: item.ParentsId,
|
||||
ReplyUserName: item.ReplyUserName,
|
||||
Text: item.Text,
|
||||
CommentCount: item.CommentCount,
|
||||
CountLike: item.CountLike,
|
||||
SubComment: make([]proto.CommentInfo, 0),
|
||||
CreatedAt: item.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// commentListProjection 限定评论列表字段,减少详情页评论查询的数据传输
|
||||
func commentListProjection() bson.M {
|
||||
return bson.M{
|
||||
commentmod.FN.Comment.Id: 1,
|
||||
commentmod.FN.Comment.ObjectId: 1,
|
||||
commentmod.FN.Comment.UserName: 1,
|
||||
commentmod.FN.Comment.Avatar: 1,
|
||||
commentmod.FN.Comment.Level: 1,
|
||||
commentmod.FN.Comment.ParentsId: 1,
|
||||
commentmod.FN.Comment.ReplyUserName: 1,
|
||||
commentmod.FN.Comment.Text: 1,
|
||||
commentmod.FN.Comment.CommentCount: 1,
|
||||
commentmod.FN.Comment.CountLike: 1,
|
||||
commentmod.FN.Comment.CreatedAt: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func SendComment(ctx context.Context, ip string, userId int64, req *proto.CommentSendReq, ua useragent.UA) *srvdef.SrvErr {
|
||||
if req == nil || req.ObjectId <= 0 || req.ParentsId < 0 || !pubdef.ValidCommentObjectType(req.ObjectType) {
|
||||
return srvdef.New(srvdef.ErrParamError, "评论参数有误")
|
||||
}
|
||||
req.Text = strings.TrimSpace(req.Text)
|
||||
if req.Text == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "评论内容不能为空")
|
||||
}
|
||||
if utf8.RuneCountInString(req.Text) > 200 {
|
||||
return srvdef.New(srvdef.ErrParamError, "评论内容不能超过200字")
|
||||
}
|
||||
curValue, err := redis.Incr(redisconst.CommentLimitKey(ip), redisconst.CommentLimitKeyExpire)
|
||||
if err != nil {
|
||||
log.Error("SendComment redis incr failed", log.E(err))
|
||||
}
|
||||
//15秒超3评论提示频繁操作
|
||||
if curValue > 1 {
|
||||
log.Error("SendComment user comment too frequently!",
|
||||
log.Any("ip", ip), log.Any("req", req), log.E(err))
|
||||
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
|
||||
}
|
||||
|
||||
//操作频繁度判断
|
||||
dayCurValue, err := redis.Incr(redisconst.CommentOneDayLimitKey(ip), redisconst.CommentDayLimitKeyExpire)
|
||||
if err != nil {
|
||||
log.Error("SendComment redis incr failed", log.E(err))
|
||||
}
|
||||
// 每天20条
|
||||
if dayCurValue > 50 {
|
||||
log.Error("SendComment user comment too frequently!",
|
||||
log.Any("ip", ip), log.Any("req", req), log.E(err))
|
||||
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
|
||||
}
|
||||
if !pubdef.ValidCommentObjectType(req.ObjectType) {
|
||||
return srvdef.New(srvdef.ErrParamError, "评论类型有误")
|
||||
}
|
||||
objectName := pubdef.CommentObjectTypeName(req.ObjectType)
|
||||
var videoInfo2 videoinformationmod.VideoInformation
|
||||
switch req.ObjectType {
|
||||
case pubdef.CommentObjectTypeVideo:
|
||||
videoInfo, err1 := videoinformationmod.FindOneVideoInformation(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: req.ObjectId,
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
})
|
||||
if err1 != nil {
|
||||
log.Warn("SendComment.FindOneVideoInformation error", log.Any("objectId", req.ObjectId), log.E(err1))
|
||||
return err1
|
||||
}
|
||||
videoInfo2 = *videoInfo
|
||||
case pubdef.CommentObjectTypePlaylet:
|
||||
_, err1 := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: req.ObjectId,
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
})
|
||||
if err1 != nil {
|
||||
log.Warn("SendComment.FindOnePlaylet error", log.Any("objectId", req.ObjectId), log.E(err1))
|
||||
return err1
|
||||
}
|
||||
case pubdef.CommentObjectTypePlayletChapter:
|
||||
chapter, err1 := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
||||
playletchaptermod.FN.PlayletChapter.Id: req.ObjectId,
|
||||
})
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
_, err1 = playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
})
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
}
|
||||
// 通过 token 中的 userId 查询用户信息,补充用户名和头像
|
||||
userInfo, err1 := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
if err1 != nil {
|
||||
log.Warn("SendComment.FindOneUser error", log.Any("userId", userId), log.E(err1))
|
||||
return err1
|
||||
}
|
||||
userName := userInfo.NickName
|
||||
if userName == "" {
|
||||
userName = userInfo.AccountNo
|
||||
}
|
||||
|
||||
level := int64(1)
|
||||
replyUserName := ""
|
||||
if req.ParentsId > 0 {
|
||||
// 回复评论
|
||||
parentsInfo, err1 := commentmod.FindOneComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: req.ParentsId,
|
||||
commentmod.FN.Comment.ObjectId: req.ObjectId,
|
||||
commentmod.FN.Comment.ObjectType: req.ObjectType,
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
})
|
||||
if err1 != nil {
|
||||
log.Warn("SendComment.FindOneComment error", log.Any("objectId", req.ObjectId), log.Any("parentsId", req.ParentsId), log.E(err1))
|
||||
return err1
|
||||
}
|
||||
level = parentsInfo.Level + 1
|
||||
if level > int64(commentTreeMaxDepth+1) {
|
||||
return srvdef.New(srvdef.ErrParamError, "回复层级过深")
|
||||
}
|
||||
replyUserName = parentsInfo.UserName
|
||||
}
|
||||
info := commentmod.CommentModel{
|
||||
Id: mongos.Lid.Comment.Get(),
|
||||
Ip: ip,
|
||||
ObjectId: req.ObjectId,
|
||||
ObjectName: objectName,
|
||||
ObjectType: req.ObjectType,
|
||||
UserId: userId,
|
||||
UserName: userName,
|
||||
Avatar: userInfo.Avatar,
|
||||
Level: level,
|
||||
ParentsId: req.ParentsId,
|
||||
ReplyUserName: replyUserName,
|
||||
Text: req.Text,
|
||||
CommentCount: 0,
|
||||
ReviewStatus: 0,
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err1 := commentmod.InsertOneComment(ctx, &info); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
var reportErr error
|
||||
switch req.ObjectType {
|
||||
case pubdef.CommentObjectTypeVideo:
|
||||
reportErr = reportser.ReportVideoComment(userId, ua, ip, &videoInfo2, req.Text)
|
||||
}
|
||||
if reportErr != nil {
|
||||
log.Warn("SendComment.ReportCommentEvent error", log.Any("objectType", req.ObjectType), log.Any("objectId", req.ObjectId), log.E(reportErr))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/commentlikemod"
|
||||
"videoweb/pub/models/commentmod"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// SetCommentLike 设置评论点赞状态
|
||||
func SetCommentLike(ctx context.Context, userId int64, req *proto.CommentLikeSetReq) (*proto.CommentLikeSetResp, *srvdef.SrvErr) {
|
||||
if userId <= 0 || req == nil || req.CommentId <= 0 || req.IsLike == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||||
}
|
||||
comment, err := commentmod.FindOneComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: req.CommentId,
|
||||
commentmod.FN.Comment.ReviewStatus: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var changed bool
|
||||
if *req.IsLike {
|
||||
changed, err = commentlikemod.Add(ctx, userId, req.CommentId)
|
||||
} else {
|
||||
changed, err = commentlikemod.Remove(ctx, userId, req.CommentId)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := comment.CountLike
|
||||
if changed {
|
||||
delta := int64(1)
|
||||
if !*req.IsLike {
|
||||
delta = -1
|
||||
}
|
||||
count, err = commentmod.ChangeLikeCount(ctx, req.CommentId, delta)
|
||||
if err != nil {
|
||||
if *req.IsLike {
|
||||
_, _ = commentlikemod.Remove(ctx, userId, req.CommentId)
|
||||
} else {
|
||||
_, _ = commentlikemod.Add(ctx, userId, req.CommentId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
current, findErr := commentmod.FindOneComment(ctx, bson.M{commentmod.FN.Comment.Id: req.CommentId})
|
||||
if findErr != nil {
|
||||
return nil, findErr
|
||||
}
|
||||
count = current.CountLike
|
||||
}
|
||||
if cacheErr := redis.Del(redisconst.CommentListCacheKey(comment.ObjectId, comment.ObjectType)); cacheErr != nil {
|
||||
log.Warn("SetCommentLike clear cache", log.E(cacheErr))
|
||||
}
|
||||
return &proto.CommentLikeSetResp{CommentId: req.CommentId, IsLike: *req.IsLike, CountLike: count}, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/adcenter"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func UpdateAnnouncementData(ctx context.Context) {
|
||||
config := GetConfigInfo()
|
||||
resp := proto.AnnouncementInfo{
|
||||
VideoInfo: make([]proto.VideoInfoData, 0),
|
||||
PdfAddr: config.PdfAddr,
|
||||
NewUrlAddr: config.DHNewUrlAddr1,
|
||||
NewUrlAddr1: config.DHNewUrlAddr2,
|
||||
NewUrlAddr2: config.DHNewUrlAddr3,
|
||||
ForeverUrlAddr: config.ForeverUrlAddr,
|
||||
AppAddr: config.AppAddr,
|
||||
TranslationAddr: config.TranslationAddr,
|
||||
}
|
||||
|
||||
videoFilter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
||||
videoinformationmod.FN.VideoInformation.IsHot: true,
|
||||
}
|
||||
videoOpt := &options.FindOptions{}
|
||||
videoOpt.SetSort(bson.M{videoinformationmod.FN.VideoInformation.Sort: -1})
|
||||
videoOpt.SetLimit(2)
|
||||
|
||||
videoList, serr := videoinformationmod.FindManyVideoInformations(ctx, videoFilter, videoOpt)
|
||||
if serr != nil {
|
||||
log.Error("VideoCategoryData", log.Any("videoFilter", videoFilter), log.E(serr))
|
||||
return
|
||||
}
|
||||
// 查询视频
|
||||
for _, videoInfo := range videoList {
|
||||
v := proto.VideoInfoToData(videoInfo, config.ImgCdn, GetCreateInfo(videoInfo.CreatorId, config.ImgCdn))
|
||||
for _, tagId := range videoInfo.Tags {
|
||||
tagInfo := GetVideoTag(tagId)
|
||||
if tagInfo != nil {
|
||||
v.Tags = append(v.Tags, proto.TagsInfoData{
|
||||
Id: tagInfo.Id,
|
||||
Name: tagInfo.Name,
|
||||
Slug: tagInfo.Slug,
|
||||
})
|
||||
}
|
||||
}
|
||||
resp.VideoInfo = append(resp.VideoInfo, v)
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Announcement = resp
|
||||
}
|
||||
|
||||
// UpdateAdvitiseDataFromJt 从广告中心拉取福利页广告
|
||||
func UpdateAdvitiseDataFromJt() {
|
||||
//config := GetConfigInfo()
|
||||
var resp proto.AdvitiseData
|
||||
jtAds, err := adcenter.JtAdvertiseThreeServer(appg.Conf.AdCenter.ApiDomain, appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode, appg.Conf.AdCenter.AesKey)
|
||||
if err != nil {
|
||||
log.Error("JtAdvertiseThreeServer error occur", log.E(err))
|
||||
return
|
||||
}
|
||||
for _, _ = range jtAds {
|
||||
//pos, err := strconv.Atoi(loc.AdvertiseLocationCode)
|
||||
//if err != nil {
|
||||
// log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode))
|
||||
// continue
|
||||
//}
|
||||
////if pos < 100001 {
|
||||
//// continue
|
||||
////}
|
||||
//ads := make([]proxy.AdvitiseInfo, 0)
|
||||
//for _, ad := range loc.AdDetailInfoList {
|
||||
// //extra := ad.GetExtraData()
|
||||
// ads = append(ads, proxy.AdvitiseInfo{
|
||||
// Id: ad.AdvertiseCode,
|
||||
// Location: int64(pos),
|
||||
// Title: ad.AdvertiseName,
|
||||
// Images: netutil.MergeURL(config.ImgCdn, ad.GetCoverLsj()),
|
||||
// Link: ad.GetRealLink(),
|
||||
// })
|
||||
//}
|
||||
//
|
||||
//switch pos {
|
||||
//case 1:
|
||||
// resp.HomeList = ads
|
||||
//case 2:
|
||||
// resp.DetailTopList = ads
|
||||
//case 3:
|
||||
// resp.NineGridList = ads
|
||||
//case 4:
|
||||
// resp.DetailbannerList = ads
|
||||
//case 5:
|
||||
// resp.LaunchScreenList = ads
|
||||
//case 6:
|
||||
// resp.MustAppList = ads
|
||||
//case 7:
|
||||
// resp.TitleAppList = ads
|
||||
//case 8:
|
||||
// resp.FriendList = ads
|
||||
//case 9:
|
||||
// resp.DialogList = ads
|
||||
//case 10:
|
||||
// resp.PostList = ads
|
||||
//case 11:
|
||||
// resp.FloatingList = ads
|
||||
//case 12:
|
||||
// resp.AList = ads
|
||||
//case 13:
|
||||
// resp.BList = ads
|
||||
//case 14:
|
||||
// resp.VideoList = ads
|
||||
//}
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Advitise = resp
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/models/cfg"
|
||||
"videoweb/pub/models/hotnavmod"
|
||||
"videoweb/pub/models/ipblackmod"
|
||||
"videoweb/pub/models/richtextcontentmod"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheMgr CacheInfo //
|
||||
skdCron *cron.Cron
|
||||
)
|
||||
|
||||
// richTextImgSrcRe 匹配富文本HTML里 img 标签的 src 图片地址,兼容单引号、双引号和JSON转义后的双引号
|
||||
var richTextImgSrcRe = regexp.MustCompile(`(?i)(<img\b[^>]*?\s*)src\s*=\s*(\\?"([^"\\]*)\\?"|'([^']*)')`)
|
||||
|
||||
type CacheInfo struct {
|
||||
Lock sync.RWMutex // 读写锁
|
||||
Config proto.ConfigInfo // 系统配置缓存
|
||||
Announcement proto.AnnouncementInfo
|
||||
Advitise proto.AdvitiseData
|
||||
IpBlackMaps map[string]struct{}
|
||||
CreateInfo map[int64]*videocreatermod.VideoCreaterInfo // 作者信息
|
||||
HotNavs []proto.HotNavItem // 热门导航
|
||||
AppRichTexts []proto.AppRichTextItemResp // APP富文本内容列表
|
||||
}
|
||||
|
||||
type appRichTextCacheItem struct {
|
||||
Id int64 `bson:"_id,omitempty"`
|
||||
Title string `bson:"title"`
|
||||
Content string `bson:"content"`
|
||||
Desc string `bson:"desc"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
skdCron = cron.New(cron.WithSeconds()) // 秒级定时任务
|
||||
CacheMgr.Config = proto.ConfigInfo{}
|
||||
CacheMgr.CreateInfo = make(map[int64]*videocreatermod.VideoCreaterInfo)
|
||||
CacheMgr.IpBlackMaps = make(map[string]struct{})
|
||||
}
|
||||
|
||||
func Start() {
|
||||
ctx := context.Background()
|
||||
|
||||
dataReport.StartSDKConfigCron()
|
||||
UpdateConfigInfo(ctx)
|
||||
//UpdateAdvitiseData(ctx)
|
||||
UpdateAdvitiseDataFromJt()
|
||||
UpdateAnnouncementData(ctx)
|
||||
updateIpBlack()
|
||||
updateHotNavCache(ctx)
|
||||
updateAppRichTexts()
|
||||
StartTopicCache()
|
||||
go UpdateVideoRankCache()
|
||||
go UpdatePlayletRankCache()
|
||||
go UpdateAllFavoriteDataCount()
|
||||
_, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
|
||||
UpdateConfigInfo(ctx)
|
||||
//UpdateAdvitiseData(ctx)
|
||||
UpdateAdvitiseDataFromJt()
|
||||
UpdateAnnouncementData(ctx)
|
||||
updateIpBlack()
|
||||
updateHotNavCache(ctx)
|
||||
updateAppRichTexts()
|
||||
})
|
||||
|
||||
_, _ = skdCron.AddFunc("* */14 * * * ?", func() {
|
||||
updateCreateCache(ctx)
|
||||
})
|
||||
// 每10分钟统计各标签关联数据量并刷新首页标签缓存
|
||||
_, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
|
||||
UpdateAllTagDataCount()
|
||||
})
|
||||
// 每10分钟统计各内容收藏量(整点 10 分)
|
||||
_, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
|
||||
UpdateAllFavoriteDataCount()
|
||||
})
|
||||
// 各内容榜单每 10 分钟刷新一次,错开整点避免任务扎堆
|
||||
_, _ = skdCron.AddFunc("0 1/10 * * * ?", func() {
|
||||
UpdateVideoRankCache()
|
||||
})
|
||||
_, _ = skdCron.AddFunc("0 5/10 * * * ?", func() {
|
||||
UpdatePlayletRankCache()
|
||||
})
|
||||
skdCron.Start()
|
||||
}
|
||||
|
||||
func UpdateConfigInfo(ctx context.Context) {
|
||||
config := proto.ConfigInfo{}
|
||||
|
||||
filter := bson.M{cfg.FN.SysCfg.Id: 1}
|
||||
sysCfg, _ := cfg.FindOneSysCfg(context.Background(), filter)
|
||||
if sysCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
config.NewUrlAddr = sysCfg.NewUrlAddr
|
||||
config.NewUrlAddr1 = sysCfg.NewUrlAddr1
|
||||
config.NewUrlAddr2 = sysCfg.NewUrlAddr2
|
||||
config.ForeverUrlAddr = sysCfg.ForeverUrlAddr
|
||||
config.GmailAddr = sysCfg.GmailAddr
|
||||
config.AppAddr = sysCfg.AppAddr
|
||||
config.WeiChart = sysCfg.WeiChart
|
||||
config.QQ = sysCfg.QQ
|
||||
config.TgGroup = sysCfg.TgGroup
|
||||
config.PotaoGroup = sysCfg.PotaoGroup
|
||||
config.GitAddr = sysCfg.GitAddr
|
||||
config.TwitterAddr = sysCfg.TwitterAddr
|
||||
config.Telegram = sysCfg.Telegram
|
||||
config.TelegramAdd = sysCfg.TelegramAdd
|
||||
config.TranslationAddr = sysCfg.TranslationAddr
|
||||
config.ImgCdn = sysCfg.ImgAppCdn
|
||||
config.TsCdn = sysCfg.MediaAppCdn
|
||||
config.ImgAppUsCdn = sysCfg.ImgAppUsCdn
|
||||
config.MediaAppUsCdn = sysCfg.MediaAppUsCdn
|
||||
config.PdfAddr = sysCfg.PdfAddr
|
||||
|
||||
config.WebTitle = sysCfg.WebTitle
|
||||
config.WebKeyworld = sysCfg.WebKeyworld
|
||||
config.WebDescription = sysCfg.WebDescription
|
||||
config.WebSiteName = sysCfg.WebSiteName
|
||||
config.WebAddr = sysCfg.WebAddr
|
||||
config.DHNewUrlAddr1 = sysCfg.DHNewUrlAddr1
|
||||
config.DHNewUrlAddr2 = sysCfg.DHNewUrlAddr2
|
||||
config.DHNewUrlAddr3 = sysCfg.DHNewUrlAddr3
|
||||
config.UseIngAddress = sysCfg.UseAddress
|
||||
config.FreeVPN = sysCfg.FreeVPN
|
||||
config.DHKeyworld = sysCfg.DHKeyworld
|
||||
config.DHTitle = sysCfg.DHTitle
|
||||
config.DHDescription = sysCfg.DHDescription
|
||||
config.DHSiteName = sysCfg.DHSiteName
|
||||
config.ReportSDKConfig = dataReport.GetSDKDomainConfig()
|
||||
config.ChannelCode = sysCfg.ChannelCode
|
||||
config.EnableH265 = sysCfg.EnableH265
|
||||
cfg.CacheAppKey(sysCfg.AppKey)
|
||||
config.M3U8DayRateLimitMax = sysCfg.M3U8DayRateLimitMax
|
||||
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Config = config
|
||||
|
||||
}
|
||||
|
||||
func GetConfigInfo() proto.ConfigInfo {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
|
||||
return CacheMgr.Config
|
||||
}
|
||||
|
||||
func GetAdvitise() proto.AdvitiseData {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
|
||||
return CacheMgr.Advitise
|
||||
}
|
||||
|
||||
func GetAnnouncement() proto.AnnouncementInfo {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
|
||||
return CacheMgr.Announcement
|
||||
}
|
||||
|
||||
func GetRandCDN() string {
|
||||
config := GetConfigInfo()
|
||||
return config.TsCdn
|
||||
}
|
||||
|
||||
func updateHotNavCache(ctx context.Context) {
|
||||
filter := bson.M{hotnavmod.FN.HotNav.Status: true}
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{
|
||||
{Key: hotnavmod.FN.HotNav.Sort, Value: -1},
|
||||
{Key: hotnavmod.FN.HotNav.CreatedAt, Value: -1},
|
||||
})
|
||||
opts.SetLimit(100)
|
||||
list, err := hotnavmod.FindManyHotNav(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("updateHotNavCache.FindManyHotNav", log.E(err))
|
||||
return
|
||||
}
|
||||
config := GetConfigInfo()
|
||||
tempList := make([]proto.HotNavItem, len(list))
|
||||
for i, item := range list {
|
||||
tempList[i] = proto.HotNavItem{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Intro: item.Intro,
|
||||
Url: item.Url,
|
||||
Icon: netutil.MergeURL(config.ImgCdn, item.Icon),
|
||||
}
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.HotNavs = tempList
|
||||
}
|
||||
|
||||
func GetHotNavs() []proto.HotNavItem {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
result := make([]proto.HotNavItem, len(CacheMgr.HotNavs))
|
||||
copy(result, CacheMgr.HotNavs)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetFooterInfo 查询APP底部信息
|
||||
func GetFooterInfo() proto.FooterInfo {
|
||||
config := GetConfigInfo()
|
||||
return proto.FooterInfo{
|
||||
Brand: buildFooterBrand(config),
|
||||
NavList: buildFooterNavList(),
|
||||
ContactList: buildFooterContactList(config),
|
||||
}
|
||||
}
|
||||
|
||||
// buildFooterBrand 构建APP底部品牌信息
|
||||
func buildFooterBrand(config proto.ConfigInfo) proto.FooterBrand {
|
||||
return proto.FooterBrand{
|
||||
Title: config.WebSiteName,
|
||||
Content: config.WebDescription,
|
||||
}
|
||||
}
|
||||
|
||||
// buildFooterNavList 构建APP底部导航列表
|
||||
func buildFooterNavList() []proto.TranslationCategory {
|
||||
bottom := translationser.GetTranslationBottomList()
|
||||
return append([]proto.TranslationCategory{}, bottom.CategoryList...)
|
||||
}
|
||||
|
||||
// buildFooterContactList 构建APP底部联系方式,固定返回原型要求的四项
|
||||
func buildFooterContactList(config proto.ConfigInfo) []proto.FooterContact {
|
||||
return []proto.FooterContact{
|
||||
{Name: "E-mail", Value: config.GmailAddr},
|
||||
{Name: "商务合作", Value: config.Telegram},
|
||||
{Name: "官方群", Value: firstNotEmpty(config.TgGroup, config.TelegramAdd)},
|
||||
{Name: "官方推特", Value: config.TwitterAddr},
|
||||
}
|
||||
}
|
||||
|
||||
// firstNotEmpty 返回第一个非空字符串
|
||||
func firstNotEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAppRichTextList 查询APP富文本内容列表
|
||||
func GetAppRichTextList(rawIds string, rawTitles string) proto.AppRichTextListResp {
|
||||
ids := parseInt64Set(rawIds)
|
||||
titles := parseStringSet(rawTitles)
|
||||
data := getCachedAppRichTexts()
|
||||
if len(data) == 0 {
|
||||
updateAppRichTexts()
|
||||
data = getCachedAppRichTexts()
|
||||
}
|
||||
return proto.AppRichTextListResp{List: filterAppRichTexts(data, ids, titles)}
|
||||
}
|
||||
|
||||
// getCachedAppRichTexts 读取APP富文本缓存副本
|
||||
func getCachedAppRichTexts() []proto.AppRichTextItemResp {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
return append([]proto.AppRichTextItemResp{}, CacheMgr.AppRichTexts...)
|
||||
}
|
||||
|
||||
// filterAppRichTexts 按富文本ID或标题过滤缓存数据
|
||||
func filterAppRichTexts(data []proto.AppRichTextItemResp, ids map[int64]struct{}, titles map[string]struct{}) []proto.AppRichTextItemResp {
|
||||
list := make([]proto.AppRichTextItemResp, 0, len(data))
|
||||
for _, item := range data {
|
||||
if len(ids) > 0 {
|
||||
if _, ok := ids[item.Id]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(titles) > 0 {
|
||||
if _, ok := titles[item.Title]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// parseInt64Set 解析英文逗号分隔的ID集合
|
||||
func parseInt64Set(raw string) map[int64]struct{} {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
resp := make(map[int64]struct{})
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
id, err := strconv.ParseInt(strings.TrimSpace(item), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
continue
|
||||
}
|
||||
resp[id] = struct{}{}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// parseStringSet 解析英文逗号分隔的字符串集合
|
||||
func parseStringSet(raw string) map[string]struct{} {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
resp := make(map[string]struct{})
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
resp[item] = struct{}{}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// updateAppRichTexts 刷新APP富文本内容缓存
|
||||
func updateAppRichTexts() {
|
||||
resp, err := queryAppRichTexts(context.Background())
|
||||
if err != nil {
|
||||
log.Warn("查询APP富文本内容失败", log.E(err))
|
||||
return
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.AppRichTexts = resp
|
||||
}
|
||||
|
||||
// queryAppRichTexts 查询启用状态的APP富文本内容,统一供定时缓存和接口兜底加载使用
|
||||
func queryAppRichTexts(ctx context.Context) ([]proto.AppRichTextItemResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
richtextcontentmod.FN.RichTextContentModel.Status: true,
|
||||
}
|
||||
opts := options.Find().
|
||||
SetProjection(bson.M{
|
||||
richtextcontentmod.FN.RichTextContentModel.Id: 1,
|
||||
richtextcontentmod.FN.RichTextContentModel.Title: 1,
|
||||
richtextcontentmod.FN.RichTextContentModel.Content: 1,
|
||||
richtextcontentmod.FN.RichTextContentModel.Desc: 1,
|
||||
}).
|
||||
SetSort(bson.D{
|
||||
{Key: richtextcontentmod.FN.RichTextContentModel.Sort, Value: -1},
|
||||
{Key: richtextcontentmod.FN.RichTextContentModel.UpdatedAt, Value: -1},
|
||||
}).
|
||||
SetLimit(100)
|
||||
list := make([]appRichTextCacheItem, 0)
|
||||
err := richtextcontentmod.FindManyRichTextContentModelModelWithBind(ctx, &list, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("查询APP富文本内容失败", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
imgCdn := GetConfigInfo().ImgCdn
|
||||
resp := make([]proto.AppRichTextItemResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp = append(resp, proto.AppRichTextItemResp{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Content: applyRichTextImgSrcCdn(imgCdn, item.Content),
|
||||
Desc: item.Desc,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// applyRichTextImgSrcCdn 给富文本 content 中 img 标签的 src 相对图片地址拼接图片CDN
|
||||
func applyRichTextImgSrcCdn(imgCdn string, content string) string {
|
||||
if imgCdn == "" || content == "" {
|
||||
return content
|
||||
}
|
||||
return richTextImgSrcRe.ReplaceAllStringFunc(content, func(match string) string {
|
||||
parts := richTextImgSrcRe.FindStringSubmatch(match)
|
||||
if len(parts) < 5 {
|
||||
return match
|
||||
}
|
||||
prefix := parts[1]
|
||||
quote := `"`
|
||||
src := parts[3]
|
||||
if src == "" {
|
||||
quote = `'`
|
||||
src = parts[4]
|
||||
} else if strings.HasPrefix(parts[2], `\"`) {
|
||||
quote = `\"`
|
||||
}
|
||||
src = strings.TrimSpace(src)
|
||||
if src == "" || isAbsoluteRichTextSrc(src) {
|
||||
return match
|
||||
}
|
||||
return prefix + "src=" + quote + netutil.MergeURL(imgCdn, src) + quote
|
||||
})
|
||||
}
|
||||
|
||||
// isAbsoluteRichTextSrc 判断图片地址是否已经是完整地址或特殊协议,避免重复拼接CDN
|
||||
func isAbsoluteRichTextSrc(src string) bool {
|
||||
lowerSrc := strings.ToLower(src)
|
||||
return strings.HasPrefix(lowerSrc, "http://") ||
|
||||
strings.HasPrefix(lowerSrc, "https://") ||
|
||||
strings.HasPrefix(lowerSrc, "data:") ||
|
||||
strings.HasPrefix(lowerSrc, "blob:") ||
|
||||
strings.HasPrefix(src, "//")
|
||||
}
|
||||
|
||||
func updateIpBlack() {
|
||||
ctx := context.Background()
|
||||
list, err := ipblackmod.FindManyIPBlackModel(ctx, bson.M{})
|
||||
if err != nil {
|
||||
log.Error("updateIpBlack FindManyIPBlackModel error", log.E(err))
|
||||
return
|
||||
}
|
||||
tempMaps := make(map[string]struct{})
|
||||
for _, v := range list {
|
||||
tempMaps[v.Ip] = struct{}{}
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.IpBlackMaps = tempMaps
|
||||
}
|
||||
|
||||
func CheckIpBlack(ip string) bool {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
if _, ok := CacheMgr.IpBlackMaps[ip]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
dataReport.StopSDKConfigCron()
|
||||
skdCron.Stop()
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ClientIPFromCtx 从 gin 请求上下文解析客户端 IP。
|
||||
func ClientIPFromCtx(ctx context.Context) string {
|
||||
c, ok := ctx.(*gin.Context)
|
||||
if !ok || c == nil {
|
||||
return ""
|
||||
}
|
||||
return ipaddr.ResolveClientIP(c)
|
||||
}
|
||||
|
||||
// IsOverseasIP 与 m3u8 TS 域名判断一致:按 v4/v6 库识别是否海外。
|
||||
func IsOverseasIP(ip string) bool {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip == "" {
|
||||
return false
|
||||
}
|
||||
detector := appg.DetectorV6
|
||||
if ipaddr.IsIPv4(ip) {
|
||||
detector = appg.DetectorV4
|
||||
}
|
||||
if detector == nil {
|
||||
return false
|
||||
}
|
||||
ok, _ := detector.IsOverseasIP(ip)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetAppImageCdn 海外 IP 且配置了 imgAppUsCdn 时用海外图床,否则用 imgAppCdn。
|
||||
func GetAppImageCdn(ip string) string {
|
||||
cfg := GetConfigInfo()
|
||||
if IsOverseasIP(ip) {
|
||||
if us := strings.TrimSpace(cfg.ImgAppUsCdn); us != "" {
|
||||
return us
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(cfg.ImgCdn)
|
||||
}
|
||||
|
||||
// ImageCdnFromCtx 按当前请求 IP 选择图片 CDN。
|
||||
func ImageCdnFromCtx(ctx context.Context) string {
|
||||
return GetAppImageCdn(ClientIPFromCtx(ctx))
|
||||
}
|
||||
|
||||
// GetRequestConfig 返回当前请求应使用的配置副本,海外 IP 且海外图床非空时覆盖 ImgCdn。
|
||||
func GetRequestConfig(ctx context.Context) proto.ConfigInfo {
|
||||
cfg := GetConfigInfo()
|
||||
if cdn := GetAppImageCdn(ClientIPFromCtx(ctx)); cdn != "" {
|
||||
cfg.ImgCdn = cdn
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// RewriteImageCdn 将已拼好的国内图床域名替换为当前请求应使用的图床,用于缓存数据。
|
||||
func RewriteImageCdn(ctx context.Context, data any) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
from := strings.TrimRight(strings.TrimSpace(GetConfigInfo().ImgCdn), "/")
|
||||
to := strings.TrimRight(GetAppImageCdn(ClientIPFromCtx(ctx)), "/")
|
||||
if from == "" || to == "" || from == to {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
replaced := bytes.ReplaceAll(raw, []byte(from), []byte(to))
|
||||
if bytes.Equal(raw, replaced) {
|
||||
return
|
||||
}
|
||||
_ = json.Unmarshal(replaced, data)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// AppContentTagList 查询APP端有绑定内容数量的启用统一标签列表
|
||||
func AppContentTagList(ctx context.Context, req *proto.AppContentTagListReq) (*proto.AppContentTagListResp, *srvdef.SrvErr) {
|
||||
if req == nil {
|
||||
req = &proto.AppContentTagListReq{}
|
||||
}
|
||||
filter := bson.M{
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
}
|
||||
bindCountExpr := appContentTagTotalCountExpr()
|
||||
if req.ContentTagType != nil {
|
||||
if !isValidAppContentTagType(*req.ContentTagType) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
countField := appContentTagCountField(*req.ContentTagType)
|
||||
filter[contenttagmod.FN.ContentTag.Types] = *req.ContentTagType
|
||||
filter[countField] = bson.M{"$gt": 0}
|
||||
bindCountExpr = bson.M{"$ifNull": bson.A{"$" + countField, 0}}
|
||||
} else {
|
||||
filter["$or"] = []bson.M{
|
||||
{contenttagmod.FN.ContentTag.VideoCount: bson.M{"$gt": 0}},
|
||||
{contenttagmod.FN.ContentTag.PlayletCount: bson.M{"$gt": 0}},
|
||||
}
|
||||
}
|
||||
list := make([]*proto.AppContentTagItem, 0)
|
||||
pipeline := []bson.M{
|
||||
{"$match": filter},
|
||||
{"$addFields": bson.M{"bindCount": bindCountExpr}},
|
||||
{"$sort": bson.D{
|
||||
{Key: "bindCount", Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Rank, Value: -1},
|
||||
{Key: contenttagmod.FN.ContentTag.Id, Value: -1},
|
||||
}},
|
||||
{"$project": bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: 1,
|
||||
contenttagmod.FN.ContentTag.Name: 1,
|
||||
contenttagmod.FN.ContentTag.Slug: 1,
|
||||
contenttagmod.FN.ContentTag.Types: 1,
|
||||
contenttagmod.FN.ContentTag.Rank: 1,
|
||||
contenttagmod.FN.ContentTag.VideoCount: 1,
|
||||
contenttagmod.FN.ContentTag.PlayletCount: 1,
|
||||
"seoInfo": 1,
|
||||
"bindCount": 1,
|
||||
}},
|
||||
}
|
||||
if err := contenttagmod.AggregateContentTag(ctx, &list, pipeline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil && item.SeoInfo.Keyworld == nil {
|
||||
item.SeoInfo.Keyworld = make([]string, 0)
|
||||
}
|
||||
}
|
||||
return &proto.AppContentTagListResp{List: list}, nil
|
||||
}
|
||||
|
||||
// HasEnabledContentTagBind 判断统一标签是否启用、支持当前内容类型且已有绑定内容
|
||||
func HasEnabledContentTagBind(ctx context.Context, tagId int64, tagType contenttagmod.ContentTagType) (bool, *srvdef.SrvErr) {
|
||||
if tagId <= 0 {
|
||||
return false, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "")
|
||||
}
|
||||
if !isValidAppContentTagType(tagType) {
|
||||
return false, srvdef.New(srvdef.ErrParamError, "标签类型无效", "")
|
||||
}
|
||||
countField := appContentTagCountField(tagType)
|
||||
_, err := contenttagmod.FindOneContentTag(ctx, bson.M{
|
||||
contenttagmod.FN.ContentTag.Id: tagId,
|
||||
contenttagmod.FN.ContentTag.Status: true,
|
||||
contenttagmod.FN.ContentTag.Types: tagType,
|
||||
countField: bson.M{"$gt": 0},
|
||||
}, options.FindOne().SetProjection(bson.M{contenttagmod.FN.ContentTag.Id: 1}))
|
||||
if err != nil {
|
||||
if err.Code == srvdef.ErrNoData {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// appContentTagTotalCountExpr 计算统一标签视频和短剧绑定数量合计,只用于查询排序不落库
|
||||
func appContentTagTotalCountExpr() bson.M {
|
||||
return bson.M{"$add": bson.A{
|
||||
bson.M{"$ifNull": bson.A{"$" + contenttagmod.FN.ContentTag.VideoCount, 0}},
|
||||
bson.M{"$ifNull": bson.A{"$" + contenttagmod.FN.ContentTag.PlayletCount, 0}},
|
||||
}}
|
||||
}
|
||||
|
||||
// isValidAppContentTagType 判断APP端统一标签类型是否有效
|
||||
func isValidAppContentTagType(tagType contenttagmod.ContentTagType) bool {
|
||||
return tagType == contenttagmod.ContentTagTypeVideo ||
|
||||
tagType == contenttagmod.ContentTagTypePlaylet
|
||||
}
|
||||
|
||||
// appContentTagCountField 返回当前内容类型对应的绑定数量字段
|
||||
func appContentTagCountField(tagType contenttagmod.ContentTagType) string {
|
||||
switch tagType {
|
||||
case contenttagmod.ContentTagTypeVideo:
|
||||
return contenttagmod.FN.ContentTag.VideoCount
|
||||
case contenttagmod.ContentTagTypePlaylet:
|
||||
return contenttagmod.FN.ContentTag.PlayletCount
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/models/videocreatermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func updateCreateCache(ctx context.Context) {
|
||||
// 更新作者相关缓存
|
||||
createList, err := videocreatermod.FindManyVideoCreaterInfos(ctx, bson.M{
|
||||
videocreatermod.FN.VideoCreaterInfo.Status: true,
|
||||
}, options.Find().SetLimit(200).SetSort(bson.M{videocreatermod.FN.VideoCreaterInfo.Sort: -1}))
|
||||
if err != nil {
|
||||
log.Warn("updateCreateCache FindManyVideoCreaterIinfos", log.E(err))
|
||||
return
|
||||
}
|
||||
tempCreateMaps := make(map[int64]*videocreatermod.VideoCreaterInfo)
|
||||
for _, createInfo := range createList {
|
||||
tempCreateMaps[createInfo.Id] = createInfo
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
CacheMgr.CreateInfo = tempCreateMaps
|
||||
CacheMgr.Lock.Unlock()
|
||||
}
|
||||
|
||||
func GetCreateInfo(createId int64, imageCdn string) proto.PostCreateInfo {
|
||||
if createId <= 0 {
|
||||
return proto.PostCreateInfo{}
|
||||
}
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
v, ok := CacheMgr.CreateInfo[createId]
|
||||
if !ok {
|
||||
return proto.PostCreateInfo{}
|
||||
}
|
||||
return proto.PostCreateInfo{
|
||||
Id: v.Id,
|
||||
Name: v.Name,
|
||||
Slug: v.Slug,
|
||||
Image: netutil.MergeURL(imageCdn, v.Image),
|
||||
Works: v.Works,
|
||||
CreateType: v.CreateType,
|
||||
Desc: v.Desc,
|
||||
Background: v.Background,
|
||||
CreatedAt: v.CreatedAt,
|
||||
SeoInfo: v.SeoInfo,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
)
|
||||
|
||||
// ApplyVideoH265Policy 根据 sys_cfg.enableH265 决定是否向客户端暴露 H265 地址。
|
||||
func ApplyVideoH265Policy(data *proto.VideoInfoData) {
|
||||
if data == nil || GetConfigInfo().EnableH265 {
|
||||
return
|
||||
}
|
||||
proto.StripH265Url(data)
|
||||
}
|
||||
|
||||
// ApplyVideoH265PolicyList 批量处理视频列表。
|
||||
func ApplyVideoH265PolicyList(list []proto.VideoInfoData) []proto.VideoInfoData {
|
||||
if GetConfigInfo().EnableH265 {
|
||||
return list
|
||||
}
|
||||
for i := range list {
|
||||
proto.StripH265Url(&list[i])
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// GetM3U8DayRateLimitMax 返回每 IP 每天最多可播放的不同正片数。
|
||||
// sys_cfg.m3u8DayRateLimitMax <= 0 时使用默认 80。
|
||||
func GetM3U8DayRateLimitMax() int64 {
|
||||
max := GetConfigInfo().M3U8DayRateLimitMax
|
||||
if max <= 0 {
|
||||
return redisconst.M3U8DayRateLimitMax
|
||||
}
|
||||
return max
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/playletcache"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/contenttagmod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/playletthememod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
PlayletRankMgr periodRankCache[proto.PlayletRankData]
|
||||
playletRankRefreshLock sync.Mutex
|
||||
playletRankCacheVersion atomic.Int64
|
||||
)
|
||||
|
||||
func UpdatePlayletRankCache() {
|
||||
version := playletcache.RankCacheVersion()
|
||||
ctx := context.Background()
|
||||
imgCdn := GetConfigInfo().ImgCdn
|
||||
PlayletRankMgr.replace(
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodTotal),
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodWeek),
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodMonth),
|
||||
buildPlayletRankList(ctx, imgCdn, RankPeriodYear),
|
||||
)
|
||||
playletRankCacheVersion.Store(version)
|
||||
}
|
||||
|
||||
func refreshPlayletRankCacheIfEmpty() {
|
||||
if PlayletRankMgr.hasAny() {
|
||||
return
|
||||
}
|
||||
playletRankRefreshLock.Lock()
|
||||
defer playletRankRefreshLock.Unlock()
|
||||
if PlayletRankMgr.hasAny() {
|
||||
return
|
||||
}
|
||||
UpdatePlayletRankCache()
|
||||
}
|
||||
|
||||
func GetPlayletRankList(p RankPeriod, limit int) []proto.PlayletRankData {
|
||||
version := playletcache.RankCacheVersion()
|
||||
if version != playletRankCacheVersion.Load() {
|
||||
playletRankRefreshLock.Lock()
|
||||
if version != playletRankCacheVersion.Load() {
|
||||
UpdatePlayletRankCache()
|
||||
}
|
||||
playletRankRefreshLock.Unlock()
|
||||
}
|
||||
return PlayletRankMgr.get(p, limit, refreshPlayletRankCacheIfEmpty)
|
||||
}
|
||||
|
||||
func playletPeriodWatchField(p RankPeriod) string {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return playletmod.FN.Playlet.WeekWatchs
|
||||
case RankPeriodMonth:
|
||||
return playletmod.FN.Playlet.MonthWatchs
|
||||
case RankPeriodYear:
|
||||
return playletmod.FN.Playlet.YearWatchs
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func buildPlayletRankList(ctx context.Context, imgCdn string, p RankPeriod) []proto.PlayletRankData {
|
||||
var ranked playletmod.PlayletList
|
||||
if p == RankPeriodTotal {
|
||||
if err := playletmod.AggregatePlaylet(ctx, &ranked, contentLikeRankPipeline(
|
||||
playletmod.FN.Playlet.Status, playletmod.StatusUp,
|
||||
playletmod.FN.Playlet.CountLike, playletmod.FN.Playlet.VirtualCountLike,
|
||||
playletmod.FN.Playlet.UpdatedAt, playletmod.FN.Playlet.Id,
|
||||
rankLimit,
|
||||
)); err != nil {
|
||||
log.Warn("buildPlayletRankList like rank query failed", log.E(err))
|
||||
ranked = playletmod.PlayletList{}
|
||||
}
|
||||
} else {
|
||||
watchField := playletPeriodWatchField(p)
|
||||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
watchField: bson.M{"$gt": 0},
|
||||
}, contentWatchRankOpts(watchField, playletmod.FN.Playlet.UpdatedAt, playletmod.FN.Playlet.Id, rankLimit))
|
||||
if err != nil {
|
||||
log.Warn("buildPlayletRankList watch rank query failed", log.Any("period", p), log.E(err))
|
||||
ranked = playletmod.PlayletList{}
|
||||
} else {
|
||||
ranked = list
|
||||
}
|
||||
}
|
||||
if int64(len(ranked)) < rankLimit {
|
||||
needFill := rankLimit - int64(len(ranked))
|
||||
excludeIds := make([]int64, 0, len(ranked))
|
||||
for _, v := range ranked {
|
||||
if v != nil {
|
||||
excludeIds = append(excludeIds, v.Id)
|
||||
}
|
||||
}
|
||||
var filled playletmod.PlayletList
|
||||
if err := playletmod.AggregatePlaylet(ctx, &filled, contentRankFillPipeline(
|
||||
playletmod.FN.Playlet.Id, playletmod.FN.Playlet.Status,
|
||||
playletmod.StatusUp, excludeIds, needFill,
|
||||
)); err != nil {
|
||||
log.Warn("buildPlayletRankList fill sample failed", log.Any("period", p), log.E(err))
|
||||
} else {
|
||||
ranked = append(ranked, filled...)
|
||||
}
|
||||
}
|
||||
tagNameMap, themeNameMap := loadPlayletRankNameMaps(ctx, ranked)
|
||||
result := make([]proto.PlayletRankData, 0, len(ranked))
|
||||
for _, item := range ranked {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, playletToRankData(item, imgCdn, tagNameMap, themeNameMap, p))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func playletToRankData(item *playletmod.Playlet, imgCdn string, tagNameMap contenttagmod.ContentTagMap, themeNameMap map[int64]string, p RankPeriod) proto.PlayletRankData {
|
||||
return proto.PlayletRankData{
|
||||
PlayletData: proto.PlayletData{
|
||||
Id: item.Id,
|
||||
Status: item.Status,
|
||||
IsRecommend: item.IsRecommend,
|
||||
IsNew: !item.OnlineAt.IsZero() && item.OnlineAt.After(utils.BeginOfWeekByTime(utils.Now())),
|
||||
IsFeatured: item.IsFeatured,
|
||||
IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
|
||||
ShowOriginal: item.ShowOriginal,
|
||||
OnlineAt: item.OnlineAt,
|
||||
MediaCreatedAt: item.MediaCreatedAt,
|
||||
MediaUpdatedAt: item.MediaUpdatedAt,
|
||||
Title: item.Title,
|
||||
Slug: item.Slug,
|
||||
Desc: item.Desc,
|
||||
Anchor: item.Anchor,
|
||||
Cover: netutil.MergeURL(imgCdn, item.DisplayCover()),
|
||||
Preview: netutil.MergeURL(imgCdn, strings.TrimSpace(item.Preview)),
|
||||
TagIds: item.TagIds,
|
||||
TagNames: playletRankTagNamesByIDs(item.TagIds, tagNameMap),
|
||||
TagSlugs: playletRankTagSlugsByIDs(item.TagIds, tagNameMap),
|
||||
ThemeIds: item.ThemeIds,
|
||||
ThemeNames: playletNamesByIDs(item.ThemeIds, themeNameMap),
|
||||
ContentType: item.ContentType,
|
||||
PayType: item.PayType,
|
||||
SingleCoins: item.SingleCoins,
|
||||
TotalEpisode: item.TotalEpisode,
|
||||
UpdateStatus: item.UpdateStatus,
|
||||
CountBrowse: item.DisplayCountBrowse(),
|
||||
HeatValue: item.DisplayHeatValue(),
|
||||
CountCollect: item.CountCollect,
|
||||
CountLike: item.CountLike,
|
||||
VirtualCountLike: item.VirtualCountLike,
|
||||
DisplayCountLike: item.CountLike + item.VirtualCountLike,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
SeoInfo: playletRankSeo(item.SeoInfo),
|
||||
},
|
||||
RankScore: periodWatchNum(p, item.WeekWatchs, item.MonthWatchs, item.YearWatchs, item.CountLike+item.VirtualCountLike),
|
||||
}
|
||||
}
|
||||
|
||||
func playletRankSeo(seo pubdef.SeoValue) pubdef.SeoValue {
|
||||
seo.Title = strings.TrimSpace(seo.Title)
|
||||
seo.Description = strings.TrimSpace(seo.Description)
|
||||
if seo.Keyworld == nil {
|
||||
seo.Keyworld = make([]string, 0)
|
||||
return seo
|
||||
}
|
||||
words := make([]string, 0, len(seo.Keyworld))
|
||||
for _, word := range seo.Keyworld {
|
||||
word = strings.TrimSpace(word)
|
||||
if word != "" {
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
seo.Keyworld = words
|
||||
return seo
|
||||
}
|
||||
|
||||
func playletNamesByIDs(ids []int64, nameMap map[int64]string) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if name := nameMap[id]; name != "" {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadPlayletRankNameMaps(ctx context.Context, list playletmod.PlayletList) (contenttagmod.ContentTagMap, map[int64]string) {
|
||||
tagIDs := make([]int64, 0)
|
||||
themeIDs := make([]int64, 0)
|
||||
seenTag := make(map[int64]struct{})
|
||||
seenTheme := make(map[int64]struct{})
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, id := range item.TagIds {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenTag[id]; ok {
|
||||
continue
|
||||
}
|
||||
seenTag[id] = struct{}{}
|
||||
tagIDs = append(tagIDs, id)
|
||||
}
|
||||
for _, id := range item.ThemeIds {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenTheme[id]; ok {
|
||||
continue
|
||||
}
|
||||
seenTheme[id] = struct{}{}
|
||||
themeIDs = append(themeIDs, id)
|
||||
}
|
||||
}
|
||||
tagNameMap := make(contenttagmod.ContentTagMap, len(tagIDs))
|
||||
if len(tagIDs) > 0 {
|
||||
if tags, err := contenttagmod.GetMapTags(ctx, tagIDs); err != nil {
|
||||
log.Warn("loadPlayletRankNameMaps tags failed", log.E(err))
|
||||
} else {
|
||||
tagNameMap = tags
|
||||
}
|
||||
}
|
||||
themeNameMap := make(map[int64]string, len(themeIDs))
|
||||
if len(themeIDs) > 0 {
|
||||
themes, err := playletthememod.FindManyPlayletTheme(ctx, bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: bson.M{"$in": themeIDs},
|
||||
}, options.Find().SetProjection(bson.M{
|
||||
playletthememod.FN.PlayletTheme.Id: 1,
|
||||
playletthememod.FN.PlayletTheme.Name: 1,
|
||||
}))
|
||||
if err != nil {
|
||||
log.Warn("loadPlayletRankNameMaps themes failed", log.E(err))
|
||||
} else {
|
||||
for _, theme := range themes {
|
||||
if theme != nil {
|
||||
themeNameMap[theme.Id] = theme.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tagNameMap, themeNameMap
|
||||
}
|
||||
|
||||
func playletRankTagNamesByIDs(ids []int64, tags contenttagmod.ContentTagMap) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if tag := tags[id]; tag != nil && tag.Name != "" {
|
||||
result = append(result, tag.Name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func playletRankTagSlugsByIDs(ids []int64, tags contenttagmod.ContentTagMap) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if tag := tags[id]; tag != nil && tag.Name != "" {
|
||||
result = append(result, tag.Slug)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const rankLimit = 48
|
||||
|
||||
// NormalizeRankPage 榜单分页:页码从 1 起,pageSize 不是 rankLimit 时强制改为 rankLimit。
|
||||
func NormalizeRankPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize != int64(rankLimit) {
|
||||
pageSize = int64(rankLimit)
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
type periodRankCache[T any] struct {
|
||||
Lock sync.RWMutex
|
||||
Total []T
|
||||
Week []T
|
||||
Month []T
|
||||
Year []T
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) hasAny() bool {
|
||||
c.Lock.RLock()
|
||||
defer c.Lock.RUnlock()
|
||||
return len(c.Total) > 0 || len(c.Week) > 0 || len(c.Month) > 0 || len(c.Year) > 0
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) pick(p RankPeriod) []T {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return c.Week
|
||||
case RankPeriodMonth:
|
||||
return c.Month
|
||||
case RankPeriodYear:
|
||||
return c.Year
|
||||
default:
|
||||
return c.Total
|
||||
}
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) replace(total, week, month, year []T) {
|
||||
c.Lock.Lock()
|
||||
c.Total, c.Week, c.Month, c.Year = total, week, month, year
|
||||
c.Lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *periodRankCache[T]) get(p RankPeriod, limit int, refreshIfEmpty func()) []T {
|
||||
c.Lock.RLock()
|
||||
list := c.pick(p)
|
||||
empty := len(list) == 0
|
||||
c.Lock.RUnlock()
|
||||
if empty {
|
||||
refreshIfEmpty()
|
||||
c.Lock.RLock()
|
||||
list = c.pick(p)
|
||||
c.Lock.RUnlock()
|
||||
}
|
||||
return sliceLimit(list, limit)
|
||||
}
|
||||
|
||||
func sliceLimit[T any](s []T, limit int) []T {
|
||||
if limit <= 0 || limit >= len(s) {
|
||||
return s
|
||||
}
|
||||
return s[:limit]
|
||||
}
|
||||
|
||||
// SliceRankPage 对榜单缓存做分页切片
|
||||
func SliceRankPage[T any](all []T, pageNum, pageSize int64) []T {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
return []T{}
|
||||
}
|
||||
start := (pageNum - 1) * pageSize
|
||||
if start >= int64(len(all)) {
|
||||
return []T{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > int64(len(all)) {
|
||||
end = int64(len(all))
|
||||
}
|
||||
return append([]T{}, all[start:end]...)
|
||||
}
|
||||
|
||||
func contentLikeRankPipeline(statusField string, statusVal interface{}, realLikes, fakeLikes, updatedAt, idField string, limit int64) []bson.M {
|
||||
return []bson.M{
|
||||
{"$match": bson.M{statusField: statusVal}},
|
||||
{"$addFields": bson.M{"rankScore": bson.M{"$add": []interface{}{
|
||||
bson.M{"$ifNull": []interface{}{"$" + realLikes, 0}},
|
||||
bson.M{"$ifNull": []interface{}{"$" + fakeLikes, 0}},
|
||||
}}}},
|
||||
{"$match": bson.M{"rankScore": bson.M{"$gt": 0}}},
|
||||
{"$sort": bson.D{
|
||||
{Key: "rankScore", Value: -1},
|
||||
{Key: updatedAt, Value: -1},
|
||||
{Key: idField, Value: -1},
|
||||
}},
|
||||
{"$limit": limit},
|
||||
}
|
||||
}
|
||||
|
||||
func contentWatchRankOpts(watchField, updatedAt, idField string, limit int64) *options.FindOptions {
|
||||
return options.Find().
|
||||
SetSort(bson.D{
|
||||
{Key: watchField, Value: -1},
|
||||
{Key: updatedAt, Value: -1},
|
||||
{Key: idField, Value: -1},
|
||||
}).
|
||||
SetLimit(limit)
|
||||
}
|
||||
|
||||
func contentRankFillPipeline(idField, statusField string, statusVal interface{}, excludeIds []int64, needFill int64) []bson.M {
|
||||
return []bson.M{
|
||||
{"$match": bson.M{
|
||||
idField: bson.M{"$nin": excludeIds},
|
||||
statusField: statusVal,
|
||||
}},
|
||||
{"$sample": bson.M{"size": needFill}},
|
||||
}
|
||||
}
|
||||
|
||||
func periodWatchNum(p RankPeriod, week, month, year, total int64) int64 {
|
||||
switch p {
|
||||
case RankPeriodWeek:
|
||||
return week
|
||||
case RankPeriodMonth:
|
||||
return month
|
||||
case RankPeriodYear:
|
||||
return year
|
||||
default:
|
||||
return total
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/models/videofavoritemod"
|
||||
"videoweb/pub/models/videoinformationmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// favoriteCountResult 收藏聚合结果
|
||||
type favoriteCountResult struct {
|
||||
Id int64 `bson:"_id"`
|
||||
Count int64 `bson:"count"`
|
||||
}
|
||||
|
||||
// UpdateAllFavoriteDataCount 统计视频收藏量并写回DB
|
||||
func UpdateAllFavoriteDataCount() {
|
||||
updateVideoFavoriteDataCount(context.Background())
|
||||
}
|
||||
|
||||
// buildFavoritePipeline 构建按时间窗口统计收藏量的聚合 pipeline
|
||||
// contentIdField: favorite表中内容ID字段名(如 videoId)
|
||||
// since: 统计起始时间,零值表示统计全量
|
||||
func buildFavoritePipeline(contentIdField string, since time.Time) []bson.M {
|
||||
matchStage := bson.M{}
|
||||
if !since.IsZero() {
|
||||
matchStage["createdAt"] = bson.M{"$gte": since}
|
||||
}
|
||||
pipeline := []bson.M{}
|
||||
if len(matchStage) > 0 {
|
||||
pipeline = append(pipeline, bson.M{"$match": matchStage})
|
||||
}
|
||||
pipeline = append(pipeline,
|
||||
bson.M{"$group": bson.M{
|
||||
"_id": "$" + contentIdField,
|
||||
"count": bson.M{"$sum": 1},
|
||||
}},
|
||||
)
|
||||
return pipeline
|
||||
}
|
||||
|
||||
// updateVideoFavoriteDataCount 统计视频收藏量,更新 Likes/WeekLikes/MonthLikes/YearLikes
|
||||
func updateVideoFavoriteDataCount(ctx context.Context) {
|
||||
now := time.Now()
|
||||
type period struct {
|
||||
field string
|
||||
since time.Time
|
||||
}
|
||||
periods := []period{
|
||||
{videoinformationmod.FN.VideoInformation.Real.Likes, time.Time{}},
|
||||
{videoinformationmod.FN.VideoInformation.Real.WeekLikes, now.AddDate(0, 0, -7)},
|
||||
{videoinformationmod.FN.VideoInformation.Real.MonthLikes, now.AddDate(0, 0, -30)},
|
||||
{videoinformationmod.FN.VideoInformation.Real.YearLikes, now.AddDate(0, 0, -365)},
|
||||
}
|
||||
idField := videofavoritemod.FN.VideoFavorite.VideoID
|
||||
for _, p := range periods {
|
||||
var results []favoriteCountResult
|
||||
pipeline := buildFavoritePipeline(idField, p.since)
|
||||
if serr := videofavoritemod.AggregateVideoFavorite(ctx, &results, pipeline); serr != nil {
|
||||
log.Warn("updateVideoFavoriteDataCount aggregate failed", log.Any("field", p.field))
|
||||
continue
|
||||
}
|
||||
if len(results) == 0 {
|
||||
continue
|
||||
}
|
||||
writes := make([]mongo.WriteModel, 0, len(results))
|
||||
for _, r := range results {
|
||||
writes = append(writes, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{videoinformationmod.FN.VideoInformation.Id: r.Id}).
|
||||
SetUpdate(bson.M{"$set": bson.M{p.field: r.Count}}),
|
||||
)
|
||||
}
|
||||
if _, serr := videoinformationmod.BulkWriteVideoInformation(ctx, writes); serr != nil {
|
||||
log.Warn("updateVideoFavoriteDataCount bulk update failed", log.Any("field", p.field))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RankPeriod 排行榜时间维度
|
||||
type RankPeriod int
|
||||
|
||||
const (
|
||||
RankPeriodTotal RankPeriod = 0 // 总榜
|
||||
RankPeriodWeek RankPeriod = 1 // 周榜
|
||||
RankPeriodMonth RankPeriod = 2 // 月榜
|
||||
RankPeriodYear RankPeriod = 3 // 年榜
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/cfg"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// GetSystemConfig 读取系统配置(MongoDB id=1)。
|
||||
func GetSystemConfig(ctx context.Context) (*cfg.SysCfg, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
cfg.FN.SysCfg.Id: 1,
|
||||
}
|
||||
return cfg.FindOneSysCfg(ctx, filter)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user