init: web_hgcg_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_91dj_app
|
||||
env GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_91dj_app -v
|
||||
tar czf web_91dj_app.tar.gz web_91dj_app
|
||||
rm -f web_91dj_app
|
||||
|
||||
test:
|
||||
@echo "test, GOPATH=$(GOPATH)"
|
||||
GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_91dj_app
|
||||
tar czf web_91dj_app.tar.gz web_91dj_app docs/
|
||||
rm -f web_91dj_app
|
||||
localtest:
|
||||
@echo "test, GOPATH=$(GOPATH)"
|
||||
go build $(APP_BUILD_FLAG) -o app
|
||||
@@ -0,0 +1,48 @@
|
||||
package appinfoapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/appser"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// QueryAppList 查询应用列表
|
||||
// @Tags 应用列表
|
||||
// @Summary 查询应用列表
|
||||
// @Description 返回应用分类列表,每个分类下含应用信息列表;categoryId=0 返回第一个分类
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param categoryId query int false "当前分类ID,0表示默认第一个分类"
|
||||
// @Success 200 {object} proto.AppListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/applist/list [get]
|
||||
func QueryAppList(c *gin.Context) {
|
||||
var req struct {
|
||||
CategoryId int64 `form:"categoryId"`
|
||||
}
|
||||
if err := c.ShouldBindQuery(&req); err != nil || req.CategoryId < 0 {
|
||||
req.CategoryId = 0
|
||||
}
|
||||
|
||||
categoryList, category, serr := appser.AppCategoryData(c, req.CategoryId)
|
||||
if serr != nil {
|
||||
common.ServerJSON(c, serr, "")
|
||||
return
|
||||
}
|
||||
|
||||
if categoryList == nil {
|
||||
categoryList = make([]proto.AppCategoryInfo, 0)
|
||||
}
|
||||
|
||||
common.ServerJSON(c, srvdef.Success, proto.AppListResp{
|
||||
Path: c.Request.URL.Path,
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
CategoryList: categoryList,
|
||||
Category: category,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package authapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/app/service/authser"
|
||||
"videoweb/app/service/wordsfilterser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func requestUA(ctx *gin.Context) useragent.UA {
|
||||
ua, _ := common.GetUA(ctx)
|
||||
return ua
|
||||
}
|
||||
|
||||
func applyRequestDevID(ua useragent.UA, devID string) useragent.UA {
|
||||
if v := strings.TrimSpace(devID); v != "" {
|
||||
ua.DevID = v
|
||||
}
|
||||
return ua
|
||||
}
|
||||
|
||||
type registerReq struct {
|
||||
AccountNo string `json:"accountNo" binding:"required"` // 账号
|
||||
Email string `json:"email" binding:"required"` // 邮箱
|
||||
Password string `json:"password" binding:"required"` // 明文密码
|
||||
AgentInviteCode string `json:"agentInviteCode"` // 邀请人的邀请码(可选)
|
||||
DevID string `json:"devID"` // 设备唯一标示(可选)
|
||||
Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入)
|
||||
}
|
||||
|
||||
type loginReq struct {
|
||||
Account string `json:"account" binding:"required"` // 账号或邮箱
|
||||
Password string `json:"password" binding:"required"` // 明文密码
|
||||
DevID string `json:"devID"` // 设备唯一标示(可选)
|
||||
Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入)
|
||||
}
|
||||
|
||||
type resetPasswordReq struct {
|
||||
AccountNo string `json:"accountNo" binding:"required"` // 账号
|
||||
Email string `json:"email" binding:"required"` // 邮箱
|
||||
NewPassword string `json:"newPassword" binding:"required"` // 新密码
|
||||
}
|
||||
|
||||
type updateProfileReq struct {
|
||||
NickName *string `json:"nickName"` // 新昵称(可选)
|
||||
Avatar *string `json:"avatar"` // 新头像 URL(可选)
|
||||
Introduction *string `json:"introduction"` // 简介(可选)
|
||||
PrivacyMode *bool `json:"privacyMode"` // 隐私模式:true-开启,false-关闭(可选)
|
||||
OldPassword *string `json:"oldPassword"` // 旧密码明文(已设置密码时修改必填)
|
||||
NewPassword *string `json:"newPassword"` // 新密码明文(设置或修改密码时必填)
|
||||
}
|
||||
|
||||
// GuestLogin 游客登录
|
||||
// @Tags 用户认证
|
||||
// @Summary 游客登录
|
||||
// @Description 按设备ID创建或找回游客账号;请求头可带 Authorization 复用已有游客会话。需传 X-User-Agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body authser.GuestLoginReq true "游客登录参数"
|
||||
// @Success 200 {object} authser.LoginResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/guest [POST]
|
||||
func GuestLogin(ctx *gin.Context) {
|
||||
var req authser.GuestLoginReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
ua, err := common.GetUA(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, srvdef.NoUA, nil)
|
||||
return
|
||||
}
|
||||
if req.DevID == "" {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "devID不能为空")
|
||||
return
|
||||
}
|
||||
ua.DevID = req.DevID
|
||||
resp, serr := authser.GuestLogin(ctx, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), &req, ua)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
// @Tags 用户认证
|
||||
// @Summary 注册
|
||||
// @Description 传入账号、邮箱、明文密码。请求头带游客 Authorization 时升级该游客为普通用户,否则新建用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body registerReq true "注册参数"
|
||||
// @Success 200 {object} authser.LoginResp "注册成功,返回 token 及用户基本信息"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/register [POST]
|
||||
func Register(ctx *gin.Context) {
|
||||
var req registerReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
ua := applyRequestDevID(requestUA(ctx), req.DevID)
|
||||
resp, serr := authser.Register(ctx, common.GetUserId(ctx), req.AccountNo, req.Email, req.Password, req.AgentInviteCode, ua, ipaddr.ResolveClientIP(ctx), req.Channel)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
// @Tags 用户认证
|
||||
// @Summary 登录
|
||||
// @Description 账号或邮箱均可作为登录凭证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body loginReq true "登录参数"
|
||||
// @Success 200 {object} authser.LoginResp "登录成功,返回 token 及用户基本信息"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/login [POST]
|
||||
func Login(ctx *gin.Context) {
|
||||
var req loginReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
ua := applyRequestDevID(requestUA(ctx), req.DevID)
|
||||
resp, serr := authser.Login(ctx, req.Account, req.Password, ua, ipaddr.ResolveClientIP(ctx), req.Channel)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改用户资料
|
||||
// @Tags 用户认证
|
||||
// @Summary 修改用户资料(头像 / 简介 / 密码,可任意组合)
|
||||
// @Description 用户信息通过 token 获取,无需传账号;nickName、avatar、introduction、oldPassword+newPassword 均为可选,至少提供一项。未设置密码时传 newPassword 可设置密码,已设置密码时修改需传 oldPassword+newPassword
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body updateProfileReq true "修改资料参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/profile [POST]
|
||||
func UpdateProfile(ctx *gin.Context) {
|
||||
userId := common.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, nil)
|
||||
return
|
||||
}
|
||||
var req updateProfileReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
if req.NickName != nil && !wordsfilterser.Validate(*req.NickName) {
|
||||
common.ServerJSON(ctx, srvdef.ErrContainIllegalWord, "包含非法字符")
|
||||
return
|
||||
}
|
||||
serr := authser.UpdateProfile(ctx, &authser.UpdateProfileReq{
|
||||
UserId: userId,
|
||||
NickName: req.NickName,
|
||||
Avatar: req.Avatar,
|
||||
Introduction: req.Introduction,
|
||||
PrivacyMode: req.PrivacyMode,
|
||||
OldPassword: req.OldPassword,
|
||||
NewPassword: req.NewPassword,
|
||||
})
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, "")
|
||||
}
|
||||
|
||||
// GetInfo 获取当前用户信息
|
||||
// @Tags 用户认证
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 通过 token 获取最新用户信息,包含金币余额、VIP状态等
|
||||
// @Produce json
|
||||
// @Success 200 {object} authser.LoginResp "用户信息"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/getInfo [GET]
|
||||
func GetInfo(ctx *gin.Context) {
|
||||
userId := common.GetUserId(ctx)
|
||||
if pubdef.IsCrawlerUser(userId) {
|
||||
common.ServerJSON(ctx, srvdef.Success, authser.CrawlerLoginResp(""))
|
||||
return
|
||||
}
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, nil)
|
||||
return
|
||||
}
|
||||
resp, serr := authser.GetInfo(ctx, userId, requestUA(ctx))
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ResetPassword 找回密码
|
||||
// @Tags 用户认证
|
||||
// @Summary 找回密码
|
||||
// @Description 通过账号和邮箱双重校验后重置密码,两者必须同时匹配同一用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body resetPasswordReq true "找回密码参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/resetPassword [POST]
|
||||
func ResetPassword(ctx *gin.Context) {
|
||||
var req resetPasswordReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
serr := authser.ResetPassword(ctx, req.AccountNo, req.Email, req.NewPassword)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, "")
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package coinapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"videoweb/app/service/coinser"
|
||||
"videoweb/app/service/rechargeser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type buyCoinReq struct {
|
||||
PackageId int64 `json:"packageId" binding:"required"` // 购买套餐ID(金币充值)
|
||||
PayMode string `json:"payMode" binding:"required"` // 支付方式:alipay/wechat
|
||||
}
|
||||
|
||||
// BuyCoin 购买金币(发起第三方支付)
|
||||
// @Tags 金币
|
||||
// @Summary 购买金币
|
||||
// @Description 根据指定金币套餐发起第三方支付,返回支付链接
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body buyCoinReq true "购买参数"
|
||||
// @Success 200 {object} coinser.BuyCoinResp "支付链接"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/coin/buy [POST]
|
||||
func BuyCoin(ctx *gin.Context) {
|
||||
var req buyCoinReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
sysType := ctx.GetString(consts.CtxSysType)
|
||||
devType := ctx.GetString(consts.CtxDevType)
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
|
||||
user, serr := coinser.GetUserInfo(ctx, userId)
|
||||
if serr != nil || user == nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrUserNotExsit, nil)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := rechargeser.PayDeviceID(ctx.GetString(consts.CtxDevId), user.DevID, user.DevV2)
|
||||
resp, serr := coinser.CreateCoinOrder(ctx, userId, user.NickName, req.PackageId, req.PayMode, sysType, devType, deviceID, ip, user.DistrictCode)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ExportCoinTrans 导出当前用户金币余额明细
|
||||
// @Tags 金币
|
||||
// @Summary 导出金币余额明细 -- by 王鸣
|
||||
// @Description 导出当前登录用户的金币收入、支出流水CSV;type=0全部 1充值 2消费 3奖励
|
||||
// @Produce text/csv
|
||||
// @Param type query int false "类型:0全部 1充值 2消费 3奖励"
|
||||
// @Success 200 {file} file "CSV文件"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/coin/trans/export [GET]
|
||||
func ExportCoinTrans(ctx *gin.Context) {
|
||||
var req coinTransListReq
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
data, fileName, err := coinser.ExportCoinTrans(ctx, common.GetUserId(ctx), req.Type)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
ctx.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename*=UTF-8''%s", url.QueryEscape(fileName)))
|
||||
ctx.Data(200, "text/csv; charset=utf-8", data)
|
||||
}
|
||||
|
||||
type coinOrderHistoryReq struct {
|
||||
Page int64 `json:"page" form:"page"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
}
|
||||
|
||||
// GetCoinOrderHistory 查询当前用户金币购买记录
|
||||
// @Tags 金币
|
||||
// @Summary 查询金币购买记录
|
||||
// @Produce json
|
||||
// @Param page query int false "页码(默认1)"
|
||||
// @Param pageSize query int false "每页数量(默认10)"
|
||||
// @Success 200 {object} object "订单列表"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/coin/orders [GET]
|
||||
func GetCoinOrderHistory(ctx *gin.Context) {
|
||||
var req coinOrderHistoryReq
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
list, total, serr := coinser.GetCoinOrderHistory(ctx, userId, req.Page, req.PageSize)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, gin.H{"total": total, "list": list})
|
||||
}
|
||||
|
||||
type coinTransListReq struct {
|
||||
// 页码,默认1
|
||||
Page int64 `json:"page" form:"page"`
|
||||
// 每页数量,默认10,最大100
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
// 类型:0-全部 1-充值 2-消费 3-奖励
|
||||
Type int64 `json:"type" form:"type"`
|
||||
}
|
||||
|
||||
// GetCoinTransList 查询当前用户金币余额明细
|
||||
// @Tags 金币
|
||||
// @Summary 查询金币余额明细 -- by 旺鸣
|
||||
// @Description 返回当前登录用户的金币收入、支出流水,按交易时间倒序;type=0全部 1充值 2消费 3奖励
|
||||
// @Produce json
|
||||
// @Param page query int false "页码(默认1)"
|
||||
// @Param pageSize query int false "每页数量(默认10,最大100)"
|
||||
// @Param type query int false "类型:0全部 1充值 2消费 3奖励"
|
||||
// @Success 200 {object} response.BasePageResp{list=[]proto.CoinTransListItemResp} "成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/coin/trans/list [GET]
|
||||
func GetCoinTransList(ctx *gin.Context) {
|
||||
var req coinTransListReq
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
userId := common.GetUserId(ctx)
|
||||
resp, err := coinser.GetCoinTransList(ctx, userId, req.Page, req.PageSize, req.Type)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package commentapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/commentser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SendComment 发布评论
|
||||
// @Tags 评论APP
|
||||
// @Summary 发布评论 --by 王鸣
|
||||
// @Description 发布短剧或短剧剧集评论,parentsId为0发布一级评论,大于0发布二级评论,replyCommentId表示实际回复的评论,objectType 10-短剧 11-短剧剧集
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.CommentSendReq true "参数列表"
|
||||
// @Success 200 {object} string "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/comment/send [post]
|
||||
func SendComment(ctx *gin.Context) {
|
||||
var in proto.CommentSendReq
|
||||
if err := ctx.ShouldBindJSON(&in); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
err := commentser.SendComment(ctx, common.GetUserId(ctx), ip, &in)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, "success")
|
||||
}
|
||||
|
||||
// List 评论列表
|
||||
// @Tags 评论APP
|
||||
// @Summary 评论列表 --by 王鸣
|
||||
// @Description 分页查询短剧或短剧剧集评论树,一级评论分页返回,二级评论挂在children中,objectType 10-短剧 11-短剧剧集
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.GetCommentListReq true "评论列表请求"
|
||||
// @Success 200 {object} proto.GetCommentListResp{list=[]proto.CommentInfo} "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Router /api/app/comment/list [get]
|
||||
func List(ctx *gin.Context) {
|
||||
var in proto.GetCommentListReq
|
||||
if err := ctx.ShouldBindQuery(&in); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := commentser.GetCommentPage(ctx, common.GetUserId(ctx), &in)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SetLike 评论点赞/取消点赞
|
||||
// @Tags 评论APP
|
||||
// @Summary 评论点赞/取消点赞 --by 王鸣
|
||||
// @Description 设置当前用户对短剧或短剧剧集评论的点赞状态
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.CommentLikeSetReq true "评论点赞设置请求"
|
||||
// @Success 200 {object} proto.CommentLikeSetResp "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/comment/like/set [post]
|
||||
func SetLike(ctx *gin.Context) {
|
||||
var in proto.CommentLikeSetReq
|
||||
if err := ctx.ShouldBindJSON(&in); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := commentser.SetCommentLike(ctx, common.GetUserId(ctx), &in)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Info 查询APP公共配置
|
||||
// @Tags APP配置相关接口
|
||||
// @Summary 查询APP公共配置 --by 王鸣
|
||||
// @Description 返回最新地址信息、完整系统配置、广告配置和全站SEO配置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.ConfigInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/config/info [get]
|
||||
func Info(ctx *gin.Context) {
|
||||
configInfo := configser.GetConfigInfo()
|
||||
resp := &proto.ConfigInfoResp{
|
||||
NewAddressInfo: configser.GetNewAddress(),
|
||||
Config: configInfo,
|
||||
Advitise: configser.GetAdvitise(),
|
||||
SeoInfo: configser.GetSiteSeoInfo(),
|
||||
SeoNavInfo: configser.GetNavSeoInfo(),
|
||||
NavPageList: configser.GetNavPageList(),
|
||||
ReportSDKConfig: configInfo.ReportSDKConfig,
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// FooterInfo 查询APP底部信息
|
||||
// @Tags APP配置相关接口
|
||||
// @Summary 查询APP底部信息 --by 王鸣
|
||||
// @Description 返回APP底部信息
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.FooterInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/config/footer/info [get]
|
||||
func FooterInfo(ctx *gin.Context) {
|
||||
resp := &proto.FooterInfoResp{
|
||||
FooterInfo: configser.GetFooterInfo(),
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// RichTextList 查询APP富文本内容列表
|
||||
// @Tags APP配置相关接口
|
||||
// @Summary 查询APP富文本内容列表 --by 王鸣
|
||||
// @Description 返回启用状态的富文本内容,支持按ids或titles过滤
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param ids query string false "富文本ID,多个用英文逗号分隔"
|
||||
// @Param titles query string false "富文本标题,多个用英文逗号分隔"
|
||||
// @Success 200 {object} proto.AppRichTextListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/config/richtext/list [get]
|
||||
func RichTextList(ctx *gin.Context) {
|
||||
common.ServerJSON(ctx, srvdef.Success, configser.GetAppRichTextList(ctx.Query("ids"), ctx.Query("titles")))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package contactusapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/contactusser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AddContactUs 提交联系我们
|
||||
// @Tags 联系我们相关接口
|
||||
// @Summary 提交联系我们 -- by 旺鸣
|
||||
// @Description 用户提交联系我们表单,问题类型:1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.ContactUsAddReq true "联系我们请求"
|
||||
// @Success 200 {object} proto.ContactUsAddResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/contactus/add [post]
|
||||
func AddContactUs(ctx *gin.Context) {
|
||||
var req proto.ContactUsAddReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(ctx)
|
||||
resp, err := contactusser.AddContactUs(ctx, userId, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package feedbackapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/feedbackser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AddFeedback 提交意见反馈
|
||||
// @Tags 意见反馈相关接口
|
||||
// @Summary 提交意见反馈 -- by 旺鸣
|
||||
// @Description 用户提交意见反馈,问题类型,1-功能建议、2-播放问题、3-账号问题、4-其他
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.FeedbackReq true "意见反馈请求"
|
||||
// @Success 200 {object} proto.FeedbackResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/feedback/add [post]
|
||||
func AddFeedback(ctx *gin.Context) {
|
||||
var req proto.FeedbackReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(ctx)
|
||||
resp, err := feedbackser.AddFeedback(ctx, userId, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -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,112 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/richtextcontentmod"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// VideoTagApi 标签搜索
|
||||
// @Tags 标签搜索
|
||||
// @Summary 标签搜索
|
||||
// @Description 标签搜索
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.HomeHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /richtext/:id [get]
|
||||
func RichtextApi(c *gin.Context) {
|
||||
data := proto.RichTextHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
richtextStr := c.Param("id")
|
||||
richtextId, err := strconv.ParseInt(richtextStr, 10, 64)
|
||||
if err != nil || richtextId < 1 {
|
||||
common.ServerJSON(c, srvdef.NotFound, "")
|
||||
return
|
||||
}
|
||||
richtextDate, serr := richtextData(c, richtextId)
|
||||
if serr != nil {
|
||||
common.ServerJSON(c, serr.Code, "richtext param error")
|
||||
return
|
||||
}
|
||||
data.CompletePath = fmt.Sprintf("/richtext/%d", richtextId)
|
||||
data.RichTextInfo = richtextDate
|
||||
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// PageNotFound 404
|
||||
// @Tags 404
|
||||
// @Summary 404
|
||||
// @Description 404
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
func PageNotFound(c *gin.Context) {
|
||||
data := proto.NotFoundHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
data.CompletePath = c.Request.URL.Path
|
||||
c.JSON(http.StatusOK, data)
|
||||
//common.ServerJSON(c, srvdef.Success, data)
|
||||
|
||||
//c.HTML(404, "404", gin.H{
|
||||
// "data": data,
|
||||
//})
|
||||
}
|
||||
|
||||
func UrlRedirect(c *gin.Context) {
|
||||
data := proto.UrlRedirectData{
|
||||
Path: c.Request.URL.Path,
|
||||
CompletePath: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
url := c.Query("url")
|
||||
data.UrlRedirect = url
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
func richtextData(ctx *gin.Context, richtextId int64) (proto.RichTextInfo, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
richtextcontentmod.FN.RichTextContentModel.Id: richtextId,
|
||||
}
|
||||
richtextInfo, serr := richtextcontentmod.FindOneRichTextContentModelModel(ctx, filter)
|
||||
if serr != nil {
|
||||
log.Error("richtextData", log.Any("filter", filter), log.E(serr))
|
||||
return proto.RichTextInfo{}, serr
|
||||
}
|
||||
return proto.RichTextInfo{
|
||||
Id: richtextInfo.Id,
|
||||
Title: richtextInfo.Title,
|
||||
Content: richtextInfo.Content,
|
||||
Desc: richtextInfo.Desc,
|
||||
SeoInfo: richtextInfo.SeoInfo,
|
||||
}, nil
|
||||
}
|
||||
@@ -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,670 @@
|
||||
package playletapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/playletser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HomePlaylet app端首页短剧接口
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端首页短剧接口 --by 王鸣
|
||||
// @Description 无请求参数,返回首页banner短剧、公告和SEO信息
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.HomePlayletResponse "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/home [get]
|
||||
func HomePlaylet(ctx *gin.Context) {
|
||||
resp, err := playletser.Home(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// AllList app端分页查询全部短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询全部短剧 --by 王鸣
|
||||
// @Description 分页查询已上架短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletAllListReq true "全部短剧分页请求"
|
||||
// @Success 200 {object} proto.PlayletAllListResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/all/list [get]
|
||||
func AllList(ctx *gin.Context) {
|
||||
var req proto.PlayletAllListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.AllList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// HotList app端分页查询热门短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询热门短剧
|
||||
// @Description 按热度查询已上架短剧,可排除指定短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletHotListReq true "热门短剧分页请求"
|
||||
// @Success 200 {object} proto.PlayletHotListResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/hot/list [get]
|
||||
func HotList(ctx *gin.Context) {
|
||||
var req proto.PlayletHotListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.HotList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TopicList app端分页查询专题剧单
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询专题剧单 --by 王鸣
|
||||
// @Description 分页查询已上架且有短剧数据的专题剧单
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletTopicPageListReq true "专题剧单分页请求"
|
||||
// @Success 200 {object} proto.PlayletTopicPageListResp{list=[]proto.PlayletTopicPageItem} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/topic/list [get]
|
||||
func TopicList(ctx *gin.Context) {
|
||||
var req proto.PlayletTopicPageListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.TopicList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TopicDetail app端查询专题详情
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端查询专题详情 --by 王鸣
|
||||
// @Description 按专题ID查询专题信息和专题短剧列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletTopicDetailReq true "专题详情请求"
|
||||
// @Success 200 {object} proto.PlayletTopicDetailResp{list=[]proto.CommonData,moreTopics=[]proto.PlayletTopicPageItem} "成功"
|
||||
// @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.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// HotRankList app端分页查询热榜
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询热榜 --by 王鸣
|
||||
// @Description 分页查询已上架短剧热榜;rankType=1热播榜 2新剧榜 3追剧榜
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletHotRankListReq true "热榜分页请求"
|
||||
// @Success 200 {object} proto.PlayletHotRankListResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/hot/rank/list [get]
|
||||
func HotRankList(ctx *gin.Context) {
|
||||
var req proto.PlayletHotRankListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.HotRankList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TagVideoList app端根据标签ID分页查询短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端根据标签ID分页查询短剧 --by 王鸣
|
||||
// @Description 根据标签ID或标签slug分页查询已上架短剧;slug非空时优先按slug解析标签ID;sortType=1最新 2最热 3收藏最多
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletTagVideoReq true "根据标签ID分页查询短剧请求"
|
||||
// @Success 200 {object} proto.PlayletTagVideoResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/tag/list [get]
|
||||
func TagVideoList(ctx *gin.Context) {
|
||||
var req proto.PlayletTagVideoReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.TagVideoList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// GuessLikeList app端分页查询猜你喜欢短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询猜你喜欢短剧 --by 王鸣
|
||||
// @Description 根据当前短剧同题材查询猜你喜欢,排除当前短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletGuessLikeListReq true "猜你喜欢分页请求"
|
||||
// @Success 200 {object} proto.PlayletGuessLikeListResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/guess-like/list [get]
|
||||
func GuessLikeList(ctx *gin.Context) {
|
||||
var req proto.PlayletGuessLikeListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.GuessLikeList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Search app端ES分页搜索短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端ES分页搜索短剧 --by 王鸣
|
||||
// @Description 使用ES分页搜索已上架短剧,支持剧名、题材名、简介和标签名检索
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletSearchReq true "短剧ES分页搜索请求"
|
||||
// @Success 200 {object} proto.PlayletSearchResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/search [get]
|
||||
func Search(ctx *gin.Context) {
|
||||
var req proto.PlayletSearchReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.Search(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SearchSuggest app端短剧搜索联想
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧搜索联想 --by 王鸣
|
||||
// @Description 搜索框下拉联想,返回短剧列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletSearchSuggestReq true "短剧搜索联想请求"
|
||||
// @Success 200 {object} proto.PlayletSearchSuggestResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/search/suggest [get]
|
||||
func SearchSuggest(ctx *gin.Context) {
|
||||
var req proto.PlayletSearchSuggestReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.SearchSuggest(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// HotLike app端热搜词和猜你喜欢
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端热搜词和猜你喜欢
|
||||
// @Description 返回缓存的前10个热搜词,以及按收藏量最多的15部猜你喜欢短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.PlayletHotLikeResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/hot-like [get]
|
||||
func HotLike(ctx *gin.Context) {
|
||||
common.ServerJSON(ctx, srvdef.Success, playletser.HotLike(ctx))
|
||||
}
|
||||
|
||||
// SetLike app端短剧点赞/取消点赞
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧点赞/取消点赞 --by 王鸣
|
||||
// @Description 设置当前用户短剧点赞状态,点赞数只在状态真实变化时增减
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletLikeSetReq true "短剧点赞设置请求"
|
||||
// @Success 200 {object} proto.PlayletLikeSetResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/like/set [post]
|
||||
func SetLike(ctx *gin.Context) {
|
||||
var req proto.PlayletLikeSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.SetLike(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// LikeList app端我的点赞短剧列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端我的点赞短剧列表 -- by 王鸣
|
||||
// @Description 按点赞时间倒序分页返回当前用户点赞且已上架的短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletLikeListReq true "我的点赞短剧列表请求"
|
||||
// @Success 200 {object} proto.PlayletLikeListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/like/list [get]
|
||||
func LikeList(ctx *gin.Context) {
|
||||
var req proto.PlayletLikeListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.LikeList(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SetCollect app端短剧收藏/取消收藏
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧收藏/取消收藏 --by 王鸣
|
||||
// @Description 设置当前用户短剧收藏状态,收藏数只在状态真实变化时增减
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletCollectSetReq true "短剧收藏设置请求"
|
||||
// @Success 200 {object} proto.PlayletCollectSetResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/collect/set [post]
|
||||
func SetCollect(ctx *gin.Context) {
|
||||
var req proto.PlayletCollectSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.SetCollect(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ForwardHeat app端短剧转发增加热度
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧转发增加热度 --by 王鸣
|
||||
// @Description 请求一次短剧热度值增加5,仅支持已上架短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletForwardHeatReq true "短剧转发加热度请求"
|
||||
// @Success 200 {object} proto.PlayletForwardHeatResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/forward/heat [post]
|
||||
func ForwardHeat(ctx *gin.Context) {
|
||||
var req proto.PlayletForwardHeatReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ForwardHeat(ctx, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// CollectList app端我的短剧收藏列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端我的短剧收藏列表 --by 王鸣
|
||||
// @Description 按当前用户收藏时间倒序分页返回短剧收藏列表,并返回已看/未看集数和最近观看位置;watchStatus=0全部 1追剧中 2已看完
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletCollectListReq true "我的短剧收藏列表请求"
|
||||
// @Success 200 {object} proto.PlayletCollectListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/collect/list [get]
|
||||
func CollectList(ctx *gin.Context) {
|
||||
var req proto.PlayletCollectListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.CollectList(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// BoughtList app端我的已购短剧列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端我的已购短剧列表 --by 王鸣
|
||||
// @Description 按当前用户最近购买时间倒序分页返回已购短剧列表,并返回已购集数、消耗金币和最近观看位置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletBoughtListReq true "我的已购短剧列表请求"
|
||||
// @Success 200 {object} proto.PlayletBoughtListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/bought/list [get]
|
||||
func BoughtList(ctx *gin.Context) {
|
||||
var req proto.PlayletBoughtListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.BoughtList(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// HistoryList app端我的短剧观看历史列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端我的短剧观看历史列表 --by 王鸣
|
||||
// @Description 按当前用户最近观看时间倒序分页返回短剧观看历史;pageSize默认20、最大100
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletHistoryListReq true "观看历史列表请求"
|
||||
// @Success 200 {object} proto.PlayletHistoryListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/history/list [get]
|
||||
func HistoryList(ctx *gin.Context) {
|
||||
var req proto.PlayletHistoryListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.HistoryList(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// DeleteHistory app端删除单条短剧观看历史
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端删除单条短剧观看历史 --by 王鸣
|
||||
// @Description 删除当前用户指定短剧的观看历史,不影响观看进度、收藏和已购记录
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletHistoryDeleteReq true "删除单条观看历史请求"
|
||||
// @Success 200 {object} proto.PlayletHistoryDeleteResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/history/delete [post]
|
||||
func DeleteHistory(ctx *gin.Context) {
|
||||
var req proto.PlayletHistoryDeleteReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.DeleteHistory(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ClearHistory app端清空我的短剧观看历史
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端清空我的短剧观看历史 --by 王鸣
|
||||
// @Description 仅清空观看历史,不影响收藏列表和观看进度
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.PlayletHistoryClearResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/history/clear [post]
|
||||
func ClearHistory(ctx *gin.Context) {
|
||||
resp, err := playletser.ClearHistory(ctx, common.GetUserId(ctx))
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SetChapterLike app端短剧剧集点赞/取消点赞
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧剧集点赞/取消点赞 --by 王鸣
|
||||
// @Description 设置当前用户短剧剧集点赞状态,点赞数只在状态真实变化时增减
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletChapterLikeSetReq true "短剧剧集点赞设置请求"
|
||||
// @Success 200 {object} proto.PlayletChapterLikeSetResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/chapter/like/set [post]
|
||||
func SetChapterLike(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterLikeSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.SetChapterLike(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SetChapterCollect app端短剧剧集收藏/取消收藏
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧剧集收藏/取消收藏 --by 王鸣
|
||||
// @Description 设置当前用户短剧剧集收藏状态,收藏数只在状态真实变化时增减
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletChapterCollectSetReq true "短剧剧集收藏设置请求"
|
||||
// @Success 200 {object} proto.PlayletChapterCollectSetResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/chapter/collect/set [post]
|
||||
func SetChapterCollect(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterCollectSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.SetChapterCollect(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ChapterCollectList app端我的短剧剧集收藏列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端我的短剧剧集收藏列表 --by 王鸣
|
||||
// @Description 按当前用户收藏时间倒序分页返回短剧剧集收藏列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletChapterCollectListReq true "我的短剧剧集收藏列表请求"
|
||||
// @Success 200 {object} proto.PlayletChapterCollectListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/chapter/collect/list [get]
|
||||
func ChapterCollectList(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterCollectListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ChapterCollectList(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ReportChapterWatch app端短剧剧集观看上报
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端短剧剧集观看上报 --by 王鸣
|
||||
// @Description 上报当前用户短剧剧集播放进度,达到已看条件时用于我的收藏未看集数计算
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletChapterWatchReportReq true "短剧剧集观看上报请求"
|
||||
// @Success 200 {object} proto.PlayletChapterWatchReportResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/chapter/watch/report [post]
|
||||
func ReportChapterWatch(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterWatchReportReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ReportChapterWatch(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// UnlockChapter app端使用金币逐集解锁短剧剧集
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端使用金币逐集解锁短剧剧集 --by 王鸣
|
||||
// @Description 仅金币付费剧集可调用;接口具备幂等性,已解锁剧集重复调用不会重复扣金币
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.PlayletChapterUnlockReq true "短剧剧集金币解锁请求"
|
||||
// @Success 200 {object} proto.PlayletChapterUnlockResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/playlet/chapter/unlock [post]
|
||||
func UnlockChapter(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterUnlockReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err1 := playletser.UnlockChapter(ctx, common.GetUserId(ctx), &req)
|
||||
if err1 != nil {
|
||||
common.ServerJSON(ctx, err1.Code, err1.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Detail app端根据ID查询短剧详情
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端根据ID查询短剧详情 --by 王鸣
|
||||
// @Description 根据短剧ID或slug查询已上架短剧详情;slug非空时优先按slug解析短剧ID
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletInfoReq true "短剧详情请求"
|
||||
// @Success 200 {object} proto.PlayletInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/detail [get]
|
||||
func Detail(ctx *gin.Context) {
|
||||
var req proto.PlayletInfoReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.Detail(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ChapterDetail app端根据ID查询短剧剧集详情
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端根据ID查询短剧剧集详情 --by 王鸣
|
||||
// @Description 根据短剧剧集ID查询播放页当前剧集详情;可传playletId或playletSlug校验剧集归属,slug非空时优先解析短剧ID
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletChapterInfoReq true "短剧剧集详情请求"
|
||||
// @Success 200 {object} proto.PlayletChapterInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/chapter/detail [get]
|
||||
func ChapterDetail(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterInfoReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ChapterDetail(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// ChapterList app端根据短剧ID查询剧集列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端根据短剧ID查询剧集列表 --by 王鸣
|
||||
// @Description 根据短剧ID或slug查询右侧选集列表;slug非空时优先按slug解析短剧ID
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletChapterListReq true "短剧剧集列表请求"
|
||||
// @Success 200 {object} proto.PlayletChapterListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/chapter/list [get]
|
||||
func ChapterList(ctx *gin.Context) {
|
||||
var req proto.PlayletChapterListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.ChapterList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package playlettagapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playlettagser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List app端短剧标签列表
|
||||
// @Tags app短剧标签相关接口
|
||||
// @Summary app端短剧标签列表 --by 王鸣
|
||||
// @Description 无请求参数,不分页,返回已上架短剧标签列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.PlayletTagListResponse "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/tag/options [get]
|
||||
func List(ctx *gin.Context) {
|
||||
resp, err := playlettagser.List(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package playletthemeapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/playletthemeser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List app端短剧题材列表
|
||||
// @Tags app短剧题材相关接口
|
||||
// @Summary app端短剧题材列表 --by 王鸣
|
||||
// @Description 无请求参数,不分页,返回已上架题材列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.PlayletThemeListResponse "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/theme/list [get]
|
||||
func List(ctx *gin.Context) {
|
||||
resp, err := playletthemeser.List(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package proto
|
||||
|
||||
// AboutNodeItem 关于我们节点
|
||||
type AboutNodeItem struct {
|
||||
NodeId int64 `json:"nodeId"`
|
||||
Title string `json:"title"`
|
||||
SubTitle string `json:"subTitle"`
|
||||
Content string `json:"content"` // JSON字符串
|
||||
//类型描述:
|
||||
//文本:录入纯文本信息
|
||||
//声明:图片、文本信息
|
||||
//条目:可以录入多条,可删可增
|
||||
//收录:收录类型(文章、网址、工具)、价格、单位、时效、描述
|
||||
//图文:图片、文本信息
|
||||
//注意:图片、文本信息
|
||||
Type int `json:"type"`
|
||||
ServiceDuration string `json:"serviceDuration"` // 服务时效
|
||||
DurationIcon string `json:"durationIcon"` // 时效图标(CDN已处理)
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// AboutData 关于我们页面数据
|
||||
type AboutData struct {
|
||||
Intro string `json:"intro"` // 简介
|
||||
Nodes []*AboutNodeItem `json:"nodes"` // 节点列表
|
||||
Categories []*ToolCategoryItem `json:"categories"` // 分类列表
|
||||
Site SiteCommonData `json:"site"` // 通用站点数据
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/storage/proxy"
|
||||
|
||||
type AdsClickReq struct {
|
||||
AdsId string `json:"adsId,omitempty" binding:"required"`
|
||||
Location int64 `json:"location,omitempty" binding:"required"` // 位置ID
|
||||
}
|
||||
|
||||
type AdsClickInfo struct {
|
||||
AppId int64 `json:"appId,omitempty" binding:"required"` // appId
|
||||
AdsId int64 `json:"adsId,omitempty" binding:"required"`
|
||||
Location int64 `json:"location,omitempty" binding:"required"` // 位置ID
|
||||
}
|
||||
|
||||
type AdvitiseData struct {
|
||||
HomeList []proxy.AdvitiseInfo `json:"homeList" bson:"homeList"` // 首页列表
|
||||
DetailTopList []proxy.AdvitiseInfo `json:"detailTopList" bson:"detailTopList"` // 详情页顶部广告
|
||||
NineGridList []proxy.AdvitiseInfo `json:"nineGridList" bson:"nineGridList"` // 9宫格
|
||||
DetailbannerList []proxy.AdvitiseInfo `json:"detailbannerList" bson:"detailbannerList"` // 详情banner
|
||||
LaunchScreenList []proxy.AdvitiseInfo `json:"launchScreenList"` // 启动页广告
|
||||
FloatingList []proxy.AdvitiseInfo `json:"floatingList" bson:"floatingList"` // 悬浮小方块
|
||||
AList []proxy.AdvitiseInfo `json:"AList"` // 固定12广告位
|
||||
BList []proxy.AdvitiseInfo `json:"BList"` // 固定13广告位
|
||||
DialogList []proxy.AdvitiseInfo `json:"dialogList" bson:"dialogList"` // 弹窗广告
|
||||
BottomBanner []proxy.AdvitiseInfo `json:"bottomBanner" bson:"bottomBanner"` // 底部banner
|
||||
|
||||
VideoInfoList []proxy.AdvitiseInfo `json:"videoInfoList" bson:"videoInfoList"` // 视频详情
|
||||
DownList []proxy.AdvitiseInfo `json:"downList" bson:"downList"` // 底部广告
|
||||
FriendList []proxy.AdvitiseInfo `json:"friendList" bson:"friendList"` // 友链
|
||||
TitleList []proxy.AdvitiseInfo `json:"titleList" bson:"titleList"` // 标题广告
|
||||
PlayList []proxy.AdvitiseInfo `json:"playList" bson:"playList"` // 播放强制跳转
|
||||
PauseyList []proxy.AdvitiseInfo `json:"pauseyList" bson:"pauseyList"` // 暂停广告
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
// -------- 公共站点数据 --------
|
||||
|
||||
// FriendlyLinkItem 友情链接
|
||||
type FriendlyLinkItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// ContactInfoItem 联系方式
|
||||
type ContactInfoItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Icon string `json:"icon"`
|
||||
Type int `json:"type"` // 1=邮箱 2=URL 3=文字
|
||||
Address string `json:"address"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// SiteCommonData 每个接口都会携带的通用站点信息
|
||||
type SiteCommonData struct {
|
||||
Config ConfigInfo `json:"config"`
|
||||
FriendlyLinks []*FriendlyLinkItem `json:"friendlyLinks"`
|
||||
ContactInfos []*ContactInfoItem `json:"contactInfos"`
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
ToolCount int64 `json:"toolCount"` // 审核通过的工具总数
|
||||
}
|
||||
|
||||
// -------- 分类 --------
|
||||
|
||||
// ToolSubCategoryItem 子分类简要信息
|
||||
type ToolSubCategoryItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
}
|
||||
|
||||
// ToolCategoryItem 主分类简要信息
|
||||
type ToolCategoryItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Sort int `json:"sort"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
SubCategories []*ToolSubCategoryItem `json:"subCategories"`
|
||||
}
|
||||
|
||||
// -------- 工具 --------
|
||||
|
||||
// ToolBriefItem 工具简要信息(列表卡片)
|
||||
type ToolBriefItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Logo string `json:"logo"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
Intro string `json:"intro"`
|
||||
OfficialUrl string `json:"officialUrl"`
|
||||
Hot int64 `json:"hot"`
|
||||
}
|
||||
|
||||
// CategorySection 首页分类区块:分类标题 + 子分类列表 + 前10条工具
|
||||
type CategorySection struct {
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
CategorySlug string `json:"categorySlug"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
CategoryIcon string `json:"categoryIcon"`
|
||||
CategorySort int `json:"categorySort"`
|
||||
SubCategories []*ToolSubCategoryItem `json:"subCategories"`
|
||||
Tools []*ToolBriefItem `json:"tools"`
|
||||
}
|
||||
|
||||
// ToolNodeItem 工具节点
|
||||
type ToolNodeItem struct {
|
||||
NodeId int64 `json:"nodeId"`
|
||||
Title string `json:"title"`
|
||||
Type int `json:"type"` // 1=文本 2=条目 3=QA 4=图片
|
||||
Content string `json:"content"` // JSON字符串,格式因类型而异
|
||||
Sort int `json:"sort"`
|
||||
ReviewStatus int `json:"reviewStatus"` // 0=待审核 1=通过 2=下架
|
||||
}
|
||||
|
||||
// ToolRecommendItem 推荐工具简要信息
|
||||
type ToolRecommendItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
// ArticleRecommendItem 推荐文章简要信息
|
||||
type ArticleRecommendItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Cover string `json:"cover"`
|
||||
CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
|
||||
}
|
||||
|
||||
// ToolDetailItem 工具详情
|
||||
type ToolDetailItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
SubCateId int64 `json:"subCateId"`
|
||||
SubCateName string `json:"subCateName"`
|
||||
Title string `json:"title"`
|
||||
Logo string `json:"logo"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
Intro string `json:"intro"`
|
||||
Desc string `json:"desc"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
OfficialUrl string `json:"officialUrl"`
|
||||
Hot int64 `json:"hot"`
|
||||
ViewCount int64 `json:"viewCount"`
|
||||
Nodes []*ToolNodeItem `json:"nodes"`
|
||||
RelatedTools []*ToolRecommendItem `json:"relatedTools"` // 相关推荐(优先同子分类,不足补其他分类)
|
||||
RecommendTools []*ToolRecommendItem `json:"recommendTools"` // 工具推荐(随机6个)
|
||||
RecommendArticles []*ArticleRecommendItem `json:"recommendArticles"` // 文章推荐(随机6个)
|
||||
CommentList GetCommentListResp `json:"commentList"` // 评论
|
||||
Categories []*ToolCategoryItem `json:"categories"` // 分类列表
|
||||
Site SiteCommonData `json:"site"`
|
||||
}
|
||||
|
||||
// -------- 文章 --------
|
||||
|
||||
// ArticleBriefItem 文章简要信息(列表卡片)
|
||||
type ArticleBriefItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Author string `json:"author"`
|
||||
AuthorAvatar string `json:"authorAvatar"`
|
||||
Cover string `json:"cover"`
|
||||
ViewCount int64 `json:"viewCount"`
|
||||
Sort int `json:"sort"`
|
||||
CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
|
||||
}
|
||||
|
||||
// ArticleNodeItem 文章节点
|
||||
type ArticleNodeItem struct {
|
||||
NodeId int64 `json:"nodeId"`
|
||||
Title string `json:"title"`
|
||||
Type int `json:"type"` // 1=文本 2=条目 3=QA 4=图片
|
||||
Content string `json:"content"` // JSON字符串,格式因类型而异
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// ArticleNavItem 文章导航(上一篇/下一篇)
|
||||
type ArticleNavItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// ArticleDetailItem 文章详情
|
||||
type ArticleDetailItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Author string `json:"author"`
|
||||
AuthorAvatar string `json:"authorAvatar"`
|
||||
Cover string `json:"cover"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"`
|
||||
ViewCount int64 `json:"viewCount"`
|
||||
Sort int `json:"sort"`
|
||||
ReviewStatus int `json:"reviewStatus"`
|
||||
CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
|
||||
Nodes []*ArticleNodeItem `json:"nodes"`
|
||||
Prev *ArticleNavItem `json:"prev"` // 上一篇(无则为 null)
|
||||
Next *ArticleNavItem `json:"next"` // 下一篇(无则为 null)
|
||||
RecommendTools []*ToolRecommendItem `json:"recommendTools"` // 工具推荐(随机6个)
|
||||
RecommendArticles []*ArticleRecommendItem `json:"recommendArticles"` // 文章推荐(随机6个)
|
||||
CommentList GetCommentListResp `json:"commentList"` // 评论
|
||||
Categories []*ToolCategoryItem `json:"categories"` // 分类列表
|
||||
Site SiteCommonData `json:"site"`
|
||||
}
|
||||
|
||||
// -------- 页面级响应 --------
|
||||
|
||||
// AIToolsHomeData 工具首页数据
|
||||
type AIToolsHomeData struct {
|
||||
Categories []*ToolCategoryItem `json:"categories"`
|
||||
HotTools []*ToolBriefItem `json:"hotTools"` // 热门推荐工具(按设置时间倒序)
|
||||
Sections []*CategorySection `json:"sections"`
|
||||
Articles []*ArticleBriefItem `json:"articles"`
|
||||
Site SiteCommonData `json:"site"`
|
||||
}
|
||||
|
||||
// AIToolsListData 工具列表页数据
|
||||
type AIToolsListData struct {
|
||||
Categories []*ToolCategoryItem `json:"categories"`
|
||||
Total int64 `json:"total"`
|
||||
List []*ToolBriefItem `json:"list"`
|
||||
Site SiteCommonData `json:"site"`
|
||||
}
|
||||
|
||||
// SearchArticleItem 搜索命中的文章
|
||||
type SearchArticleItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Intro string `json:"intro"`
|
||||
Author string `json:"author"`
|
||||
AuthorAvatar string `json:"authorAvatar"`
|
||||
Cover string `json:"cover"`
|
||||
CreatedAt string `json:"createdAt"` // 格式:yyyy/MM/dd
|
||||
Score float32 `json:"score"` // 相关度分数
|
||||
}
|
||||
|
||||
// SearchToolItem 搜索命中的工具
|
||||
type SearchToolItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
SubCateId int64 `json:"subCateId"`
|
||||
Title string `json:"title"`
|
||||
Logo string `json:"logo"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
Intro string `json:"intro"`
|
||||
OfficialUrl string `json:"officialUrl"`
|
||||
Hot int64 `json:"hot"`
|
||||
Score float32 `json:"score"` // 相关度分数
|
||||
}
|
||||
|
||||
// AISearchData 搜索结果
|
||||
type AISearchData struct {
|
||||
Keyword string `json:"keyword"`
|
||||
Articles []*SearchArticleItem `json:"articles"`
|
||||
Tools []*SearchToolItem `json:"tools"`
|
||||
Site SiteCommonData `json:"site"`
|
||||
}
|
||||
|
||||
// AIRecommendData 推荐展示数据
|
||||
type AIRecommendData struct {
|
||||
Articles []*ArticleBriefItem `json:"articles"` // 随机2篇文章
|
||||
Tools []*ToolBriefItem `json:"tools"` // 随机20个工具
|
||||
Site SiteCommonData `json:"site"`
|
||||
}
|
||||
|
||||
// AIArticleListData 文章列表页数据
|
||||
type AIArticleListData struct {
|
||||
Categories []*ToolCategoryItem `json:"categories"`
|
||||
Total int64 `json:"total"`
|
||||
List []*ArticleBriefItem `json:"list"`
|
||||
Site SiteCommonData `json:"site"`
|
||||
RecommendTools []*ToolRecommendItem `json:"recommendTools"` // 工具推荐(随机6个)
|
||||
RecommendArticles []*ArticleRecommendItem `json:"recommendArticles"` // 文章推荐(随机6个)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package proto
|
||||
|
||||
// AppListResp 应用列表接口响应
|
||||
type AppListResp struct {
|
||||
// 当前路径
|
||||
Path string `json:"path"`
|
||||
// 配置信息
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 应用分类列表(含各分类下的应用列表)
|
||||
CategoryList []AppCategoryInfo `json:"categoryList"`
|
||||
// 当前分类(含应用列表)
|
||||
Category AppCategoryInfo `json:"category"`
|
||||
}
|
||||
|
||||
// AppCategoryInfo 应用分类信息
|
||||
type AppCategoryInfo struct {
|
||||
// 分类ID
|
||||
Id int64 `json:"id"`
|
||||
// 分类标题
|
||||
Title string `json:"title"`
|
||||
// 分类简介
|
||||
Intro string `json:"intro"`
|
||||
// 排序
|
||||
Sort int64 `json:"sort"`
|
||||
// 分类编码(特殊值:12=广告位A 13=广告位B)
|
||||
Code int64 `json:"code"`
|
||||
// 应用列表
|
||||
AppList []AppInfoData `json:"appList"`
|
||||
}
|
||||
|
||||
// AppInfoData 应用信息
|
||||
type AppInfoData struct {
|
||||
// 应用ID
|
||||
Id int64 `json:"id"`
|
||||
// 所属分类ID
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
// 应用名称
|
||||
Name string `json:"name"`
|
||||
// 简介
|
||||
Intro string `json:"intro"`
|
||||
// 详细描述
|
||||
Description string `json:"description"`
|
||||
// Logo 图片地址
|
||||
Logo string `json:"logo"`
|
||||
// 跳转链接(已含渠道码)
|
||||
Url string `json:"url"`
|
||||
// 排序
|
||||
Sort int64 `json:"sort"`
|
||||
// 广告ID(广告条目时返回)
|
||||
AdvitiseId string `json:"advitiseId"`
|
||||
// 广告位置ID
|
||||
Location int64 `json:"location"`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/models/cointransmod"
|
||||
|
||||
// CoinTransListItemResp 金币余额明细列表项
|
||||
type CoinTransListItemResp struct {
|
||||
// 流水ID
|
||||
Id int64 `json:"id"`
|
||||
// 标题,优先使用备注,备注为空时使用交易类型名称
|
||||
Title string `json:"title"`
|
||||
// 收支方向:1-收入,-1-支出
|
||||
FlowDir cointransmod.FlowDir `json:"flowDir"`
|
||||
// 收支方向名称:收入/支出
|
||||
FlowDirName string `json:"flowDirName"`
|
||||
// 交易类型 1-金币充值(购买金币)2-购买会员赠送金币 3-历史遗留 4-视频解锁 5-管理员手动调整
|
||||
TranType cointransmod.TranType `json:"tranType"`
|
||||
// 交易类型名称
|
||||
TranTypeName string `json:"tranTypeName"`
|
||||
// 本次交易金币数,始终为正数
|
||||
Amount int64 `json:"amount"`
|
||||
// 带方向的金币变动数,收入为正数,支出为负数
|
||||
ChangeAmount int64 `json:"changeAmount"`
|
||||
// 交易前余额
|
||||
BalanceBefore int64 `json:"balanceBefore"`
|
||||
// 交易后余额
|
||||
BalanceAfter int64 `json:"balanceAfter"`
|
||||
// 关联订单号
|
||||
OrderNo string `json:"orderNo"`
|
||||
// 关联内容ID
|
||||
RelatedId int64 `json:"relatedId"`
|
||||
// 备注
|
||||
Remark string `json:"remark"`
|
||||
// 交易时间,格式:2006-01-02 15:04:05
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// GetCommentListReq 评论列表请求
|
||||
type GetCommentListReq struct {
|
||||
ObjectId int64 `form:"objectId" json:"objectId" binding:"required"` // 对象ID(短剧id 短剧剧集id)
|
||||
ObjectType int64 `form:"objectType" json:"objectType" binding:"required"` // 对象类型 10-短剧 11-短剧剧集
|
||||
SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1-热门,按点赞数倒序 2-最新,按创建时间倒序
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// GetCommentListResp 评论列表响应
|
||||
type GetCommentListResp struct {
|
||||
ObjectId int64 `json:"objectId"` // 对象ID
|
||||
TotalCount int64 `json:"totalCount"` // 总数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommentInfo `json:"list"` // 数据
|
||||
}
|
||||
|
||||
// CommentSendReq 评论发布请求
|
||||
type CommentSendReq struct {
|
||||
ObjectId int64 `json:"objectId"` // 对象ID,短剧评论传短剧ID,剧集评论传剧集ID
|
||||
ObjectType int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集
|
||||
ParentsId int64 `json:"parentsId"` // 一级评论ID,0-发布一级评论,大于0-发布二级评论
|
||||
ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论ID,0时默认回复parentsId
|
||||
Text string `json:"text"` // 评论内容
|
||||
}
|
||||
|
||||
// CommentLikeSetReq 评论点赞设置请求
|
||||
type CommentLikeSetReq struct {
|
||||
CommentId int64 `json:"commentId" binding:"required"` // 评论ID
|
||||
IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
|
||||
}
|
||||
|
||||
// CommentLikeSetResp 评论点赞设置响应
|
||||
type CommentLikeSetResp struct {
|
||||
CommentId int64 `json:"commentId"` // 评论ID
|
||||
IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
|
||||
CountLike int64 `json:"countLike"` // 评论点赞数
|
||||
}
|
||||
|
||||
// CommentInfo 评论信息
|
||||
type CommentInfo struct {
|
||||
Id int64 `json:"id"` // 评论ID
|
||||
ObjectId int64 `json:"objectId"` // 对象ID
|
||||
ObjectType int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集
|
||||
UserId int64 `json:"userId"` // 评论用户ID
|
||||
UserName string `json:"userName"` // 用户名称
|
||||
UserAvatar string `json:"userAvatar"` // 用户头像,已拼接图片域名
|
||||
Text string `json:"text"` // 评论内容
|
||||
Level int64 `json:"level"` // 评论级别 1-一级评论 2-二级评论
|
||||
ParentsId int64 `json:"parentsId"` // 父评论ID,一级评论为0
|
||||
ReplyCommentId int64 `json:"replyCommentId"` // 被回复评论ID,一级评论为0
|
||||
ReplyUserName string `json:"replyUserName"` // 被回复用户昵称
|
||||
CommentCount int64 `json:"commentCount"` // 二级评论数量
|
||||
CountLike int64 `json:"countLike"` // 评论点赞数
|
||||
IsLike int64 `json:"isLike"` // 当前用户点赞状态 1-已点赞 2-未点赞
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
Children []CommentInfo `json:"children"` // 二级评论列表
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package proto
|
||||
|
||||
type ConfigInfo struct {
|
||||
PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
|
||||
NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
|
||||
NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
|
||||
NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
|
||||
ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
|
||||
GmailAddr string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
|
||||
AppAddr string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
|
||||
OfficialAppAddr string `json:"officialAppAddr" bson:"officialAppAddr"` //官网app地址
|
||||
WeiChart string `json:"weiChart" bson:"weiChart"` //微信群
|
||||
QQ string `json:"qq" bson:"qq"` //qq群
|
||||
TgGroup string `json:"tgGroup" bson:"tgGroup"` //tg群
|
||||
PotaoGroup string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
|
||||
GitAddr string `json:"gitAddr" bson:"gitAddr"` //git地址
|
||||
TwitterAddr string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
|
||||
Telegram string `json:"telegram" bson:"telegram"` //商务tg
|
||||
TelegramAdd string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
|
||||
TranslationAddr string `json:"translationAddr" bson:"translationAddr"` //应用导航
|
||||
ImgCdn string `json:"imgCdn" bson:"imgCdn"` // 图片CDN
|
||||
TsCdn string `json:"tsCdn" bson:"tsCdn"` // 视频CDN
|
||||
ImgAppUsCdn string `json:"imgAppUsCdn" bson:"imgAppUsCdn"` // 图片CDN(海外app使用)
|
||||
MediaAppUsCdn string `json:"mediaAppUsCdn" bson:"mediaAppUsCdn"` // 视频CDN(海外app使用)
|
||||
WebTitle string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
|
||||
WebKeyworld []string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
|
||||
WebDescription string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
|
||||
WebSiteName string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
|
||||
WebAddr string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
|
||||
DHNewUrlAddr1 string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路1
|
||||
DHNewUrlAddr2 string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
|
||||
DHNewUrlAddr3 string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
|
||||
FreeVPN string `json:"freeVPN" bson:"freeVPN"` // 免费vpn链接
|
||||
UseIngAddress []string `json:"useIngAddress" bson:"useIngAddress"` // 所有使用中的域名
|
||||
AdvitiesTopicNum int64 `json:"advitiesTopicNum" bson:"advitiesTopicNum"` // 广告间隔
|
||||
DHTitle string `json:"dhTitle" bson:"dhTitle"` // 导航SEO标题
|
||||
DHKeyworld []string `json:"dhKeyworld" bson:"dhKeyworld"` // 导航SEO关键字
|
||||
DHDescription string `json:"dhDescription" bson:"dhDescription"` // 导航SEO描述
|
||||
DHSiteName string `json:"dhSiteName" bson:"dhSiteName"` // 导航SEO网站名称
|
||||
ActressBackGround string `json:"actressBackGround" bson:"actressBackGround"` // 女优通用背景图
|
||||
ReportSDKConfig string `json:"reportSdkConfig"` // 数据上报SDK域名配置
|
||||
WatermarkImage string `json:"watermarkImage"` // 图片水印文件
|
||||
CustomerServiceHost string `json:"customerServiceHost"` // 在线客服
|
||||
ChannelCode string `json:"channelCode"` // 渠道码
|
||||
EnableH265 bool `json:"enableH265"` // 是否允许客户端使用 H265 地址播放
|
||||
}
|
||||
|
||||
type AdvitiseInfo struct {
|
||||
Id int64 `json:"id" bson:"_id"` // 广告id
|
||||
Title string `json:"title" bson:"title"` // 广告标题
|
||||
Images string `json:"images" bson:"images"` // 视频banner
|
||||
Link string `json:"link" bson:"link"` // 链接
|
||||
}
|
||||
|
||||
// ConfigInfoResp APP配置响应
|
||||
type ConfigInfoResp struct {
|
||||
// 最新地址信息
|
||||
NewAddressInfo NewAddressInfo `json:"newAddressInfo"`
|
||||
// 完整系统配置(与首页等接口的 config 一致,含 potaoGroup 等其余字段)
|
||||
Config ConfigInfo `json:"config"`
|
||||
// 广告配置
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 全站SEO信息
|
||||
SeoInfo SeoInfo `json:"seoInfo"`
|
||||
// 导航SEO信息
|
||||
SeoNavInfo SeoNavInfo `json:"seoNavInfo"`
|
||||
// 导航页线路集合
|
||||
NavPageList []NavPageItem `json:"navPageList"`
|
||||
// 数据上报SDK域名配置
|
||||
ReportSDKConfig string `json:"reportSdkConfig"`
|
||||
}
|
||||
|
||||
// SeoNavInfo 导航SEO信息
|
||||
type SeoNavInfo struct {
|
||||
DHTitle string `json:"dhTitle"` // 导航标题
|
||||
DHKeyworld []string `json:"dhKeyworld"` // 关键词
|
||||
DHDescription string `json:"dhDescription"` // 导航描述
|
||||
DHSiteName string `json:"dhSiteName"` // 导航名称
|
||||
}
|
||||
|
||||
// NavPageItem 导航页线路
|
||||
type NavPageItem struct {
|
||||
Line int64 `json:"line"` // 线路序号
|
||||
Url string `json:"url"` // 线路地址
|
||||
}
|
||||
|
||||
// FooterInfoResp APP底部信息响应
|
||||
type FooterInfoResp struct {
|
||||
FooterInfo FooterInfo `json:"footerInfo"` // 底部信息
|
||||
}
|
||||
|
||||
// FooterInfo APP底部信息
|
||||
type FooterInfo struct {
|
||||
Brand FooterBrand `json:"brand"` // 品牌信息
|
||||
NavList []TranslationCategory `json:"navList"` // 底部导航
|
||||
ContactList []FooterContact `json:"contactList"` // 联系方式
|
||||
}
|
||||
|
||||
// FooterBrand APP底部品牌信息
|
||||
type FooterBrand struct {
|
||||
Title string `json:"title"` // 品牌标题
|
||||
Content string `json:"content"` // 品牌标语
|
||||
}
|
||||
|
||||
// FooterContact APP底部联系方式
|
||||
type FooterContact struct {
|
||||
Name string `json:"name"` // 联系方式名称
|
||||
Value string `json:"value"` // 联系方式值
|
||||
}
|
||||
|
||||
// NewAddressInfo 最新地址
|
||||
type NewAddressInfo struct {
|
||||
NewUrlAddr string `json:"newUrlAddr"` // 新国内地址
|
||||
NewUrlAddr1 string `json:"newUrlAddr1"` // 备用地址1
|
||||
NewUrlAddr2 string `json:"newUrlAddr2"` // 备用地址1
|
||||
ForeverUrlAddr string `json:"foreverUrlAddr"` // 永久海外地址(翻墙访问 永不迷路)
|
||||
GmailAddr string `json:"gmailAddr"` // 邮箱地址
|
||||
TgGroup string `json:"tgGroup"` // tg群
|
||||
Telegram string `json:"telegram" bson:"telegram"` // 商务tg
|
||||
CustomerServiceHost string `json:"customerServiceHost"` // 在线客服
|
||||
TwitterAddr string `json:"twitterAddr"` // 推特地址
|
||||
PdfAddr string `json:"pdfAddr"` // 回家的路PDF
|
||||
AppAddr string `json:"appAddr"` // APP下载地址(落地页)
|
||||
OfficialAppAddr string `json:"officialAppAddr"` // 官网app地址
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package proto
|
||||
|
||||
// ContactUsAddReq 联系我们请求
|
||||
type ContactUsAddReq struct {
|
||||
// 邮箱
|
||||
Email string `json:"email" binding:"required"`
|
||||
// 问题类型,1-订单问题、2-账号问题、3-功能问题、4-体验问题、5-其他
|
||||
Type int32 `json:"type" binding:"required"`
|
||||
// 标题
|
||||
Title string `json:"title" binding:"required"`
|
||||
// 内容
|
||||
Content string `json:"content" binding:"required"`
|
||||
// 附件地址,可存放多张图片或文件地址
|
||||
Attachments []string `json:"attachments"`
|
||||
}
|
||||
|
||||
// ContactUsAddResp 联系我们响应
|
||||
type ContactUsAddResp struct {
|
||||
// 提示信息
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package proto
|
||||
|
||||
type ContributeData struct {
|
||||
Path string `json:"path"`
|
||||
RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package proto
|
||||
|
||||
// DHHtmlData 导航数据
|
||||
type DHHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
|
||||
// DHConsultingHtmlData 招募
|
||||
type DHConsultingHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
|
||||
// DHConsultingInfoHtmlData 招募
|
||||
type DHConsultingInfoHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package proto
|
||||
|
||||
// FeedbackReq 意见反馈请求
|
||||
type FeedbackReq struct {
|
||||
// 问题类型,1-功能建议、2-Bug反馈、3-体验问题、4-账号问题、5-其他
|
||||
Type int32 `json:"type" binding:"required"`
|
||||
// 描述内容,最多500字
|
||||
Content string `json:"content" binding:"required"`
|
||||
// 问题截图,可存放多张图片地址
|
||||
Screenshots []string `json:"screenshots"`
|
||||
// 联系方式,可为空
|
||||
Contact string `json:"contact"`
|
||||
}
|
||||
|
||||
// FeedbackResp 意见反馈响应
|
||||
type FeedbackResp struct {
|
||||
// 提示信息
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -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,12 @@
|
||||
package proto
|
||||
|
||||
// LawAndTrustHtmlData
|
||||
type LawAndTrustHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
// HomePlayletResponse 首页响应
|
||||
type HomePlayletResponse struct {
|
||||
HomeBannerData []*CommonData `json:"homeBannerData"` // 首页banner短剧信息列表
|
||||
HomeConfigData []*HomeConfigData `json:"homeConfigData"` // 首页配置集合
|
||||
HotSearch []PlayletHotSearchItem `json:"hotSearch"` // 大家都在搜,按搜索次数排序,最多15个
|
||||
Announcements []*AnnouncementItem `json:"announcements"` // 启用中的公告,来自独立缓存
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
|
||||
}
|
||||
|
||||
// HomeConfigData 首页配置数据
|
||||
type HomeConfigData struct {
|
||||
Id int64 `json:"id"` // ID
|
||||
MoreCategoryId int64 `json:"moreCategoryId"` // “更多”跳转导航分类ID
|
||||
MoreUrl string `json:"moreUrl"` // “更多”跳转地址,空表示不展示
|
||||
Name string `json:"name"` // 名称
|
||||
Slug string `json:"slug"` // slug,H5 更多页优先用它
|
||||
Desc string `json:"desc"` // 描述
|
||||
Type int32 `json:"type"` // 类型 1-短剧 3-题材
|
||||
Rank int64 `json:"rank"` // 排序,越大越靠前
|
||||
SortType int32 `json:"sortType"` // 排序字段 1-推荐 2-热播 3-最近更新 4-新片预告 5-分类
|
||||
Status bool `json:"status"` // 上下架状态 true-上架 false-下架
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updatedAt"` // 修改时间
|
||||
List []*CommonData `json:"list"` // 配置对应短剧集合,type=1时返回
|
||||
ThemeList []*HomeConfigThemeData `json:"themeList"` // 题材集合,type=3时返回
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// HomeConfigThemeData 首页配置题材数据
|
||||
type HomeConfigThemeData struct {
|
||||
Id int64 `json:"id"` // 题材ID
|
||||
Name string `json:"name"` // 题材名称
|
||||
Slug string `json:"slug"` // 题材slug
|
||||
Icon string `json:"icon"` // 题材图标,已拼接图片域名
|
||||
Rank int64 `json:"rank"` // 排序,越大越靠前
|
||||
PlayletCount int64 `json:"playletCount"` // 绑定短剧数量
|
||||
List []*CommonData `json:"list"` // 题材下短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo"` // 题材对应页面SEO信息
|
||||
}
|
||||
|
||||
// AnnouncementItem 首页公告
|
||||
type AnnouncementItem struct {
|
||||
Id int64 `json:"id"` // 公告ID
|
||||
Title string `json:"title"` // 标题
|
||||
Content string `json:"content"` // 内容
|
||||
}
|
||||
|
||||
// PlayletAllListReq 全部短剧分页请求
|
||||
type PlayletAllListReq struct {
|
||||
pubdef.Page
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 1-最新更新 2-当前热播 3-独家原创 4-随机推荐,默认1
|
||||
}
|
||||
|
||||
// PlayletAllListResp 全部短剧分页响应
|
||||
type PlayletAllListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息
|
||||
}
|
||||
|
||||
// PlayletHotListReq 热门短剧分页请求
|
||||
type PlayletHotListReq struct {
|
||||
pubdef.Page
|
||||
ExcludePlayletId int64 `form:"excludePlayletId" json:"excludePlayletId"` // 排除的短剧ID,默认不排除
|
||||
}
|
||||
|
||||
// PlayletHotListResp 热门短剧分页响应
|
||||
type PlayletHotListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
}
|
||||
|
||||
// CommonData 首页通用数据
|
||||
type CommonData struct {
|
||||
Id int64 `json:"id"` // 短剧id
|
||||
Status int32 `json:"status"` // 状态: 0下架 1上架
|
||||
IsFeatured bool `json:"isFeatured"` // 是否爆剧(true-是,false-否)
|
||||
IsRecommend bool `json:"isRecommend"` // 是否推荐 true-推荐,false-不推荐
|
||||
IsNew bool `json:"isNew"` // 是否上新,本周内上架为true
|
||||
OperateType int32 `json:"operateType"` // 运营类型 1-短剧 2-漫剧
|
||||
ShowOnBanner bool `json:"showOnBanner"` // 是否显示在首页banner
|
||||
Banner string `json:"banner"` // banner图,已拼接图片域名
|
||||
OnlineAt time.Time `json:"onlineAt"` // 上架时间
|
||||
MediaCreatedAt time.Time `json:"mediaCreatedAt"` // 媒资中心创建时间
|
||||
MediaUpdatedAt time.Time `json:"mediaUpdatedAt"` // 媒资中心更新时间
|
||||
ThemeIds []int64 `json:"themeIds"` // 题材ID集合
|
||||
ThemeNames []string `json:"themeNames"` // 题材名称集合
|
||||
ThemeSlugs []string `json:"themeSlugs"` // 题材slug集合
|
||||
CategoryIds []int64 `json:"categoryIds"` // 分类ID集合
|
||||
Title string `json:"title"` // 标题
|
||||
Description string `json:"description"` // SEO Description
|
||||
Slug string `json:"slug"` // slug
|
||||
Desc string `json:"desc"` // 简介
|
||||
Anchor string `json:"anchor"` // 作者/演员
|
||||
Cover string `json:"cover"` // 竖版封面,已拼接图片域名
|
||||
HorizontalCover string `json:"horizontalCover"` // 横版封面,已拼接图片域名
|
||||
Preview string `json:"preview"` // 预告地址,详情接口返回时带播放签名
|
||||
TagIds []int64 `json:"tagIds"` // 标签ID集合
|
||||
TagNames []string `json:"tagNames"` // 标签名称集合
|
||||
TagSlugs []string `json:"tagSlugs"` // 标签slug集合
|
||||
TotalEpisode int64 `json:"totalEpisode"` // 总集数
|
||||
UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
|
||||
CountBrowse int64 `json:"countBrowse"` // 观看量
|
||||
HeatValue int64 `json:"heatValue"` // 热度值
|
||||
CountCollect int64 `json:"countCollect"` // 收藏数
|
||||
CountLike int64 `json:"countLike"` // 点赞数,包含真实点赞数和后台虚拟点赞数
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updatedAt"` // 修改时间
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// PlayletHotRankListReq 热榜分页请求
|
||||
type PlayletHotRankListReq struct {
|
||||
pubdef.Page
|
||||
RankType int64 `form:"rankType" json:"rankType"` // 榜单类型 1-热播榜 2-新剧榜 3-追剧榜
|
||||
}
|
||||
|
||||
// PlayletHotRankListResp 热榜分页响应
|
||||
type PlayletHotRankListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
RankType int64 `json:"rankType"` // 榜单类型 1-热播榜 2-新剧榜 3-追剧榜
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息
|
||||
}
|
||||
|
||||
// PlayletGuessLikeListReq 猜你喜欢分页请求
|
||||
type PlayletGuessLikeListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认6、最大100
|
||||
pubdef.Page
|
||||
PlayletId int64 `form:"playletId" json:"playletId" binding:"required"` // 当前短剧ID
|
||||
}
|
||||
|
||||
// PlayletGuessLikeListResp 猜你喜欢分页响应
|
||||
type PlayletGuessLikeListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 猜你喜欢短剧集合
|
||||
}
|
||||
|
||||
// PlayletTagVideoReq 根据标签ID分页查询短剧请求
|
||||
type PlayletTagVideoReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
TagId int64 `form:"tagId" json:"tagId"` // 标签ID,tagSlug为空时必传
|
||||
TagSlug string `form:"tagSlug" json:"tagSlug"` // 标签slug,非空时优先按slug解析标签ID
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序 1-最新 2-最热 3-收藏最多
|
||||
}
|
||||
|
||||
// PlayletTagVideoResp 根据标签ID分页查询短剧响应
|
||||
type PlayletTagVideoResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(标签SEO)
|
||||
}
|
||||
|
||||
// PlayletSearchReq 短剧ES分页搜索请求
|
||||
type PlayletSearchReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
Keyword string `form:"keyword" json:"keyword"` // 搜索关键词,支持剧名、题材名、简介和标签名
|
||||
}
|
||||
|
||||
// PlayletSearchResp 短剧ES分页搜索响应
|
||||
type PlayletSearchResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
Keyword string `json:"keyword"` // 搜索词
|
||||
List []CommonData `json:"list"` // 搜索结果
|
||||
EveryoneWatching []*CommonData `json:"everyoneWatching"` // 大家都在看,搜索无结果时返回
|
||||
}
|
||||
|
||||
// PlayletSearchSuggestReq 短剧搜索联想请求
|
||||
type PlayletSearchSuggestReq struct {
|
||||
Keyword string `form:"keyword" json:"keyword" binding:"required"` // 搜索关键词
|
||||
}
|
||||
|
||||
// PlayletSearchSuggestResp 短剧搜索联想响应
|
||||
type PlayletSearchSuggestResp struct {
|
||||
Keyword string `json:"keyword"` // 搜索关键词
|
||||
List []CommonData `json:"list"` // 短剧列表
|
||||
}
|
||||
|
||||
// PlayletHotSearchItem 热搜词
|
||||
type PlayletHotSearchItem struct {
|
||||
Keyword string `json:"keyword"` // 搜索词
|
||||
Count int64 `json:"count"` // 搜索次数
|
||||
}
|
||||
|
||||
// PlayletHotLikeResp 热搜词和猜你喜欢
|
||||
type PlayletHotLikeResp struct {
|
||||
HotSearch []PlayletHotSearchItem `json:"hotSearch"` // 前10个热搜词
|
||||
GuessLike []*CommonData `json:"guessLike"` // 按收藏量最多的15部上架短剧
|
||||
}
|
||||
|
||||
// PlayletLikeSetReq 短剧点赞设置请求
|
||||
type PlayletLikeSetReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 短剧ID
|
||||
IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
|
||||
}
|
||||
|
||||
// PlayletLikeSetResp 短剧点赞设置响应
|
||||
type PlayletLikeSetResp struct {
|
||||
Id int64 `json:"id"` // 短剧ID
|
||||
IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
|
||||
CountLike int64 `json:"countLike"` // 短剧点赞数
|
||||
}
|
||||
|
||||
// PlayletLikeListReq 我的点赞短剧分页请求
|
||||
type PlayletLikeListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletLikeData 我的点赞短剧列表项
|
||||
type PlayletLikeData struct {
|
||||
CommonData
|
||||
IsLike int64 `json:"isLike"` // 点赞状态,固定为1
|
||||
LikedAt time.Time `json:"likedAt"` // 点赞时间
|
||||
}
|
||||
|
||||
// PlayletLikeListResp 我的点赞短剧分页响应
|
||||
type PlayletLikeListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []PlayletLikeData `json:"list"` // 点赞短剧集合
|
||||
}
|
||||
|
||||
// PlayletCollectSetReq 短剧收藏设置请求
|
||||
type PlayletCollectSetReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 短剧ID
|
||||
IsCollect *bool `json:"isCollect" binding:"required"` // 是否收藏,true-收藏,false-取消收藏
|
||||
}
|
||||
|
||||
// PlayletCollectSetResp 短剧收藏设置响应
|
||||
type PlayletCollectSetResp struct {
|
||||
Id int64 `json:"id"` // 短剧ID
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
|
||||
CountCollect int64 `json:"countCollect"` // 短剧收藏数
|
||||
}
|
||||
|
||||
// PlayletForwardHeatReq 短剧转发加热度请求
|
||||
type PlayletForwardHeatReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 短剧ID
|
||||
}
|
||||
|
||||
// PlayletForwardHeatResp 短剧转发加热度响应
|
||||
type PlayletForwardHeatResp struct {
|
||||
Id int64 `json:"id"` // 短剧ID
|
||||
HeatValue int64 `json:"heatValue"` // 加热后的热度值
|
||||
}
|
||||
|
||||
// PlayletCollectListReq 我的收藏列表请求
|
||||
type PlayletCollectListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
WatchStatus int64 `form:"watchStatus" json:"watchStatus"` // 观看状态 0-全部 1-追剧中 2-已看完
|
||||
}
|
||||
|
||||
// PlayletCollectData 我的收藏列表项
|
||||
type PlayletCollectData struct {
|
||||
Id int64 `json:"id"` // 短剧ID
|
||||
Title string `json:"title"` // 标题
|
||||
Slug string `json:"slug"` // slug
|
||||
Cover string `json:"cover"` // 封面,已拼接图片域名
|
||||
ThemeIds []int64 `json:"themeIds"` // 题材ID集合
|
||||
ThemeNames []string `json:"themeNames"` // 题材名称集合
|
||||
ThemeSlugs []string `json:"themeSlugs"` // 题材slug集合
|
||||
TagIds []int64 `json:"tagIds"` // 标签ID集合
|
||||
TagNames []string `json:"tagNames"` // 标签名称集合
|
||||
TagSlugs []string `json:"tagSlugs"` // 标签slug集合
|
||||
TotalEpisode int64 `json:"totalEpisode"` // 总集数
|
||||
UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
|
||||
CountBrowse int64 `json:"countBrowse"` // 观看量
|
||||
HeatValue int64 `json:"heatValue"` // 热度值
|
||||
CountCollect int64 `json:"countCollect"` // 收藏数
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态,收藏列表固定返回1-已收藏
|
||||
CollectedAt time.Time `json:"collectedAt"` // 收藏时间
|
||||
WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
|
||||
UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数,按totalEpisode-watchedCount计算
|
||||
LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID,无观看进度时为0
|
||||
LastEpisode int64 `json:"lastEpisode"` // 最近观看集数,无观看进度时为0
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// PlayletCollectListResp 我的收藏列表响应
|
||||
type PlayletCollectListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总收藏数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []PlayletCollectData `json:"list"` // 收藏短剧集合
|
||||
}
|
||||
|
||||
// PlayletBoughtListReq 我的已购短剧列表请求
|
||||
type PlayletBoughtListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletBoughtData 我的已购短剧列表项
|
||||
type PlayletBoughtData struct {
|
||||
CommonData
|
||||
BoughtChapterCount int64 `json:"boughtChapterCount"` // 已购剧集数量
|
||||
CostCoins int64 `json:"costCoins"` // 已消耗金币
|
||||
BoughtAt time.Time `json:"boughtAt"` // 最近购买时间
|
||||
LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID,无观看进度时为0
|
||||
LastEpisode int64 `json:"lastEpisode"` // 最近观看集数,无观看进度时为0
|
||||
WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
|
||||
UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数,按totalEpisode-watchedCount计算
|
||||
IsFinished bool `json:"isFinished"` // 当前用户是否已看完
|
||||
}
|
||||
|
||||
// PlayletBoughtListResp 我的已购短剧列表响应
|
||||
type PlayletBoughtListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总已购短剧数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []PlayletBoughtData `json:"list"` // 已购短剧集合
|
||||
}
|
||||
|
||||
// PlayletHistoryListReq 观看历史列表请求
|
||||
type PlayletHistoryListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认20、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletHistoryData 观看历史列表项
|
||||
type PlayletHistoryData struct {
|
||||
CommonData
|
||||
LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID
|
||||
LastEpisode int64 `json:"lastEpisode"` // 最近观看集数
|
||||
WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
|
||||
UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数
|
||||
IsFinished bool `json:"isFinished"` // 是否已看完
|
||||
HistoryAt string `json:"historyAt"` // 最近观看时间
|
||||
}
|
||||
|
||||
// PlayletHistoryListResp 观看历史列表响应
|
||||
type PlayletHistoryListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总历史数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []PlayletHistoryData `json:"list"` // 观看历史集合
|
||||
}
|
||||
|
||||
// PlayletHistoryDeleteReq 删除单条观看历史请求
|
||||
type PlayletHistoryDeleteReq struct {
|
||||
PlayletId int64 `json:"playletId" binding:"required"` // 短剧ID
|
||||
}
|
||||
|
||||
// PlayletHistoryDeleteResp 删除单条观看历史响应
|
||||
type PlayletHistoryDeleteResp struct {
|
||||
DeletedCount int64 `json:"deletedCount"` // 删除记录数量
|
||||
}
|
||||
|
||||
// PlayletHistoryClearResp 清空观看历史响应
|
||||
type PlayletHistoryClearResp struct {
|
||||
DeletedCount int64 `json:"deletedCount"` // 删除记录数量
|
||||
}
|
||||
|
||||
// PlayletChapterLikeSetReq 短剧剧集点赞设置请求
|
||||
type PlayletChapterLikeSetReq struct {
|
||||
ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
|
||||
IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
|
||||
}
|
||||
|
||||
// PlayletChapterLikeSetResp 短剧剧集点赞设置响应
|
||||
type PlayletChapterLikeSetResp struct {
|
||||
ChapterId int64 `json:"chapterId"` // 短剧剧集ID
|
||||
IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
|
||||
CountLike int64 `json:"countLike"` // 短剧剧集点赞数
|
||||
}
|
||||
|
||||
// PlayletChapterCollectSetReq 短剧剧集收藏设置请求
|
||||
type PlayletChapterCollectSetReq struct {
|
||||
ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
|
||||
IsCollect *bool `json:"isCollect" binding:"required"` // 是否收藏,true-收藏,false-取消收藏
|
||||
}
|
||||
|
||||
// PlayletChapterCollectSetResp 短剧剧集收藏设置响应
|
||||
type PlayletChapterCollectSetResp struct {
|
||||
ChapterId int64 `json:"chapterId"` // 短剧剧集ID
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
|
||||
CountCollect int64 `json:"countCollect"` // 短剧剧集收藏数
|
||||
}
|
||||
|
||||
// PlayletChapterCollectListReq 我的剧集收藏列表请求
|
||||
type PlayletChapterCollectListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletChapterCollectData 我的剧集收藏列表项
|
||||
type PlayletChapterCollectData struct {
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
PlayletTitle string `json:"playletTitle"` // 短剧标题
|
||||
PlayletCover string `json:"playletCover"` // 短剧封面,已拼接图片域名
|
||||
TotalEpisode int64 `json:"totalEpisode"` // 短剧总集数
|
||||
UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
|
||||
ChapterId int64 `json:"chapterId"` // 短剧剧集ID
|
||||
CurrentEpisode int64 `json:"currentEpisode"` // 第几集
|
||||
ChapterTitle string `json:"chapterTitle"` // 剧集标题
|
||||
VideoUrl string `json:"videoUrl"` // 播放地址
|
||||
MediaTime int64 `json:"mediaTime"` // 影片长度(秒)
|
||||
PayType int32 `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
|
||||
SingleCoins int64 `json:"singleCoins"` // 单集金币
|
||||
CountBrowse int64 `json:"countBrowse"` // 剧集观看数
|
||||
CountCollect int64 `json:"countCollect"` // 剧集收藏数
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态,收藏列表固定返回1-已收藏
|
||||
CollectedAt time.Time `json:"collectedAt"` // 收藏时间
|
||||
}
|
||||
|
||||
// PlayletChapterCollectListResp 我的剧集收藏列表响应
|
||||
type PlayletChapterCollectListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总收藏数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []PlayletChapterCollectData `json:"list"` // 收藏剧集集合
|
||||
}
|
||||
|
||||
// PlayletChapterWatchReportReq 短剧剧集观看上报请求
|
||||
type PlayletChapterWatchReportReq struct {
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID,传入时用于校验剧集归属
|
||||
ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
|
||||
}
|
||||
|
||||
// PlayletChapterWatchReportResp 短剧剧集观看上报响应
|
||||
type PlayletChapterWatchReportResp struct {
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
ChapterId int64 `json:"chapterId"` // 短剧剧集ID
|
||||
CurrentEpisode int64 `json:"currentEpisode"` // 当前集数
|
||||
WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
|
||||
UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数
|
||||
LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID
|
||||
LastEpisode int64 `json:"lastEpisode"` // 最近观看集数
|
||||
}
|
||||
|
||||
// PlayletInfoReq 详情请求
|
||||
type PlayletInfoReq struct {
|
||||
Id int64 `form:"id" json:"id"` // 短剧id
|
||||
Slug string `form:"slug" json:"slug"` // 短剧slug,非空时优先按slug解析短剧ID
|
||||
}
|
||||
|
||||
// PlayletChapterInfoReq 短剧剧集详情请求
|
||||
type PlayletChapterInfoReq struct {
|
||||
PlayletId int64 `form:"playletId" json:"playletId"` // 短剧ID,传入时用于校验剧集归属
|
||||
PlayletSlug string `form:"playletSlug" json:"playletSlug"` // 短剧slug,非空时优先按slug解析短剧ID
|
||||
ChapterId int64 `form:"chapterId" json:"chapterId" binding:"required"` // 短剧剧集ID
|
||||
}
|
||||
|
||||
// PlayletChapterListReq 短剧剧集列表请求
|
||||
type PlayletChapterListReq struct {
|
||||
PlayletId int64 `form:"playletId" json:"playletId"` // 短剧ID
|
||||
PlayletSlug string `form:"playletSlug" json:"playletSlug"` // 短剧slug,非空时优先按slug解析短剧ID
|
||||
}
|
||||
|
||||
// PlayletChapterListResp 短剧剧集列表响应
|
||||
type PlayletChapterListResp struct {
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
List []PlayletChapterData `json:"list"` // 短剧剧集集合
|
||||
}
|
||||
|
||||
// PlayletChapterInfoResp 短剧剧集详情响应
|
||||
type PlayletChapterInfoResp struct {
|
||||
Id int64 `json:"id"` // 短剧剧集ID
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
CurrentEpisode int64 `json:"currentEpisode"` // 第几集
|
||||
Title string `json:"title"` // 章节名
|
||||
Md5 string `json:"md5"` // 媒体md5
|
||||
Hash string `json:"hash"` // 媒体hash
|
||||
Height int64 `json:"height"` // 高
|
||||
Width int64 `json:"width"` // 宽
|
||||
Ratio float64 `json:"ratio"` // 宽高比
|
||||
VideoUrl string `json:"videoUrl"` // 视频地址
|
||||
MediaSize int64 `json:"mediaSize"` // 资源大小
|
||||
MediaTime int64 `json:"mediaTime"` // 影片长度(秒)
|
||||
CountComment int64 `json:"countComment"` // 短剧剧集评论数
|
||||
CountBrowse int64 `json:"countBrowse"` // 短剧剧集观看数
|
||||
CountCollect int64 `json:"countCollect"` // 短剧剧集收藏数
|
||||
CountLike int64 `json:"countLike"` // 短剧剧集点赞数
|
||||
IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
|
||||
PayType int32 `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
|
||||
SingleCoins int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
|
||||
CanWatch bool `json:"canWatch"` // 是否可观看
|
||||
NeedUnlock bool `json:"needUnlock"` // 是否需要金币解锁
|
||||
NeedVip bool `json:"needVip"` // 是否需要开通VIP
|
||||
CostCoins int64 `json:"costCoins"` // 本次解锁消耗金币
|
||||
Balance int64 `json:"balance"` // 当前金币余额
|
||||
VipValid bool `json:"vipValid"` // 当前用户VIP是否有效
|
||||
IsUnlocked bool `json:"isUnlocked"` // 当前用户是否已解锁本集
|
||||
// 页面SEO信息
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
|
||||
}
|
||||
|
||||
// PlayletChapterUnlockReq 短剧剧集金币解锁请求
|
||||
type PlayletChapterUnlockReq struct {
|
||||
PlayletId int64 `json:"playletId" binding:"required"` // 短剧ID
|
||||
ChapterId int64 `json:"chapterId" binding:"required"` // 短剧剧集ID
|
||||
}
|
||||
|
||||
// PlayletChapterUnlockResp 短剧剧集金币解锁响应
|
||||
type PlayletChapterUnlockResp struct {
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
ChapterId int64 `json:"chapterId"` // 短剧剧集ID
|
||||
Unlocked bool `json:"unlocked"` // 是否已解锁
|
||||
CanWatch bool `json:"canWatch"` // 是否可观看
|
||||
Balance int64 `json:"balance"` // 当前金币余额
|
||||
CostCoins int64 `json:"costCoins"` // 本次解锁消耗金币
|
||||
ChangeCoin int64 `json:"changeCoin"` // 本次实际扣减金币,幂等命中时为0
|
||||
}
|
||||
|
||||
// PlayletInfoResp 详情响应
|
||||
type PlayletInfoResp struct {
|
||||
CommonData
|
||||
// 是否点赞(1-已点赞,2-未点赞)
|
||||
IsLike int64 `json:"isLike"`
|
||||
// 是否收藏(1-已收藏,2-未收藏)
|
||||
IsCollect int64 `json:"isCollect"`
|
||||
// 短剧章节集合
|
||||
PlayletChapterInfo []PlayletChapterData `json:"playletChapterInfo"`
|
||||
// 页面SEO信息
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
}
|
||||
|
||||
// PlayletChapterData 短剧章节信息
|
||||
type PlayletChapterData struct {
|
||||
Id int64 `json:"id"` // 短剧剧集ID
|
||||
PlayletId int64 `json:"playletId"` // 短剧ID
|
||||
CurrentEpisode int64 `json:"currentEpisode"` // 第几集
|
||||
Title string `json:"title"` // 章节名
|
||||
Md5 string `json:"md5"` // 媒体md5
|
||||
Hash string `json:"hash"` // 媒体hash
|
||||
Height int64 `json:"height"` // 高
|
||||
Width int64 `json:"width"` // 宽
|
||||
Ratio float64 `json:"ratio"` // 宽高比
|
||||
VideoUrl string `json:"videoUrl"` // 视频地址
|
||||
MediaSize int64 `json:"mediaSize"` // 资源大小
|
||||
MediaTime int64 `json:"mediaTime"` // 影片长度(秒)
|
||||
PayType int32 `json:"payType"` // 支付方式 0-免费 1-金币 2-VIP
|
||||
SingleCoins int64 `json:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
|
||||
CanWatch bool `json:"canWatch"` // 是否可观看
|
||||
NeedUnlock bool `json:"needUnlock"` // 是否需要金币解锁
|
||||
NeedVip bool `json:"needVip"` // 是否需要开通VIP
|
||||
CostCoins int64 `json:"costCoins"` // 本次解锁消耗金币
|
||||
Balance int64 `json:"balance"` // 当前金币余额
|
||||
VipValid bool `json:"vipValid"` // 当前用户VIP是否有效
|
||||
IsUnlocked bool `json:"isUnlocked"` // 当前用户是否已解锁本集
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
// PlayletTopicPageListReq 专题剧单分页请求
|
||||
type PlayletTopicPageListReq struct {
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletTopicPageListResp 专题剧单分页响应
|
||||
type PlayletTopicPageListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []*PlayletTopicPageItem `json:"list"` // 专题集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息
|
||||
}
|
||||
|
||||
// PlayletTopicPageItem 专题剧单分页项
|
||||
type PlayletTopicPageItem struct {
|
||||
Id int64 `json:"id"` // 专题ID
|
||||
Name string `json:"name"` // 专题名称
|
||||
Slug string `json:"slug"` // slug
|
||||
Desc string `json:"desc"` // 描述
|
||||
Icon string `json:"icon"` // 图标
|
||||
Rank int64 `json:"rank"` // 排序,越大越靠前
|
||||
PlayletCount int64 `json:"playletCount"` // 上架短剧数量
|
||||
Covers []string `json:"covers"` // 展示封面集合
|
||||
List []*CommonData `json:"list"` // 专题下短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo"` // 专题SEO
|
||||
}
|
||||
|
||||
// PlayletTopicDetailReq 专题详情请求
|
||||
type PlayletTopicDetailReq struct {
|
||||
pubdef.Page
|
||||
Id int64 `form:"id" json:"id" binding:"required"` // 专题ID
|
||||
}
|
||||
|
||||
// PlayletTopicDetailResp 专题详情响应
|
||||
type PlayletTopicDetailResp struct {
|
||||
Id int64 `json:"id"` // 专题ID
|
||||
Name string `json:"name"` // 专题名称
|
||||
Slug string `json:"slug"` // slug
|
||||
Desc string `json:"desc"` // 描述
|
||||
Icon string `json:"icon"` // 图标
|
||||
Rank int64 `json:"rank"` // 排序,越大越靠前
|
||||
PlayletCount int64 `json:"playletCount"` // 上架短剧数量
|
||||
Covers []string `json:"covers"` // 顶部封面集合
|
||||
TotalCount int64 `json:"totalCount"` // 短剧总数
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 专题短剧列表
|
||||
MoreTopics []*PlayletTopicPageItem `json:"moreTopics"` // 更多专题
|
||||
SeoInfo *SeoInfo `json:"seoInfo"` // 专题SEO
|
||||
}
|
||||
|
||||
// PlayletThemeListResponse app端题材列表响应
|
||||
type PlayletThemeListResponse struct {
|
||||
List []*PlayletThemeItem `json:"list"` // 题材列表
|
||||
}
|
||||
|
||||
// PlayletThemeItem app端题材列表项
|
||||
type PlayletThemeItem struct {
|
||||
Id int64 `json:"id"` // 题材ID
|
||||
Name string `json:"name"` // 题材名称
|
||||
Slug string `json:"slug"` // slug
|
||||
Icon string `json:"icon"` // 题材图标,已拼接图片域名
|
||||
Rank int64 `json:"rank"` // 排序,越大越靠前
|
||||
Status bool `json:"status"` // 上架状态
|
||||
IsRecommend bool `json:"isRecommend"` // 是否推荐
|
||||
PlayletCount int64 `json:"playletCount"` // 该题材绑定的上架短剧数量
|
||||
SeoInfo *SeoInfo `json:"seoInfo"` // 题材对应页面 SEO 信息
|
||||
}
|
||||
|
||||
// PlayletTagListResponse app端短剧标签列表响应
|
||||
type PlayletTagListResponse struct {
|
||||
List []*PlayletTagItem `json:"list"` // 标签列表
|
||||
}
|
||||
|
||||
// PlayletTagItem app端短剧标签列表项
|
||||
type PlayletTagItem struct {
|
||||
Id int64 `json:"id"` // 标签ID
|
||||
Name string `json:"name"` // 标签名称
|
||||
Slug string `json:"slug"` // slug
|
||||
Rank int64 `json:"rank"` // 排序,越大越靠前
|
||||
Status bool `json:"status"` // 上架状态
|
||||
SeoInfo *SeoInfo `json:"seoInfo"` // 标签对应页面 SEO 信息
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/models/rechargepackagemod"
|
||||
|
||||
// PayMethodInfo 单个支付方式
|
||||
type PayMethodInfo struct {
|
||||
Type string `json:"type"` // alipay / wechat / union
|
||||
TypeName string `json:"typeName"` // 支付宝 / 微信 / 银联
|
||||
}
|
||||
|
||||
// RechargePackageItem App购买套餐列表项
|
||||
type RechargePackageItem struct {
|
||||
Id int64 `json:"id"` // 套餐ID
|
||||
PackageType rechargepackagemod.PackageType `json:"packageType"` // 套餐类型:1-会员畅享 2-金币充值
|
||||
Name string `json:"name"` // 展示标题
|
||||
SubTitle string `json:"subTitle"` // 展示副标题
|
||||
Price int64 `json:"price"` // 售价,单位分
|
||||
OriginalPrice int64 `json:"originalPrice"` // 原价,单位分
|
||||
DurationDays int64 `json:"durationDays"` // 会员有效天数
|
||||
DurationHours int64 `json:"durationHours"` // 会员有效小时数,由durationDays计算
|
||||
BaseCoin int64 `json:"baseCoin"` // 基础金币数
|
||||
GiftCoin int64 `json:"giftCoin"` // 赠送金币数
|
||||
TotalCoin int64 `json:"totalCoin"` // 到账总金币数,由baseCoin+giftCoin计算
|
||||
Sort int32 `json:"sort"` // 排序,越小越靠前
|
||||
PayMethods []PayMethodInfo `json:"payMethods"` // 该套餐支持的支付方式
|
||||
}
|
||||
|
||||
// RechargePackageListResp App购买套餐列表响应。
|
||||
type RechargePackageListResp struct {
|
||||
VipList []*RechargePackageItem `json:"vipList"` // 会员畅享列表
|
||||
CoinList []*RechargePackageItem `json:"coinList"` // 金币充值列表
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package proto
|
||||
|
||||
import "videoweb/pub/pubdef"
|
||||
|
||||
type RichTextInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"`
|
||||
Title string `json:"title" bson:"title"` // 标题
|
||||
Content string `json:"content" bson:"content"` // 内容
|
||||
Desc string `json:"desc" bson:"desc"` // 标题(app)
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// AppRichTextItemResp APP富文本内容列表项
|
||||
type AppRichTextItemResp struct {
|
||||
// 富文本ID
|
||||
Id int64 `json:"id"`
|
||||
// 标题
|
||||
Title string `json:"title"`
|
||||
// 内容
|
||||
Content string `json:"content"`
|
||||
// 标题(app)
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// AppRichTextListResp APP富文本内容列表响应
|
||||
type AppRichTextListResp struct {
|
||||
// 富文本内容列表
|
||||
List []AppRichTextItemResp `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package proto
|
||||
|
||||
import "time"
|
||||
|
||||
type SearchSortType string
|
||||
|
||||
const (
|
||||
SearchSortTypeDefault SearchSortType = "default" // 默认 相似度
|
||||
SearchSortTypeHot SearchSortType = "hot" // 热度排序
|
||||
SearchSortTypeNew SearchSortType = "new" // 最新排序
|
||||
)
|
||||
|
||||
// RankType 排行榜类型
|
||||
type RankType int64
|
||||
|
||||
const (
|
||||
RankType_Video = 1 // 视频
|
||||
RankType_Actress = 2 // 女优
|
||||
RankType_Filmreview = 3 // 影评
|
||||
)
|
||||
|
||||
// RankPeriod 排行榜周期
|
||||
type RankPeriod int64
|
||||
|
||||
const (
|
||||
RankPeriod_Week RankPeriod = 1
|
||||
RankPeriod_Month RankPeriod = 2
|
||||
RankPeriod_Year RankPeriod = 3
|
||||
)
|
||||
|
||||
type SearchRankHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
VideoWeek SearchRankData `json:"videoWeek"`
|
||||
ActressWeek SearchRankData `json:"actressWeek"`
|
||||
FilmreviewWeek SearchRankData `json:"filmreviewWeek"`
|
||||
VideoMonth SearchRankData `json:"videoMonth"`
|
||||
ActressMonth SearchRankData `json:"actressMonth"`
|
||||
FilmreviewMonth SearchRankData `json:"filmreviewMonth"`
|
||||
VideoYear SearchRankData `json:"videoYear"`
|
||||
ActressYear SearchRankData `json:"actressYear"`
|
||||
FilmreviewYear SearchRankData `json:"filmreviewYear"`
|
||||
}
|
||||
type SearchRankData struct {
|
||||
RankName string `json:"rankName" form:"rankName"` // 排行榜名字
|
||||
RankType RankType `json:"rankType" form:"rankType"` // 排行榜类型 1视频, 2女优 ,3影评
|
||||
RankPeriod RankPeriod `json:"rankPeriod" form:"rankPeriod"` // 排行榜周期 1=周 2=月 3=年
|
||||
List []HotSearchItem `json:"list" form:"list"`
|
||||
}
|
||||
|
||||
type HotSearchItem struct {
|
||||
Id int64 `json:"id" form:"id"` // ID
|
||||
Title string `json:"title" form:"title"` //标题
|
||||
HotValue int64 `json:"hotValue" form:"hotValue"` // 热度值
|
||||
Images string `json:"images" form:"images"` // 封面
|
||||
Works int64 `json:"works" bson:"works,omitempty"` // 作品数量
|
||||
Desc string `json:"desc" bson:"desc"` // 描述
|
||||
AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 上架shi时间
|
||||
}
|
||||
|
||||
type SearchResp struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
InTitle string `json:"inTitle"` //输入标题
|
||||
InType int64 `json:"inType"` // 输入类型 0=视频 1=女优 2=影评 3=帖子
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Sort []SearchSortItem `json:"sorts"` // 排序选项
|
||||
SearchRecommend []string `json:"searchRecommend"` // 搜索推荐
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
}
|
||||
|
||||
type SearchSortItem struct {
|
||||
Key string `json:"key"` // 值
|
||||
Title string `json:"title"` // 标题
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package proto
|
||||
|
||||
// SeoInfo 页面SEO信息,用于前端渲染 title、meta 和 canonical。
|
||||
type SeoInfo struct {
|
||||
// 页面标题
|
||||
Title string `json:"title"`
|
||||
// 页面关键词
|
||||
Keywords []string `json:"keywords"`
|
||||
// 页面描述
|
||||
Description string `json:"description"`
|
||||
// 网站名称
|
||||
WebSiteName string `json:"webSiteName"`
|
||||
// 当前内容的标准URL
|
||||
CanonicalURL string `json:"canonicalUrl"`
|
||||
}
|
||||
@@ -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"` // 导航类型
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
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:"-"` // 导航分类
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package proto
|
||||
|
||||
// UserCenterDataResp app个人中心数据响应
|
||||
type UserCenterDataResp struct {
|
||||
WatchCount int64 `json:"watchCount"` // 观看数量
|
||||
CollectCount int64 `json:"collectCount"` // 收藏数量
|
||||
ChasingCount int64 `json:"chasingCount"` // 追剧中数量
|
||||
BoughtChapterCount int64 `json:"boughtChapterCount"` // 已购剧集数量
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package proto
|
||||
|
||||
// HomeHtmlData 首页
|
||||
type HomeHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"` // 请求路径
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
}
|
||||
|
||||
type PostDetailsHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
|
||||
CreateInfo PostCreateInfo `json:"createInfo" form:"createInfo"` // 作者信息
|
||||
}
|
||||
|
||||
type PostCreateInfo struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Image string `json:"image" form:"image"` //头像
|
||||
Works int64 `json:"works" form:"works"`
|
||||
CreateType int64 `json:"createType"` // 创作者类型 0=普通 1=官方
|
||||
Desc string `json:"desc" form:"desc"` // 作者描述
|
||||
Background string `json:"background"` // 背景
|
||||
}
|
||||
|
||||
// TagHtmlData 首页
|
||||
type TagHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
|
||||
type RichTextHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
type NotFoundHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
|
||||
type UrlRedirectData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
UrlRedirect string `json:"urlRedirect" form:"urlRedirect"`
|
||||
}
|
||||
|
||||
type ContactUsReq struct {
|
||||
Name string `json:"name" form:"name"` // 姓名
|
||||
Email string `json:"email" form:"email"` // 邮件
|
||||
Content string `json:"content" form:"content"` // 内容
|
||||
CategoryName string `json:"categoryName" form:"categoryName"` // 主旨分类
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package rechargeapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/rechargeser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type submitOrderReq struct {
|
||||
PackageId int64 `json:"packageId" binding:"required"` // 购买套餐ID(会员畅享)
|
||||
PayMode string `json:"payMode" binding:"required"` // 支付方式:alipay/wechat
|
||||
}
|
||||
|
||||
// SubmitOrder 发起第三方支付下单
|
||||
// @Tags 充值
|
||||
// @Summary 创建 VIP 充值订单(第三方支付)
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body submitOrderReq true "下单参数"
|
||||
// @Success 200 {object} rechargeser.SubmitOrderResp "支付链接"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/vip/pay [POST]
|
||||
func SubmitOrder(ctx *gin.Context) {
|
||||
var req submitOrderReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
sysType := ctx.GetString(consts.CtxSysType)
|
||||
devType := ctx.GetString(consts.CtxDevType)
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
|
||||
user, serr := rechargeser.GetUserInfo(ctx, userId)
|
||||
if serr != nil || user == nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrUserNotExsit, nil)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := rechargeser.PayDeviceID(ctx.GetString(consts.CtxDevId), user.DevID, user.DevV2)
|
||||
resp, serr := rechargeser.CreateOrder(ctx, userId, user.NickName, req.PackageId, req.PayMode, sysType, devType, deviceID, ip, user.DistrictCode)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
type orderHistoryReq struct {
|
||||
Page int64 `json:"page" form:"page"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
}
|
||||
|
||||
// GetOrderHistory 查询当前用户 VIP 订单历史
|
||||
// @Tags 充值
|
||||
// @Summary 查询 VIP 充值记录
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码(默认1)"
|
||||
// @Param pageSize query int false "每页数量(默认10)"
|
||||
// @Success 200 {array} vipordermod.VipOrder "订单列表"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/vip/orders [GET]
|
||||
func GetOrderHistory(ctx *gin.Context) {
|
||||
var req orderHistoryReq
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
list, total, serr := rechargeser.GetOrderHistory(ctx, userId, req.Page, req.PageSize)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, gin.H{"total": total, "list": list})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package rechargepackageapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/rechargepackageser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List 查询购买套餐列表
|
||||
// @Tags 金币与会员
|
||||
// @Summary 查询购买套餐列表 -- by 王鸣
|
||||
// @Description 返回会员畅享和金币充值两个列表,并附带各套餐可用支付方式
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.RechargePackageListResp "成功"
|
||||
// @Router /api/app/recharge/package/list [GET]
|
||||
func List(ctx *gin.Context) {
|
||||
resp, err := rechargepackageser.GetRechargePackageList(ctx)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package statcenterapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/statcenter"
|
||||
"videoweb/pub/models/cointransmod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/uservisitlog"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// @Tags 数据同步
|
||||
// @Summary 统计中心增量同步
|
||||
// @Description 按 job 拉取注册/日活/充值/绑定/消费/售卡等增量数据
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body statcenter.StatcenterSyncReq true "参数"
|
||||
// @Success 200 {object} statcenter.StatcenterSyncResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/statcenter/sync [POST]
|
||||
func StatcenterSyncList(ctx *gin.Context) {
|
||||
var request statcenter.StatcenterSyncReq
|
||||
if err := ctx.ShouldBind(&request); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var resp statcenter.StatcenterSyncResp
|
||||
resp.Job = request.Job
|
||||
resp.Code = 200
|
||||
appId := statcenter.STAT_APPID
|
||||
|
||||
var err error
|
||||
switch request.Job {
|
||||
case string(statcenter.USER_ACCE):
|
||||
resp.AccessList, err = UserAccessList(appId, request, true)
|
||||
case string(statcenter.USER_REG):
|
||||
resp.RegisterList, err = UserRegisterList(appId, request, true)
|
||||
case string(statcenter.USER_BINDING):
|
||||
resp.BindingList, err = UserBindingList(appId, request)
|
||||
case string(statcenter.USER_INVITE):
|
||||
resp.InviteList, err = UserInviteList(appId, request)
|
||||
case string(statcenter.ConsumeRecordJob):
|
||||
resp.ConsumeList, err = ConsumeRecordList(appId, request)
|
||||
case string(statcenter.USER_RECH):
|
||||
resp.RechargeList, err = UserRechargeList(appId, request, true)
|
||||
case string(statcenter.USER_RECH_ALL):
|
||||
resp.AllRechargeList, err = UserAllRechargeList(appId, request, true)
|
||||
case string(statcenter.CardSellJob):
|
||||
resp.CardSellList, err = CardSellList(appId, request)
|
||||
case string(statcenter.AI_SELL_JOB):
|
||||
resp.AiSellList, err = AiSellList(appId, request)
|
||||
case string(statcenter.VipJob):
|
||||
resp.VipList, err = UserSyncVipList(appId, request)
|
||||
case string(statcenter.RechargeAll): // 兼容旧任务名
|
||||
resp.RechargeList, err = UserAllRechargeList(appId, request, true)
|
||||
default:
|
||||
resp.Code = 501
|
||||
resp.Msg = "job is error"
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.Code = 502
|
||||
resp.Msg = err.Error()
|
||||
}
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// UserAccessList 日活数据同步
|
||||
func UserAccessList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (accessList []statcenter.UserVisitMsg, err error) {
|
||||
platformId := request.PlatformId
|
||||
if platformId == "" {
|
||||
platformId = "000000000000000000000000"
|
||||
}
|
||||
visitList, err := uservisitlog.AccessSyncById(context.Background(), platformId, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("UserAccessList AccessSyncById", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
accessList = make([]statcenter.UserVisitMsg, 0, len(visitList))
|
||||
userIds := make([]int64, 0, len(visitList))
|
||||
for _, visitInfo := range visitList {
|
||||
userIds = append(userIds, visitInfo.UserId)
|
||||
}
|
||||
userList, _ := usermod.FindManyUser(context.Background(), bson.M{usermod.FN.User.Id: bson.M{"$in": userIds}})
|
||||
userMap := make(map[int64]*usermod.User, len(userList))
|
||||
for _, u := range userList {
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
for _, visitInfo := range visitList {
|
||||
sType, dType := visitInfo.SysType, visitInfo.DevType
|
||||
if appId == statcenter.STAT_APPID && isNewStat {
|
||||
sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(visitInfo.SysType), visitInfo.DevType)
|
||||
}
|
||||
accessInfo := statcenter.UserVisitMsg{
|
||||
UserId: uint64(visitInfo.UserId),
|
||||
AppId: appId,
|
||||
SysType: sType,
|
||||
DevType: dType,
|
||||
IP: visitInfo.IP,
|
||||
Version: visitInfo.Version,
|
||||
DevID: visitInfo.DevID,
|
||||
VisitAt: visitInfo.VisitAt,
|
||||
PlatformId: visitInfo.Id.Hex(),
|
||||
}
|
||||
if userInfo, ok := userMap[visitInfo.UserId]; ok {
|
||||
accessInfo.IsDirect = userInfo.IsDirect
|
||||
accessInfo.DistrictCode = userInfo.DistrictCode
|
||||
accessInfo.RegisterTime = userInfo.CreatedAt
|
||||
}
|
||||
accessList = append(accessList, accessInfo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UserRegisterList 注册数据同步
|
||||
func UserRegisterList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (registerList []statcenter.UserRegisterMsg, err error) {
|
||||
var userList usermod.UserList
|
||||
if appId == statcenter.STAT_APPID && isNewStat {
|
||||
if request.UserId > math.MaxInt64 {
|
||||
return nil, fmt.Errorf("invalid register cursor userId: %d", request.UserId)
|
||||
}
|
||||
userList, err = usermod.StatcenterSyncListByUserID(context.Background(), int64(request.UserId), request.MaxSize)
|
||||
} else {
|
||||
userList, err = usermod.StatcenterSyncList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("UserRegisterList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
registerList = make([]statcenter.UserRegisterMsg, 0, len(userList))
|
||||
for _, userInfo := range userList {
|
||||
if userInfo.Device == "robot" {
|
||||
continue
|
||||
}
|
||||
isNativeIOS := pubdef.SysTypeFromString(userInfo.SysType) == pubdef.ST_IOS
|
||||
districtCode := userInfo.DistrictCode
|
||||
if isNativeIOS && statcenter.IOSRegistrationDistrictCode != "" {
|
||||
districtCode = statcenter.IOSRegistrationDistrictCode
|
||||
}
|
||||
|
||||
sType, dType := userInfo.SysType, userInfo.Device
|
||||
if appId == statcenter.STAT_APPID && isNewStat {
|
||||
sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(userInfo.SysType), userInfo.Device)
|
||||
}
|
||||
registerList = append(registerList, statcenter.UserRegisterMsg{
|
||||
UserId: uint64(userInfo.Id),
|
||||
AppId: appId,
|
||||
SysType: sType,
|
||||
DevType: dType,
|
||||
Mobile: userInfo.Email,
|
||||
Name: userInfo.NickName,
|
||||
IP: userInfo.RegIp,
|
||||
IsDirect: userInfo.IsDirect,
|
||||
DistrictCode: districtCode,
|
||||
PromSeqe: userInfo.AgentInviteCode,
|
||||
PromCode: userInfo.InviteCode,
|
||||
RegisterTime: userInfo.CreatedAt,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UserBindingList 绑定数据同步(本项目:正式注册并绑定邮箱)
|
||||
func UserBindingList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.UserBindingMsg, error) {
|
||||
userList, err := usermod.StatcenterSyncBindUserList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("UserBindingList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
list := make([]statcenter.UserBindingMsg, 0, len(userList))
|
||||
for _, userInfo := range userList {
|
||||
sType, dType := pubdef.GetCenterType(pubdef.SysTypeFromString(userInfo.SysType), userInfo.Device)
|
||||
bindAt := userInfo.UpdatedAt
|
||||
list = append(list, statcenter.UserBindingMsg{
|
||||
UserId: uint64(userInfo.Id),
|
||||
AppId: appId,
|
||||
PlatformId: fmt.Sprintf("%d", userInfo.Id),
|
||||
SysType: sType,
|
||||
DevType: dType,
|
||||
Mobile: userInfo.Email,
|
||||
BindingTime: &bindAt,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// UserInviteList 邀请绑定同步
|
||||
func UserInviteList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.UserInviteBindMsg, error) {
|
||||
userList, err := usermod.StatcenterSyncInviteList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("UserInviteList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
list := make([]statcenter.UserInviteBindMsg, 0, len(userList))
|
||||
for _, userInfo := range userList {
|
||||
list = append(list, statcenter.UserInviteBindMsg{
|
||||
UserId: uint64(userInfo.Id),
|
||||
AppId: appId,
|
||||
ParentPromCode: userInfo.AgentInviteCode,
|
||||
InviteTime: userInfo.CreatedAt,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// ConsumeRecordList 金币消费流水同步
|
||||
func ConsumeRecordList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.ConsumeRecordMsg, error) {
|
||||
startId := int64(0)
|
||||
if request.PlatformId != "" {
|
||||
id, err := strconv.ParseInt(request.PlatformId, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid consume cursor platformId: %s", request.PlatformId)
|
||||
}
|
||||
startId = id
|
||||
}
|
||||
transList, err := cointransmod.StatSyncConsumeList(context.Background(), startId, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("ConsumeRecordList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
list := make([]statcenter.ConsumeRecordMsg, 0, len(transList))
|
||||
for _, item := range transList {
|
||||
consumeType := statcenter.StatValueAddSer
|
||||
if item.TranType == cointransmod.TranTypeVideoUnlock {
|
||||
consumeType = statcenter.StatVideoUnlock
|
||||
}
|
||||
list = append(list, statcenter.ConsumeRecordMsg{
|
||||
AppID: appId,
|
||||
UID: uint64(item.UserId),
|
||||
Type: consumeType,
|
||||
CurrencyType: statcenter.CurrencyTypeGold,
|
||||
Money: decimal.Zero,
|
||||
Amount: decimal.NewFromInt(item.Amount),
|
||||
Uniq: strconv.FormatInt(item.Id, 10),
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// UserRechargeList 成功充值订单同步(会员+金币)
|
||||
func UserRechargeList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (registerList []statcenter.UserRechargeMsg, err error) {
|
||||
orderList, err := vipordermod.StatSyncSuccessOrderList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("UserRechargeList StatSyncSuccessOrderList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
userMap := findUserDistrictMap(orderList)
|
||||
registerList = make([]statcenter.UserRechargeMsg, 0, len(orderList))
|
||||
for _, order := range orderList {
|
||||
sType, dType := order.SysType, ""
|
||||
if appId == statcenter.STAT_APPID && isNewStat {
|
||||
sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(order.SysType), "")
|
||||
}
|
||||
msg := statcenter.UserRechargeMsg{
|
||||
UserId: uint64(order.UserId),
|
||||
AppId: appId,
|
||||
SysType: sType,
|
||||
DevType: dType,
|
||||
PlatformId: fmt.Sprintf("%d", order.Id),
|
||||
ChannelName: order.PayChannel,
|
||||
CID: order.Oid,
|
||||
Type: order.PayMode,
|
||||
OrderId: order.TradeNo,
|
||||
OID: order.Oid,
|
||||
Money: order.Amount,
|
||||
PayMoney: order.Amount,
|
||||
Status: statcenter.MapOrderStatusToStat(order.Status),
|
||||
Rate: order.PayRate,
|
||||
SuccessAt: order.FinishedAt,
|
||||
CreatedAt: order.CreatedAt,
|
||||
}
|
||||
fillRechargeDistrict(&msg, userMap)
|
||||
if order.OrderType == vipordermod.OrderTypeCoin {
|
||||
msg.ChanShareMod = 1
|
||||
} else {
|
||||
msg.ChanShareMod = 2
|
||||
}
|
||||
registerList = append(registerList, msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UserAllRechargeList 全部非失败充值订单同步
|
||||
func UserAllRechargeList(appId int32, request statcenter.StatcenterSyncReq, isNewStat bool) (registerList []statcenter.UserRechargeMsg, err error) {
|
||||
orderList, err := vipordermod.StatSyncAllOrderList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("UserAllRechargeList StatSyncAllOrderList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
userMap := findUserDistrictMap(orderList)
|
||||
registerList = make([]statcenter.UserRechargeMsg, 0, len(orderList))
|
||||
for _, order := range orderList {
|
||||
sType, dType := order.SysType, ""
|
||||
if appId == statcenter.STAT_APPID && isNewStat {
|
||||
sType, dType = pubdef.GetCenterType(pubdef.SysTypeFromString(order.SysType), "")
|
||||
}
|
||||
msg := statcenter.UserRechargeMsg{
|
||||
UserId: uint64(order.UserId),
|
||||
AppId: appId,
|
||||
SysType: sType,
|
||||
DevType: dType,
|
||||
PlatformId: fmt.Sprintf("%d", order.Id),
|
||||
ChannelName: order.PayChannel,
|
||||
CID: order.Oid,
|
||||
Type: order.PayMode,
|
||||
OrderId: order.TradeNo,
|
||||
OID: order.Oid,
|
||||
Money: order.Amount,
|
||||
PayMoney: order.Amount,
|
||||
Status: statcenter.MapOrderStatusToStat(order.Status),
|
||||
Rate: order.PayRate,
|
||||
CreatedAt: order.CreatedAt,
|
||||
SuccessAt: order.CreatedAt,
|
||||
}
|
||||
fillRechargeDistrict(&msg, userMap)
|
||||
registerList = append(registerList, msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CardSellList 会员卡销售流水同步(成功会员订单)
|
||||
func CardSellList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.CardSellMsg, error) {
|
||||
orderList, err := vipordermod.StatSyncVipOrderList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("CardSellList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
list := make([]statcenter.CardSellMsg, 0, len(orderList))
|
||||
for _, order := range orderList {
|
||||
sType, _ := pubdef.GetCenterType(pubdef.SysTypeFromString(order.SysType), "")
|
||||
list = append(list, statcenter.CardSellMsg{
|
||||
AppID: appId,
|
||||
UID: uint64(order.UserId),
|
||||
UniqID: fmt.Sprintf("%d", order.Id),
|
||||
Amount: order.Amount,
|
||||
TranTypeInt: int64(order.OrderType),
|
||||
TranType: "会员卡",
|
||||
SysType: sType,
|
||||
CurrencyType: statcenter.CurrencyTypeCash,
|
||||
TranCreatedAt: order.FinishedAt,
|
||||
Product: statcenter.Product{
|
||||
ID: fmt.Sprintf("%d", order.CardId),
|
||||
Name: order.CardName,
|
||||
DiscountedPrice: order.Amount,
|
||||
},
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// AiSellList AI 销售流水(本项目无 AI 业务,返回空列表)
|
||||
func AiSellList(appId int32, request statcenter.StatcenterSyncReq) ([]statcenter.AiSellMsg, error) {
|
||||
return make([]statcenter.AiSellMsg, 0), nil
|
||||
}
|
||||
|
||||
// UserSyncVipList VIP 购买成功订单同步
|
||||
func UserSyncVipList(appId int32, request statcenter.StatcenterSyncReq) (registerList []statcenter.UserBuyVIPMsg, err error) {
|
||||
orderList, err := vipordermod.StatSyncVipOrderList(context.Background(), request.SuccessTime, request.MaxSize)
|
||||
if err != nil {
|
||||
log.Error("UserSyncVipList StatSyncVipOrderList", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
userMap := findUserDistrictMap(orderList)
|
||||
registerList = make([]statcenter.UserBuyVIPMsg, 0, len(orderList))
|
||||
for _, order := range orderList {
|
||||
msg := statcenter.UserBuyVIPMsg{
|
||||
UID: uint64(order.UserId),
|
||||
AppId: appId,
|
||||
PlatformId: fmt.Sprintf("%d", order.Id),
|
||||
Name: order.CardName,
|
||||
Money: order.Amount,
|
||||
BuyVIPType: statcenter.BuyVIPTypeMoney,
|
||||
OID: order.Oid,
|
||||
DeviceType: order.SysType,
|
||||
Status: 3,
|
||||
SuccessAt: order.FinishedAt,
|
||||
}
|
||||
if userInfo, ok := userMap[order.UserId]; ok {
|
||||
msg.IsDirect = userInfo.IsDirect
|
||||
msg.DistrictCode = userInfo.DistrictCode
|
||||
}
|
||||
registerList = append(registerList, msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func findUserDistrictMap(orderList vipordermod.VipOrderList) map[int64]*usermod.User {
|
||||
userIds := make([]int64, 0, len(orderList))
|
||||
seen := make(map[int64]struct{}, len(orderList))
|
||||
for _, order := range orderList {
|
||||
if order == nil || order.UserId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[order.UserId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[order.UserId] = struct{}{}
|
||||
userIds = append(userIds, order.UserId)
|
||||
}
|
||||
userMap := make(map[int64]*usermod.User, len(userIds))
|
||||
if len(userIds) == 0 {
|
||||
return userMap
|
||||
}
|
||||
userList, _ := usermod.FindManyUser(context.Background(), bson.M{usermod.FN.User.Id: bson.M{"$in": userIds}})
|
||||
for _, u := range userList {
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
return userMap
|
||||
}
|
||||
|
||||
func fillRechargeDistrict(msg *statcenter.UserRechargeMsg, userMap map[int64]*usermod.User) {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
userInfo, ok := userMap[int64(msg.UserId)]
|
||||
if !ok || userInfo == nil {
|
||||
return
|
||||
}
|
||||
msg.DistrictCode = userInfo.DistrictCode
|
||||
msg.RegisterTime = userInfo.CreatedAt
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package translationapi
|
||||
|
||||
import (
|
||||
"videoweb/app/service/translationser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CategoryList app端导航分类列表
|
||||
// @Tags APP导航相关接口
|
||||
// @Summary app端导航分类列表 --by 王鸣
|
||||
// @Description 无请求参数,不分页,返回启用的顶部导航分类
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.AppTranslationInfo "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/translation/category/list [get]
|
||||
func CategoryList(ctx *gin.Context) {
|
||||
common.ServerJSON(ctx, srvdef.Success, translationser.GetTranslationData())
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package uploadapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"videoweb/app/service/uploadser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UploadImage 上传图片
|
||||
// @Tags 上传
|
||||
// @Summary 图片上传
|
||||
// @Description 将图片转发至存储服务 /media/web/autopublish/image
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "图片文件"
|
||||
// @Param watermark formData string false "是否加水印:1加,其他情况不加(头像不加)"
|
||||
// @Success 200 {object} uploadser.ImageUploadResp "成功"
|
||||
// @Failure 400 {string} string "参数错误"
|
||||
// @Router /api/app/upload/image [post]
|
||||
func UploadImage(ctx *gin.Context) {
|
||||
f, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, "file 不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
watermark := int64(0)
|
||||
if ctx.PostForm("watermark") == "1" {
|
||||
watermark = 1
|
||||
}
|
||||
|
||||
param := &uploadser.ImageParam{
|
||||
FileHeader: f,
|
||||
Watermark: watermark,
|
||||
}
|
||||
respBytes, err := uploadser.ForwardImage(param)
|
||||
if err != nil {
|
||||
log.Error("UploadImage forward failed", log.E(err))
|
||||
common.ServerJSON(ctx, srvdef.ServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package usercenterapi
|
||||
|
||||
import (
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/usercenterser"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Data app个人中心数据
|
||||
// @Tags app个人中心相关接口
|
||||
// @Summary app个人中心数据 --by 王鸣
|
||||
// @Description 返回当前用户观看、收藏、追剧中、已购剧集统计
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.UserCenterDataResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/user/center/data [get]
|
||||
func Data(ctx *gin.Context) {
|
||||
resp, err := usercenterser.Data(ctx, common.GetUserId(ctx))
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
var _ = proto.UserCenterDataResp{}
|
||||
@@ -0,0 +1,125 @@
|
||||
package appg
|
||||
|
||||
import (
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/payCenter"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
const EsIndex = "sz"
|
||||
|
||||
var (
|
||||
Conf GlobalCfgEx
|
||||
Cache *cache.Cache
|
||||
DetectorV6 *ipaddr.XDBDetector
|
||||
DetectorV4 *ipaddr.XDBDetector
|
||||
)
|
||||
|
||||
type NameSource struct {
|
||||
FamilyNames []string `yaml:"familyNames"`
|
||||
MiddleNames []string `yaml:"middleNames"`
|
||||
AfterNames []string `yaml:"afterNames"`
|
||||
}
|
||||
type PictureSource struct {
|
||||
Portrait []string `yaml:"portrait"`
|
||||
UserbackGround []string `yaml:"userbackground"`
|
||||
ActressBackground []string `yaml:"actressbackground"`
|
||||
}
|
||||
type GlobalCfgEx struct {
|
||||
Redis struct {
|
||||
Addr string `yaml:"addr"`
|
||||
Db int `yaml:"db"`
|
||||
Passwd string `yaml:"passwd"`
|
||||
Subscribe bool `yaml:"subscribe"`
|
||||
}
|
||||
|
||||
Mongodb struct {
|
||||
Addr string `yaml:"addr"`
|
||||
LogAddr string `yaml:"logaddr"`
|
||||
RecomAddr string `yaml:"recomaddr"`
|
||||
AIOrderCenter string `yaml:"aiordercenter"`
|
||||
}
|
||||
Es struct {
|
||||
Addr string `yaml:"addr"`
|
||||
User string `yaml:"user"`
|
||||
Pass string `yaml:"pass"`
|
||||
} `yaml:"es"`
|
||||
RecomSys struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Addr string `yaml:"addr"`
|
||||
ApiKey string `yaml:"apikey"`
|
||||
} `yaml:"recomsys"`
|
||||
HttpOptions struct {
|
||||
DcCtxTimeOut uint64 `yaml:"dcCtxTimeOut"` //代充http请求 超时ctx取消时间
|
||||
} `yaml:"httpOptions"`
|
||||
Kafka struct {
|
||||
Url string `yaml:"url"`
|
||||
Group string `yaml:"group"`
|
||||
} `yaml:"kafka"`
|
||||
JwtKey string `yaml:"jwtKey"`
|
||||
RunMod string `yaml:"runMod"`
|
||||
ResKey string `yaml:"resKey"`
|
||||
CdnKey string `yaml:"cdnKey"`
|
||||
Pprof bool `yaml:"pprof"`
|
||||
Master bool `yaml:"master"` //是否是主节点
|
||||
Host string `yaml:"host"`
|
||||
ListenAddr string `yaml:"listenAddr"`
|
||||
KfAppId string `yaml:"kfAppId"`
|
||||
KfSecret string `yaml:"kfSecret"`
|
||||
KfApiUrlPrefix string `yaml:"kfApiUrlPrefix"`
|
||||
VersionUrl string `yaml:"versionUrl"`
|
||||
KafkaUrl string `yaml:"kafkaUrl"`
|
||||
CenterDomain string `yaml:"centerDomain"`
|
||||
TgToken string `yaml:"tgToken"`
|
||||
ReplayAttackConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
AppKey string `yaml:"appkey"`
|
||||
GuestKey string `yaml:"guestKey"`
|
||||
} `yaml:"attack"`
|
||||
ReplayH5AttackConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
AppKey string `yaml:"appkey"`
|
||||
GuestKey string `yaml:"guestKey"`
|
||||
} `yaml:"h5attack"`
|
||||
PayCenter struct {
|
||||
Addr string `yaml:"addr"`
|
||||
AppId int64 `yaml:"appId"`
|
||||
Serect string `yaml:"serect"`
|
||||
} `yaml:"payCenter"`
|
||||
AiCenterAppId int64 `yaml:"aiCenterAppId"`
|
||||
AiUndressingCallbackUrl string `yaml:"aiUndressingCallbackUrl"`
|
||||
NavigateUrl string `yaml:"navigateUrl"`
|
||||
NewProductUrl string `yaml:"newProductUrl"`
|
||||
TransactionUrl string `yaml:"transactionUrl"` // 支付下单地址
|
||||
LocalWebUrl string `yaml:"localWebUrl"`
|
||||
NewPayCenter payCenter.Conf `yaml:"newPayCenter"`
|
||||
JituanProductUrl string `yaml:"jituanProductUrl"`
|
||||
AiImageToVideoUrl string `yaml:"aiImageToVideoUrl"` // AI图生视频请求地址
|
||||
AiTextToImageUrl string `yaml:"aiTextToImageUrl"` // Ai文生图请求地址
|
||||
AiMateH5 string `yaml:"aiMateH5"`
|
||||
OrigDomain string `yaml:"origDomain"` // M3U8下载地址
|
||||
CdnAuthKey string `yaml:"cdnAuthKey"`
|
||||
TsAuthKey string `yaml:"tsAuthKey"`
|
||||
TrustedProxies string `yaml:"trustedProxies"`
|
||||
BucketVideoAddressV2 string `yaml:"bucketVideoAddressV2"`
|
||||
AdCenter struct {
|
||||
ApiDomain string `yaml:"apiDomain"` // api地址 如:https://adopenapi.adcs01.top
|
||||
MerchantCode string `yaml:"merchantCode"` // 商户 code
|
||||
AppCode string `yaml:"appCode"` // 应用 code
|
||||
AesKey string `yaml:"aesKey"` // 数据解密KEY
|
||||
} `yaml:"adCenter"`
|
||||
DataReport struct {
|
||||
Url string `yaml:"url"`
|
||||
} `yaml:"dataReport"`
|
||||
LsjMediaConf struct {
|
||||
AppId string `yaml:"appId"`
|
||||
ApiKey string `yaml:"apiKey"`
|
||||
ApiUrl string `yaml:"apiUrl"`
|
||||
ImageYuan string `yaml:"imageYuan"`
|
||||
KeyHex string `yaml:"keyHex"`
|
||||
M3u8Yuan string `yaml:"m3u8Yuan"`
|
||||
TsYuan string `yaml:"tsYuan"`
|
||||
DomainsRefreshHr int64 `yaml:"domainsRefreshHr"`
|
||||
} `yaml:"lsjMediaConf"`
|
||||
}
|
||||
@@ -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,34 @@
|
||||
package appg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
var familyNames = []string{
|
||||
"赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈",
|
||||
"褚", "卫", "蒋", "沈", "韩", "杨", "朱", "秦", "尤", "许",
|
||||
"何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏",
|
||||
"陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章",
|
||||
"云", "苏", "潘", "葛", "奚", "范", "彭", "郎", "鲁", "韦",
|
||||
}
|
||||
|
||||
var givenNames = []string{
|
||||
"伟", "芳", "娜", "秀英", "敏", "静", "丽", "强", "磊", "洋",
|
||||
"艳", "勇", "军", "杰", "娟", "涛", "明", "超", "秀兰", "霞",
|
||||
"平", "刚", "桂英", "华", "玲", "辉", "英", "志明", "建国", "文",
|
||||
"国强", "小红", "建军", "小明", "宇", "浩", "晨", "鑫", "博", "宸",
|
||||
"若曦", "雨桐", "梓涵", "紫萱", "雨萱", "诗涵", "欣怡", "嘉怡", "子涵", "梦琪",
|
||||
}
|
||||
|
||||
// GenerateNickname 随机生成一个中文昵称,格式:姓+名
|
||||
// 若词表为空则降级为"用户"+时间戳后6位
|
||||
func GenerateNickname() string {
|
||||
if len(familyNames) == 0 || len(givenNames) == 0 {
|
||||
return fmt.Sprintf("用户%06d", time.Now().UnixNano()%1000000)
|
||||
}
|
||||
family := familyNames[rand.IntN(len(familyNames))]
|
||||
given := givenNames[rand.IntN(len(givenNames))]
|
||||
return family + given
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
redis:
|
||||
addr: "172.104.181.99:16789"
|
||||
db: 2
|
||||
passwd: "B6vJHlYiTseLj77z"
|
||||
subscribe: true
|
||||
mongodb:
|
||||
addr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_qmdj?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
|
||||
logaddr: "mongodb://admin:TESTwfeZYP0kjBdy8DAUdwU@172.104.181.99:57017,172.104.181.99:57018,172.104.181.99:57019/web_qmdj_log?replicaSet=yctest&maxPoolSize=200&connectTimeout=30&authSource=admin"
|
||||
es:
|
||||
addr: "http://172.104.181.99:9200"
|
||||
user: "elastic"
|
||||
pass: "admin123456"
|
||||
httpOptions:
|
||||
dcCtxTimeOut: 10
|
||||
jwtKey: "2311b15a4da1c80dc541b99abb622233"
|
||||
runMod: "debug"
|
||||
resKey: "vEukA&w15z4VAD3kAY#fkL#rBnU!WDhN"
|
||||
cdnKey: "vEukAw15z4VAD3kAYfkLrBnUWDhN123"
|
||||
tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
|
||||
tgToken: ""
|
||||
attack:
|
||||
enable: false
|
||||
appkey: "kaFtkDJRcchRMTI9"
|
||||
h5key: "kaFttDJRcahRMTI7"
|
||||
windowDurationSeconds: 60
|
||||
pprof: false
|
||||
host: "172.104.181.99"
|
||||
listenAddr: "25119"
|
||||
origDomain: "https://yuan.bbaazz.me/"
|
||||
trustedProxies: ""
|
||||
bucketVideoAddressV2: "http://139.162.22.207:24996"
|
||||
transactionUrl: "https://bill.ruishengces.com"
|
||||
newPayCenter:
|
||||
appName: ""
|
||||
callbackUrl: ""
|
||||
apiUrl: ""
|
||||
mercID: ""
|
||||
mercSecret: ""
|
||||
adCenter:
|
||||
apiDomain: "https://adopenapi.adcs01.top"
|
||||
merchantCode: "1991129808640122880"
|
||||
appCode: "JHA-1009"
|
||||
aesKey: "gD4+rmbX5TS5cLbb3wAN5uEAYp/1j4hkQrS9SzuSjc8"
|
||||
dataReport:
|
||||
url: "https://api.shuifeng.cc"
|
||||
lsjMediaConf:
|
||||
appId: "430696"
|
||||
apiKey: "ed48e96ce490bc94bd5eb5133dd542d0"
|
||||
apiUrl: "https://zhlsaj.zrr4ij4.site"
|
||||
imageYuan: "https://d1kh52pvweq1c0.cloudfront.net"
|
||||
keyHex: "525202f9149e061d"
|
||||
m3u8Yuan: "https://zhlsaj.zrr4ij4.site"
|
||||
tsYuan: "https://tssdaa.hlvwcn.cn"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
—Џ€И’љБCЏ„
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
FILE_NAME="web_mdcom_app"
|
||||
REMOTE_FILE_PATH="/home/server2/web_mdcom/app/"
|
||||
TAG_NAME="web_mdcom_app.tar.gz"
|
||||
|
||||
set -e
|
||||
set -o
|
||||
function send_msg() {
|
||||
msg="@jscdsndj @xiuyuan_Lee @steven0yu %0A服务:SERVER2 站点-91spz.com管理服务APP%0A更新内容:$content%0A创建时间: $(date +%F_%X)%0A操作人:$(git config user.name)"
|
||||
curl -X POST "https://api.telegram.org/bot6357085965:AAEmXfjZnZwnDTicL7Wjbd7cXvYv_G98w8A/sendMessage" -d "chat_id=$1&text=$msg" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
read -p "输入yes, 开始上传: " word
|
||||
if [[ ! "yes" =~ $word ]]; then
|
||||
echo "终止"
|
||||
else
|
||||
read -e -p "本次更新内容(输入q退出编辑): " -r content
|
||||
echo "开始"
|
||||
rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
|
||||
GOOS=linux GOARCH=amd64 go build -o $FILE_NAME
|
||||
md5sum $FILE_NAME | cut -d ' ' -f1 >yinsemd5.txt
|
||||
tar zvcf $TAG_NAME ./$FILE_NAME yinsemd5.txt
|
||||
md5sum $TAG_NAME | cut -d ' ' -f1 >yinsemd5.txt
|
||||
scp -i ~/.ssh/id_rsa -P 22889 ./yinsemd5.txt server2@116.212.120.2:$REMOTE_FILE_PATH
|
||||
scp -i ~/.ssh/id_rsa -P 22889 ./$TAG_NAME server2@116.212.120.2:$REMOTE_FILE_PATH
|
||||
send_msg -4791758991 #服务器小组
|
||||
fi
|
||||
rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
sudo make test
|
||||
|
||||
scp -P 22889 web_91av_app.tar.gz root@172.104.181.99:/server/web_91av/app/
|
||||
ssh root@172.104.181.99 -p 22889 "cd /server/web_91av/app/ && /bin/tar -xf /server/web_91av/app/web_91av_app.tar.gz && /usr/bin/supervisorctl restart web_91av_app"
|
||||
if [ $? -eq 0 ];then
|
||||
echo -e "\033[32m 91spz.com H5更新成功 \033[0m"
|
||||
else
|
||||
echo -e "\033[31m 91spz.com H5更新失败 \033[0m"
|
||||
fi
|
||||
rm -f web_91av_app.tar.gz
|
||||
+7561
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 https://视频站-api.lcncctn.com
|
||||
// @BasePath /
|
||||
// @Schemes http https
|
||||
|
||||
// @securityDefinitions.basic BasicAuth
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @securitydefinitions.oauth2.application OAuth2Application
|
||||
// @tokenUrl https://example.com/oauth/token
|
||||
// @scope.write Grants write access
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
// @securitydefinitions.oauth2.implicit OAuth2Implicit
|
||||
// @authorizationUrl https://example.com/oauth/authorize
|
||||
// @scope.write Grants write access
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
// @securitydefinitions.oauth2.password OAuth2Password
|
||||
// @tokenUrl https://example.com/oauth/token
|
||||
// @scope.read Grants read access
|
||||
// @scope.write Grants write access
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
|
||||
// @tokenUrl https://example.com/oauth/token
|
||||
// @authorizationUrl https://example.com/oauth/authorize
|
||||
// @scope.admin Grants read and write access to administrative information
|
||||
|
||||
func main() {
|
||||
server.Run(os.Args)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// gin 验证中间件
|
||||
|
||||
var (
|
||||
// 白名单,不需要验证token的api
|
||||
whiteList = []string{
|
||||
"/swagger",
|
||||
"/api/app/auth/guest",
|
||||
"/api/app/auth/register",
|
||||
"/api/app/auth/login",
|
||||
"/api/app/auth/resetPassword",
|
||||
"/api/app/playlet/home",
|
||||
"/api/app/playlet/all/list",
|
||||
"/api/app/playlet/hot/list",
|
||||
"/api/app/playlet/topic/list",
|
||||
"/api/app/playlet/topic/detail",
|
||||
"/api/app/playlet/hot/rank/list",
|
||||
"/api/app/playlet/tag/options",
|
||||
"/api/app/playlet/tag/list",
|
||||
"/api/app/playlet/guess-like/list",
|
||||
"/api/app/playlet/search",
|
||||
"/api/app/playlet/hot-like",
|
||||
"/api/app/playlet/detail",
|
||||
"/api/app/playlet/chapter/detail",
|
||||
"/api/app/recharge/package/list",
|
||||
"/api/app/comment/list",
|
||||
"/api/app/theme/list",
|
||||
"/api/app/config/info",
|
||||
"/api/app/config/footer/info",
|
||||
"/api/app/config/richtext/list",
|
||||
"/api/app/applist",
|
||||
"/api/app/translation/category/list",
|
||||
"/api/app/navigation/header",
|
||||
"/api/app/health/ping",
|
||||
"/api/app/statcenter/sync",
|
||||
"/h5/m3u8",
|
||||
"/enkey",
|
||||
"/newenkey",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
tokener = utils.NewTokenRedis()
|
||||
)
|
||||
|
||||
func Auth(ctx *gin.Context) {
|
||||
dotoken := func() srvdef.CodeType {
|
||||
token := ctx.Request.Header.Get("Authorization")
|
||||
// 去掉Bearer,如果有
|
||||
token = utils.StripBearerPrefixFromTokenString(token)
|
||||
|
||||
if token == "" {
|
||||
for _, w := range whiteList {
|
||||
if strings.Contains(ctx.Request.URL.Path, w) {
|
||||
return srvdef.Success
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("Auth:token is empty", log.Any("sysType", ctx.GetString(consts.CtxSysType)))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
|
||||
userId, newToken, err := utils.ParseUserToken(token, appg.Conf.JwtKey)
|
||||
if err != nil {
|
||||
for _, w := range whiteList {
|
||||
if strings.Contains(ctx.Request.URL.Path, w) {
|
||||
return srvdef.Success
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("Auth:ParseUserToken", log.Any("token", token), log.E(err))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
|
||||
if pubdef.IsCrawlerUser(userId) {
|
||||
if len(newToken) > 0 {
|
||||
ctx.Writer.Header().Set("Refresh-Authorization", newToken)
|
||||
}
|
||||
ctx.Set(consts.CtxUserID, userId)
|
||||
ctx.Set(consts.CtxToken, token)
|
||||
return srvdef.Success
|
||||
}
|
||||
|
||||
has, err := tokener.Exists(userId, token)
|
||||
if err != nil {
|
||||
log.Error("Auth:Exists", log.Any("userId", userId), log.Any("token", token), log.E(err))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
if !has {
|
||||
log.Error("Auth:Exists:token not exists", log.Any("userId", userId), log.Any("token", token), log.E(err))
|
||||
return srvdef.InvalidToken
|
||||
}
|
||||
|
||||
if len(newToken) > 0 {
|
||||
err = tokener.Save(userId, newToken)
|
||||
if err != nil {
|
||||
log.Error("Auth:Save", log.Any("userId", userId), log.Any("token", newToken), log.E(err))
|
||||
return srvdef.StoreLevel
|
||||
}
|
||||
ctx.Writer.Header().Set("Refresh-Authorization", newToken)
|
||||
}
|
||||
|
||||
ctx.Set(consts.CtxUserID, userId)
|
||||
ctx.Set(consts.CtxToken, token)
|
||||
|
||||
return srvdef.Success
|
||||
}
|
||||
|
||||
errType := dotoken()
|
||||
if errType != srvdef.Success {
|
||||
// log.Info("Auth bad request", log.Any("Path", ctx.Request.URL.Path), log.Any("ip", ctx.ClientIP()))
|
||||
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
|
||||
"code": errType,
|
||||
"msg": errType.Msg(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,18 @@
|
||||
package ipblacklist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func IpBlackListMiddleware(ctx *gin.Context) {
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
if configser.CheckIpBlack(ip) {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errors.New("login limit"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package ipstat
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func IpStatMiddleware(ctx *gin.Context) {
|
||||
ctx.Next()
|
||||
|
||||
normalized := normalizeIP(ipaddr.ResolveClientIP(ctx))
|
||||
if normalized == "" {
|
||||
return
|
||||
}
|
||||
|
||||
date := time.Now().In(utils.LocBJ).Format("20060102")
|
||||
key := redisconst.IpStatKey(pubdef.AppId, date)
|
||||
|
||||
added, err := redis.SAddCount(key, normalized)
|
||||
if err != nil {
|
||||
log.Warn("IpStatMiddleware SAdd failed", log.Any("key", key), log.E(err))
|
||||
return
|
||||
}
|
||||
if added > 0 {
|
||||
if err := redis.Expire(key, redisconst.IpStatExpire); err != nil {
|
||||
log.Warn("IpStatMiddleware Expire failed", log.Any("key", key), log.E(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeIP(raw string) string {
|
||||
ip := net.ParseIP(raw)
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
ip = v4
|
||||
}
|
||||
if isPrivateIP(ip) {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
var privateRanges = []net.IPNet{
|
||||
parseCIDR("10.0.0.0/8"),
|
||||
parseCIDR("172.16.0.0/12"),
|
||||
parseCIDR("192.168.0.0/16"),
|
||||
parseCIDR("127.0.0.0/8"),
|
||||
parseCIDR("::1/128"),
|
||||
parseCIDR("fe80::/10"),
|
||||
parseCIDR("fc00::/7"),
|
||||
}
|
||||
|
||||
func parseCIDR(s string) net.IPNet {
|
||||
_, network, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *network
|
||||
}
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
for _, r := range privateRanges {
|
||||
if r.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -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,266 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/tgbot"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Check 运行监控检查
|
||||
func Check(ctx *gin.Context) {
|
||||
//查看是否有限制登陆
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
ifKey := redisconst.IpForbitKey(ip)
|
||||
retValue, _ := redis.Get(ifKey)
|
||||
if retValue != nil {
|
||||
//log.Info("Test Check", log.Any("ip", ip), log.Any("retValue", *retValue))
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"code": srvdef.ErrIpForbid,
|
||||
"msg": srvdef.ErrIpForbid.Msg(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := ctx.Request.URL.Path
|
||||
raw := ctx.Request.URL.RawQuery
|
||||
rawData, err := ctx.GetRawData()
|
||||
if err != nil {
|
||||
log.Info("Test get raw data error", log.E(err))
|
||||
}
|
||||
// 将取出来的执行条件放回ctx中
|
||||
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData))
|
||||
//recover
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
buf := make([]byte, 2048)
|
||||
length := runtime.Stack(buf, false)
|
||||
info := string(buf[:length])
|
||||
log.Error("catch a unkown excetion", log.Any("excetion :", info))
|
||||
//发送tg消息
|
||||
msg := "monitor check crash,the stack:\n" + info
|
||||
tgbot.SendToProgram("API接口处理宕了", msg)
|
||||
}
|
||||
}()
|
||||
// Process request
|
||||
ctx.Next()
|
||||
}()
|
||||
// Stop timer
|
||||
stopTime := time.Now()
|
||||
latency := stopTime.Sub(start).Milliseconds()
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
//添加记录
|
||||
//log.Info("Test", log.Any("rawData", rawData))
|
||||
rawString := string(rawData)
|
||||
if path == "/api/app/feedback/add" {
|
||||
rawString = ""
|
||||
}
|
||||
addApiCall(path, int64(latency), rawString)
|
||||
//如果时间大于1秒发送tg消息
|
||||
if latency > 4000 {
|
||||
userId := ctx.GetInt64(consts.CtxUserID)
|
||||
msg := ""
|
||||
msg = msg + fmt.Sprintf("path:%s", path) + "\n"
|
||||
msg = msg + fmt.Sprintf("userId:%d", userId) + "\n"
|
||||
msg = msg + fmt.Sprintf("userIp:%s", ip) + "\n"
|
||||
msg = msg + fmt.Sprintf("rawData:%s", string(rawData)) + "\n"
|
||||
msg = msg + fmt.Sprintf("time:%d", latency) + "\n"
|
||||
tgbot.SendToProgram("API接口调用超时", msg)
|
||||
}
|
||||
//log.Info("monitor:Check", log.Any("latency", latency), log.Any("path", path))
|
||||
}
|
||||
|
||||
func init() {
|
||||
hourStatises = make(map[string]*LogicStatis)
|
||||
muniteStatises = make(map[string]*LogicStatis)
|
||||
apiCallChan = make(chan *StatisParam, 1024)
|
||||
godef.Go(loop)
|
||||
log.Info("(monitor:init) init success.")
|
||||
}
|
||||
|
||||
func loop() {
|
||||
log.Info("(monitor:loop) start...")
|
||||
defer log.Info("monitor:loop) end")
|
||||
var param *StatisParam
|
||||
for {
|
||||
select {
|
||||
case param = <-apiCallChan:
|
||||
onStatis(param)
|
||||
default:
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onStatis(param *StatisParam) {
|
||||
if param == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
hourStr := now.Format("2006-01-02 15")
|
||||
hourStatis, hourOk := hourStatises[hourStr]
|
||||
if !hourOk {
|
||||
hourStatis = &LogicStatis{
|
||||
ApisStatis: make(map[string]*APIStatis),
|
||||
}
|
||||
hourStatises[hourStr] = hourStatis
|
||||
}
|
||||
muniteStr := now.Format("2006-01-02 15:04")
|
||||
minuteStatis, minuteOk := muniteStatises[muniteStr]
|
||||
if !minuteOk {
|
||||
minuteStatis = &LogicStatis{
|
||||
ApisStatis: make(map[string]*APIStatis),
|
||||
}
|
||||
muniteStatises[muniteStr] = minuteStatis
|
||||
}
|
||||
onStatisApiCall(hourStr, param, hourStatis, 10000000)
|
||||
onStatisApiCall(muniteStr, param, minuteStatis, 20000)
|
||||
//如果新的一小时,发送上一小时数据
|
||||
if !hourOk {
|
||||
//清空分钟数据
|
||||
muniteStatises = make(map[string]*LogicStatis)
|
||||
lastHour := now.Add(-time.Hour).Format("2006-01-02 15")
|
||||
hourStatis, hourOk = hourStatises[lastHour]
|
||||
if hourOk {
|
||||
msg := fmt.Sprintf("统计时间:%s\n", lastHour)
|
||||
msg += formatStatis(hourStatis)
|
||||
tgbot.SendToProgram("API接口调用统计", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatStatis(hourStatis *LogicStatis) string {
|
||||
apiStatises := make([]*APIStatis, 0)
|
||||
for _, static := range hourStatis.ApisStatis {
|
||||
apiStatises = append(apiStatises, static)
|
||||
}
|
||||
sort.Slice(apiStatises, func(i, j int) bool {
|
||||
return apiStatises[i].CallNum > apiStatises[j].CallNum
|
||||
})
|
||||
//发送数据
|
||||
msg := ""
|
||||
maxlen := len(apiStatises)
|
||||
if maxlen > 10 {
|
||||
maxlen = 10
|
||||
}
|
||||
for i := 0; i < maxlen; i++ {
|
||||
apiStatis := apiStatises[i]
|
||||
msg += formatApiStatis(apiStatis)
|
||||
}
|
||||
timePerCall := int64(0)
|
||||
callPerSec := int64(0)
|
||||
if hourStatis.ApiCallNum > 0 {
|
||||
timePerCall = hourStatis.ApiTotalTime / hourStatis.ApiCallNum
|
||||
diffSecdonds := int64(time.Now().Sub(hourStatis.Start).Seconds())
|
||||
if diffSecdonds > 0 {
|
||||
callPerSec = hourStatis.ApiCallNum / diffSecdonds
|
||||
}
|
||||
}
|
||||
msg += fmt.Sprintf("ApiCallNum:%d \n", hourStatis.ApiCallNum)
|
||||
msg += fmt.Sprintf("ApiTotalTime:%d \n", hourStatis.ApiTotalTime)
|
||||
msg += fmt.Sprintf("ApiTimePerCall:%d \n", timePerCall)
|
||||
msg += fmt.Sprintf("ApiCallPerSec:%d \n", callPerSec)
|
||||
return msg
|
||||
}
|
||||
|
||||
func formatApiStatis(apiStatis *APIStatis) string {
|
||||
msg := ""
|
||||
if apiStatis.CallNum > 0 {
|
||||
apiStatis.TimePerCall = apiStatis.TotalTime / apiStatis.CallNum
|
||||
diffSeconds := int64(time.Now().Sub(apiStatis.Start).Seconds())
|
||||
if diffSeconds > 0 {
|
||||
apiStatis.CallPerSec = apiStatis.CallNum / diffSeconds
|
||||
}
|
||||
}
|
||||
|
||||
statisStr := fmt.Sprintf(`path %s:
|
||||
MaxRawData:%s
|
||||
CallNum:%d;MinTime:%d;MaxTime:%d;TotalTime:%d;TimePerCall:%d;CallPerSec:%d`,
|
||||
apiStatis.Path,
|
||||
apiStatis.MaxTimeRawData,
|
||||
apiStatis.CallNum,
|
||||
apiStatis.MinTime,
|
||||
apiStatis.MaxTime,
|
||||
apiStatis.TotalTime,
|
||||
apiStatis.TimePerCall,
|
||||
apiStatis.CallPerSec)
|
||||
msg += statisStr
|
||||
msg += "\n---------------------------------------------------\n"
|
||||
return msg
|
||||
}
|
||||
|
||||
// api统计
|
||||
func onStatisApiCall(dateStr string, param *StatisParam,
|
||||
logicStatis *LogicStatis, maxCallNum int64) {
|
||||
logicStatis.ApiCallNum++
|
||||
logicStatis.ApiTotalTime += param.Param2
|
||||
apiStatis, ok := logicStatis.ApisStatis[param.Param1]
|
||||
if !ok {
|
||||
apiStatis = &APIStatis{
|
||||
Path: param.Param1,
|
||||
MaxTimeRawData: param.Param3,
|
||||
CallNum: 0,
|
||||
MinTime: math.MaxInt64,
|
||||
MaxTime: 0,
|
||||
TotalTime: 0,
|
||||
Start: time.Now(),
|
||||
Send: 0,
|
||||
}
|
||||
logicStatis.ApisStatis[param.Param1] = apiStatis
|
||||
}
|
||||
apiStatis.CallNum++
|
||||
apiStatis.TotalTime += param.Param2
|
||||
if param.Param2 > apiStatis.MaxTime {
|
||||
apiStatis.MaxTime = param.Param2
|
||||
apiStatis.MaxTimeRawData = param.Param3
|
||||
}
|
||||
if param.Param2 < apiStatis.MinTime {
|
||||
apiStatis.MinTime = param.Param2
|
||||
}
|
||||
if apiStatis.CallNum > maxCallNum && apiStatis.Send < 4 {
|
||||
apiStatis.Send++
|
||||
msg := fmt.Sprintf("接口%s每分钟调用超过最大次数%d\n", apiStatis.Path, maxCallNum)
|
||||
msg += formatApiStatis(apiStatis)
|
||||
tgbot.SendToProgram("API调用警告", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func addApiCall(path string, latency int64, rawData string) {
|
||||
param := &StatisParam{
|
||||
Type: SP_ApiCall,
|
||||
Param1: path,
|
||||
Param2: latency,
|
||||
Param3: rawData,
|
||||
}
|
||||
pushParam(param)
|
||||
}
|
||||
|
||||
func pushParam(param *StatisParam) {
|
||||
//no block
|
||||
select {
|
||||
case apiCallChan <- param:
|
||||
default:
|
||||
info := "(monitor:pushParam) failed,chan is full."
|
||||
log.Info(info)
|
||||
tgbot.SendToProgram("monitor pushParam failed", info)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package reptilerecord
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const ctxKeyIsBot = "isBot"
|
||||
|
||||
var (
|
||||
botPatterns = map[string]string{
|
||||
"Googlebot": "Googlebot",
|
||||
"Bingbot": "Bingbot",
|
||||
"Slurp": "Slurp",
|
||||
"DuckDuckBot": "DuckDuckBot",
|
||||
"Baiduspider": "Baiduspider",
|
||||
"YandexBot": "YandexBot",
|
||||
"Sogou": "Sogou",
|
||||
"Exabot": "Exabot",
|
||||
"facebot": "facebot",
|
||||
"facebookexternalhit": "facebookexternalhit",
|
||||
}
|
||||
)
|
||||
|
||||
func ReptileRecord(c *gin.Context) {
|
||||
ua := c.Request.UserAgent()
|
||||
botName := identifyBot(ua)
|
||||
if botName != "" {
|
||||
c.Set(ctxKeyIsBot, true)
|
||||
log.Info("爬虫访问:", log.Any("botName", botName), log.Any("ua", ua), log.Any("path", c.Request.URL.Path), log.Any("ip", ipaddr.ResolveClientIP(c)))
|
||||
}
|
||||
}
|
||||
|
||||
// IsBot 返回当前请求是否来自已知搜索引擎爬虫
|
||||
func IsBot(c *gin.Context) bool {
|
||||
v, exists := c.Get(ctxKeyIsBot)
|
||||
return exists && v.(bool)
|
||||
}
|
||||
|
||||
func identifyBot(userAgent string) string {
|
||||
for name, pattern := range botPatterns {
|
||||
if strings.Contains(userAgent, pattern) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package templatecache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Cache *cache.Cache
|
||||
)
|
||||
|
||||
func init() {
|
||||
Cache = cache.New(5*time.Minute, 5*time.Minute)
|
||||
}
|
||||
|
||||
// 自定义 ResponseWriter 用于捕获 HTML 输出
|
||||
type templateResponseWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *templateResponseWriter) Write(b []byte) (int, error) {
|
||||
w.body.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func WithCache(duration time.Duration) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
// 只缓存 GET 请求(HEAD 由 _web 层处理,_server 不会收到 HEAD)
|
||||
if ctx.Request.Method != "GET" {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 生成缓存键(包含路径和查询参数)
|
||||
cacheKey := ctx.Request.URL.String()
|
||||
var cacheJson []byte
|
||||
if cacheData, exists := Cache.Get(cacheKey); exists {
|
||||
if item, ok := cacheData.([]byte); ok {
|
||||
cacheJson = item
|
||||
}
|
||||
}
|
||||
// 检查缓存
|
||||
if cacheJson != nil {
|
||||
ctx.Data(http.StatusOK, "application/json; charset=utf-8", cacheJson)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
writer := &templateResponseWriter{
|
||||
body: bytes.NewBuffer(nil),
|
||||
ResponseWriter: ctx.Writer,
|
||||
}
|
||||
ctx.Writer = writer
|
||||
|
||||
ctx.Next()
|
||||
|
||||
// 只有成功的响应才缓存
|
||||
status := ctx.Writer.Status()
|
||||
if status == 200 {
|
||||
cacheJsonData := writer.body.Bytes()
|
||||
cacheDuration := duration
|
||||
if cacheDuration == 0 {
|
||||
cacheDuration = 1 * time.Minute
|
||||
}
|
||||
Cache.Set(cacheKey, cacheJsonData, cacheDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,114 @@
|
||||
package visit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/middleware/reptilerecord"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/uservisitlog"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Visit 用户日活访问中间件:每天首次访问写入 user_visit_log
|
||||
func Visit(ctx *gin.Context) {
|
||||
if reptilerecord.IsBot(ctx) {
|
||||
return
|
||||
}
|
||||
val, ok := ctx.Get(consts.CtxUA)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ua, ok := val.(useragent.UA)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
RecordDailyVisit(ctx, ctx.GetInt64(consts.CtxUserID), ua, ipaddr.ResolveClientIP(ctx))
|
||||
}
|
||||
|
||||
// RecordDailyVisit 当天首次访问写入 user_visit_log,游客新建成功后也会立刻记一笔。
|
||||
func RecordDailyVisit(ctx context.Context, userId int64, ua useragent.UA, ip string) {
|
||||
if userId <= 0 {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
userVisitLogKey := redisconst.UserVisitLogKey(userId, now)
|
||||
exist, err := redis.Exists(userVisitLogKey)
|
||||
if err != nil || exist {
|
||||
return
|
||||
}
|
||||
visitInfo := &uservisitlog.UserVisitLog{
|
||||
Id: primitive.NewObjectID(),
|
||||
UserId: userId,
|
||||
SysType: ua.SysType,
|
||||
DevType: ua.DevType,
|
||||
DevID: ua.DevID,
|
||||
Version: ua.Ver,
|
||||
IP: ip,
|
||||
VisitAt: now,
|
||||
}
|
||||
if serr := uservisitlog.InsertOneVisitLog(ctx, visitInfo); serr != nil {
|
||||
return
|
||||
}
|
||||
if serr := userVisitStatus(ctx, userId, ua, visitInfo.IP, visitInfo.VisitAt); serr != nil {
|
||||
return
|
||||
}
|
||||
_ = redis.Set(userVisitLogKey, "1", utils.BeginOfToday().Add(24*time.Hour).Sub(now)+5*time.Minute)
|
||||
}
|
||||
|
||||
func userVisitStatus(ctx context.Context, uId int64, ua useragent.UA, ip string, visitTime time.Time) *srvdef.SrvErr {
|
||||
uFilter := primitive.M{usermod.FN.User.Id: uId}
|
||||
user, serr := usermod.FindOneUser(ctx, uFilter)
|
||||
if serr != nil {
|
||||
return serr
|
||||
}
|
||||
|
||||
todayZero := utils.BeginOfToday()
|
||||
yesterdayZero := todayZero.AddDate(0, 0, -1)
|
||||
setDoc := primitive.M{
|
||||
usermod.FN.User.AppVer: ua.Ver,
|
||||
usermod.FN.User.SysType: ua.SysType,
|
||||
usermod.FN.User.Device: ua.DevType,
|
||||
usermod.FN.User.LastIp: ip,
|
||||
usermod.FN.User.LastLoginAt: visitTime,
|
||||
}
|
||||
mergeUAExtFields(setDoc, user, ua)
|
||||
var updateDoc primitive.M
|
||||
if user.LastLoginAt.Before(todayZero) && user.LastLoginAt.After(yesterdayZero) {
|
||||
updateDoc = primitive.M{
|
||||
"$inc": primitive.M{usermod.FN.User.ConsecutiveLogin: 1},
|
||||
"$set": setDoc,
|
||||
}
|
||||
} else {
|
||||
setDoc[usermod.FN.User.ConsecutiveLogin] = int64(1)
|
||||
updateDoc = primitive.M{"$set": setDoc}
|
||||
}
|
||||
return usermod.UpdateOneUser(ctx, uFilter, updateDoc)
|
||||
}
|
||||
|
||||
func mergeUAExtFields(setDoc primitive.M, user *usermod.User, ua useragent.UA) {
|
||||
if v := strings.TrimSpace(ua.SessionID); v != "" && v != user.SessionID {
|
||||
setDoc[usermod.FN.User.SessionID] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DeviceBrand); v != "" && v != user.DeviceBrand {
|
||||
setDoc[usermod.FN.User.DeviceBrand] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DeviceModel); v != "" && v != user.DeviceModel {
|
||||
setDoc[usermod.FN.User.DeviceModel] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SystemName); v != "" && v != user.SystemName {
|
||||
setDoc[usermod.FN.User.SystemName] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SystemVersion); v != "" && v != user.SystemVersion {
|
||||
setDoc[usermod.FN.User.SystemVersion] = v
|
||||
}
|
||||
}
|
||||
@@ -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,19 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/authapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func authRouter(r *gin.Engine) {
|
||||
auth := r.Group("/api/app/auth")
|
||||
{
|
||||
auth.POST("/guest", authapi.GuestLogin)
|
||||
auth.POST("/register", authapi.Register)
|
||||
auth.POST("/login", authapi.Login)
|
||||
auth.GET("/getInfo", authapi.GetInfo)
|
||||
auth.POST("/profile", authapi.UpdateProfile)
|
||||
auth.POST("/resetPassword", authapi.ResetPassword)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/coinapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func coinRouter(r *gin.Engine) {
|
||||
coinGroup := r.Group("/api/app/coin")
|
||||
{
|
||||
coinGroup.POST("/buy", coinapi.BuyCoin) // 发起购买金币(第三方支付)
|
||||
coinGroup.GET("/orders", coinapi.GetCoinOrderHistory) // 查询购买记录
|
||||
coinGroup.GET("/trans/list", coinapi.GetCoinTransList) // 查询金币余额明细
|
||||
coinGroup.GET("/trans/export", coinapi.ExportCoinTrans) // 导出金币余额明细
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/commentapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func commentRouter(r *gin.Engine) {
|
||||
r.POST("/comment/send/", commentapi.SendComment)
|
||||
group := r.Group("/api/app/comment")
|
||||
{
|
||||
group.POST("/send", commentapi.SendComment)
|
||||
group.POST("/send/", commentapi.SendComment)
|
||||
group.GET("/list", commentapi.List)
|
||||
group.POST("/like/set", commentapi.SetLike)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
configapi "videoweb/app/api/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func configRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/config/info", configapi.Info)
|
||||
r.GET("/api/app/config/footer/info", configapi.FooterInfo)
|
||||
r.GET("/api/app/config/richtext/list", configapi.RichTextList)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/contactusapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func contactUsRouter(r *gin.Engine) {
|
||||
r.POST("/api/app/contactus/add", contactusapi.AddContactUs)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/feedbackapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func feedbackRouter(r *gin.Engine) {
|
||||
r.POST("/api/app/feedback/add", feedbackapi.AddFeedback)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/api/home"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/app/middleware/auth"
|
||||
"videoweb/app/middleware/cors"
|
||||
"videoweb/app/middleware/ipblacklist"
|
||||
"videoweb/app/middleware/ipstat"
|
||||
"videoweb/app/middleware/reptilerecord"
|
||||
"videoweb/app/middleware/templatecache"
|
||||
"videoweb/app/middleware/visit"
|
||||
"videoweb/pub/common"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"github.com/DeanThompson/ginpprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
_ "videoweb/app/docs"
|
||||
)
|
||||
|
||||
func Init() (engine *gin.Engine) {
|
||||
// 设置启动模式
|
||||
switch appg.Conf.RunMod {
|
||||
case "dev":
|
||||
gin.SetMode(gin.DebugMode)
|
||||
case "test":
|
||||
gin.SetMode(gin.TestMode)
|
||||
case "prod":
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
default:
|
||||
gin.SetMode(gin.DebugMode)
|
||||
}
|
||||
// 初始化gin引擎
|
||||
engine = gin.Default()
|
||||
if appg.Conf.TrustedProxies != "" {
|
||||
proxies := strings.Split(appg.Conf.TrustedProxies, ",")
|
||||
for i, p := range proxies {
|
||||
proxies[i] = strings.TrimSpace(p)
|
||||
}
|
||||
_ = engine.SetTrustedProxies(proxies)
|
||||
log.Infof("SetTrustedProxies: %v", proxies)
|
||||
} else {
|
||||
_ = engine.SetTrustedProxies([]string{})
|
||||
log.Infof("SetTrustedProxies: none (direct TCP IP mode)")
|
||||
}
|
||||
engine.HandleMethodNotAllowed = true
|
||||
engine.MaxMultipartMemory = 60 << 20
|
||||
|
||||
common.InitGinSecret(appg.Conf.ResKey, consts.RunMod(appg.Conf.RunMod))
|
||||
// 注册中间件
|
||||
registeMiddleware(engine)
|
||||
// 注册路由
|
||||
registeRouter(engine)
|
||||
|
||||
if appg.Conf.RunMod != "prod" {
|
||||
setGinSwagger(engine)
|
||||
}
|
||||
// 是否开启监控服务
|
||||
if appg.Conf.Pprof {
|
||||
ginpprof.Wrap(engine)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func registeMiddleware(engine *gin.Engine) {
|
||||
engine.Use(
|
||||
// gin.Recovery(),
|
||||
cors.Cors(&cors.Options{
|
||||
Origin: nil, // "*"
|
||||
Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete},
|
||||
AllowedHeaders: []string{
|
||||
"Content-Length", "Content-Type", "Authorization", "X-User-Agent",
|
||||
"device", "api_version", "x-api-key",
|
||||
"sid",
|
||||
"device_brand", "device-brand",
|
||||
"device_model", "device-model",
|
||||
"system_name", "system-name",
|
||||
"system_version", "system-version",
|
||||
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) {
|
||||
videoRouter(r)
|
||||
playletRouter(r)
|
||||
playletThemeRouter(r)
|
||||
commentRouter(r)
|
||||
coinRouter(r)
|
||||
feedbackRouter(r)
|
||||
contactUsRouter(r)
|
||||
userCenterRouter(r)
|
||||
authRouter(r)
|
||||
rechargeRouter(r)
|
||||
rechargePackageRouter(r)
|
||||
uploadRouter(r)
|
||||
healthRouter(r)
|
||||
configRouter(r)
|
||||
appInfoRouter(r)
|
||||
translationRouter(r)
|
||||
navigationRouter(r)
|
||||
statcenterRouter(r)
|
||||
r.GET("/notpage", templatecache.WithCache(time.Minute*1), home.PageNotFound)
|
||||
|
||||
}
|
||||
|
||||
// 设置swag 接口信息
|
||||
func setGinSwagger(router *gin.Engine) {
|
||||
url := ginSwagger.URL(fmt.Sprintf("%s/swagger/doc.json", appg.Conf.Host))
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
}
|
||||
@@ -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,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,43 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/playletapi"
|
||||
"videoweb/app/api/playlettagapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func playletRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/playlet")
|
||||
{
|
||||
group.GET("/home", playletapi.HomePlaylet)
|
||||
group.GET("/all/list", playletapi.AllList)
|
||||
group.GET("/hot/list", playletapi.HotList)
|
||||
group.GET("/topic/list", playletapi.TopicList)
|
||||
group.GET("/topic/detail", playletapi.TopicDetail)
|
||||
group.GET("/hot/rank/list", playletapi.HotRankList)
|
||||
group.GET("/tag/options", playlettagapi.List)
|
||||
group.GET("/tag/list", playletapi.TagVideoList)
|
||||
group.GET("/guess-like/list", playletapi.GuessLikeList)
|
||||
group.GET("/search", playletapi.Search)
|
||||
group.GET("/search/suggest", playletapi.SearchSuggest)
|
||||
group.GET("/hot-like", playletapi.HotLike)
|
||||
group.POST("/like/set", playletapi.SetLike)
|
||||
group.GET("/like/list", playletapi.LikeList)
|
||||
group.POST("/collect/set", playletapi.SetCollect)
|
||||
group.POST("/forward/heat", playletapi.ForwardHeat)
|
||||
group.GET("/collect/list", playletapi.CollectList)
|
||||
group.GET("/bought/list", playletapi.BoughtList)
|
||||
group.GET("/history/list", playletapi.HistoryList)
|
||||
group.POST("/history/delete", playletapi.DeleteHistory)
|
||||
group.POST("/history/clear", playletapi.ClearHistory)
|
||||
group.POST("/chapter/like/set", playletapi.SetChapterLike)
|
||||
group.POST("/chapter/collect/set", playletapi.SetChapterCollect)
|
||||
group.GET("/chapter/collect/list", playletapi.ChapterCollectList)
|
||||
group.POST("/chapter/watch/report", playletapi.ReportChapterWatch)
|
||||
group.POST("/chapter/unlock", playletapi.UnlockChapter)
|
||||
group.GET("/detail", playletapi.Detail)
|
||||
group.GET("/chapter/detail", playletapi.ChapterDetail)
|
||||
group.GET("/chapter/list", playletapi.ChapterList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/playletthemeapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func playletThemeRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/theme")
|
||||
{
|
||||
group.GET("/list", playletthemeapi.List)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/rechargeapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func rechargeRouter(r *gin.Engine) {
|
||||
vipGroup := r.Group("/api/app/vip")
|
||||
{
|
||||
vipGroup.POST("/pay", rechargeapi.SubmitOrder)
|
||||
vipGroup.GET("/orders", rechargeapi.GetOrderHistory)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/rechargepackageapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func rechargePackageRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/recharge/package")
|
||||
{
|
||||
group.GET("/list", rechargepackageapi.List)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/statcenterapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func statcenterRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/statcenter")
|
||||
{
|
||||
group.POST("/sync", statcenterapi.StatcenterSyncList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/translationapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func translationRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/translation")
|
||||
{
|
||||
group.GET("/category/list", translationapi.CategoryList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/uploadapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func uploadRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/upload")
|
||||
{
|
||||
group.POST("/image", uploadapi.UploadImage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"videoweb/app/api/usercenterapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func userCenterRouter(r *gin.Engine) {
|
||||
group := r.Group("/api/app/user/center")
|
||||
{
|
||||
group.GET("/data", usercenterapi.Data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/app/api/home"
|
||||
"videoweb/app/middleware/templatecache"
|
||||
"videoweb/app/service/videoser"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func videoRouter(r *gin.Engine) {
|
||||
r.GET("/richtext/:id/", templatecache.WithCache(time.Minute*1), home.RichtextApi)
|
||||
|
||||
r.GET("/h5/m3u8/*key", videoser.GetM3U8Ex)
|
||||
r.GET("/h5/vtt/*key", videoser.GetVTTEx)
|
||||
r.GET("/enkey/", videoser.Enkey)
|
||||
r.GET("/newenkey/", videoser.NewEnkey)
|
||||
r.GET("/lsjenkey", videoser.LsjEnkey)
|
||||
|
||||
r.GET("/urlRedirect/", templatecache.WithCache(time.Minute*1), home.UrlRedirect)
|
||||
}
|
||||
@@ -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/playletcache"
|
||||
"videoweb/app/service/translationser"
|
||||
ipstatsync "videoweb/app/skd/ipstat"
|
||||
"videoweb/app/skd/seo"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvversion"
|
||||
"videoweb/pub/common/tgbot"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/esmanager"
|
||||
"videoweb/pub/models"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/payCenter"
|
||||
"videoweb/pub/redis"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
time.Local, _ = time.LoadLocation("Asia/Shanghai")
|
||||
}
|
||||
func Run(args []string) {
|
||||
showVersion()
|
||||
if err := initConfig(); err != nil {
|
||||
log.Error("init config failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := initMongoDB(); err != nil {
|
||||
log.Error("init mongodb failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//初始化tg机器人
|
||||
if appg.Conf.TgToken != "" {
|
||||
// tgbot.InitTgBot(appg.Conf.TgToken, "[汁乎App]")
|
||||
//tgbot.SendToProgram("Test", "Test")
|
||||
}
|
||||
detectorV6, err := ipaddr.NewXDBDetectorV6("conf/ip2region_v6.xdb")
|
||||
if err != nil {
|
||||
fmt.Printf("创建检测器失败: %v\n", err)
|
||||
fmt.Println("正在尝试下载数据库文件...")
|
||||
}
|
||||
appg.DetectorV6 = detectorV6
|
||||
|
||||
detectorV4, err := ipaddr.NewXDBDetectorV4("conf/ip2region_v4.xdb")
|
||||
if err != nil {
|
||||
fmt.Printf("创建检测器失败: %v\n", err)
|
||||
fmt.Println("正在尝试下载数据库文件...")
|
||||
}
|
||||
appg.DetectorV4 = detectorV4
|
||||
proxy.InitBucketVideoAddress(appg.Conf.BucketVideoAddressV2)
|
||||
mongos.StartLid()
|
||||
initRedis()
|
||||
if err := initElasticSearch(); err != nil {
|
||||
log.Error("init ES failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
initData()
|
||||
ipstatsync.StartSync()
|
||||
dataReport.Init(dataReport.Config{
|
||||
ApiUrl: appg.Conf.DataReport.Url,
|
||||
RunMod: appg.Conf.RunMod,
|
||||
})
|
||||
|
||||
initHttpServer()
|
||||
//tgbot.SendToProgram("启动", "app启动完毕")
|
||||
shutdownHandler()
|
||||
}
|
||||
|
||||
func initConfig() error {
|
||||
|
||||
buf, err := ioutil.ReadFile("conf/app.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = yaml.Unmarshal(buf, &appg.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
payCenter.Init(appg.Conf.NewPayCenter)
|
||||
// nameBuf, err := ioutil.ReadFile("conf/names.yaml")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err = yaml.Unmarshal(nameBuf, &appg.Name); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// pictureBuf, err := ioutil.ReadFile("conf/picture.yaml")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// if err = yaml.Unmarshal(pictureBuf, &appg.Picture); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initMongoDB() error {
|
||||
shopDb := mongos.NewManager(appg.Conf.Mongodb.Addr)
|
||||
if err := shopDb.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
mongos.Shop = shopDb
|
||||
|
||||
ycShopDb := mongos.NewManager(appg.Conf.Mongodb.LogAddr)
|
||||
if err := ycShopDb.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
mongos.ShopLog = ycShopDb
|
||||
return nil
|
||||
}
|
||||
|
||||
func initElasticSearch() error {
|
||||
es, err := esmanager.NewManager(appg.Conf.Es.Addr,
|
||||
appg.Conf.Es.User, appg.Conf.Es.Pass)
|
||||
if err != nil {
|
||||
log.Info("new es failed", log.Any("err", err))
|
||||
return err
|
||||
}
|
||||
if err = es.ClientConnect(); err != nil {
|
||||
log.Info("connect es failed", log.Any("err", err))
|
||||
return err
|
||||
}
|
||||
esmanager.Instance = es
|
||||
log.Info("es:Init Ok!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 数据
|
||||
func initData() {
|
||||
models.Init()
|
||||
// service.Start()
|
||||
appg.InitCache()
|
||||
translationser.Start()
|
||||
configser.Start()
|
||||
appser.Start()
|
||||
playletcache.Start()
|
||||
seo.Start()
|
||||
}
|
||||
|
||||
func initRedis() error {
|
||||
if err := redis.Init(redis.Options{
|
||||
Addr: appg.Conf.Redis.Addr,
|
||||
Psw: appg.Conf.Redis.Passwd,
|
||||
Db: appg.Conf.Redis.Db,
|
||||
}); err != nil {
|
||||
log.Error("redis connect err", log.E(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func showVersion() {
|
||||
log.Info(fmt.Sprintf("sevice version:%s", srvversion.SERVICE_VERSION))
|
||||
log.Info(fmt.Sprintf("git version:%s", srvversion.GIT_VERSION))
|
||||
log.Info(fmt.Sprintf("build date:%s", srvversion.BUILD_DATE))
|
||||
log.Info(fmt.Sprintf("build ip:%s", srvversion.BUILD_IP))
|
||||
}
|
||||
|
||||
func initHttpServer() {
|
||||
addr := fmt.Sprintf(":%s", appg.Conf.ListenAddr)
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router.Init(),
|
||||
}
|
||||
go func() {
|
||||
// service connections
|
||||
log.Info("服务已启动", log.Any("地址", addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("listen:", log.E(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func shutdownHandler() {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Kill)
|
||||
<-quit
|
||||
log.Warn("Shutdown... server")
|
||||
|
||||
done := make(chan int)
|
||||
go func() {
|
||||
mongos.Shop.DisConnect()
|
||||
mongos.ShopLog.DisConnect()
|
||||
redis.Disconnect()
|
||||
configser.Stop()
|
||||
tgbot.ReleaseTgBot()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Warn("shutdown timeout....")
|
||||
case <-done:
|
||||
log.Info("graceful shutdown")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package appser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/srvdef"
|
||||
)
|
||||
|
||||
// AppCategoryData 查询应用分类列表及当前分类数据
|
||||
func AppCategoryData(ctx context.Context, categoryId int64) ([]proto.AppCategoryInfo, proto.AppCategoryInfo, *srvdef.SrvErr) {
|
||||
return GetAppListCache(), GetCategoryCache(categoryId), nil
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package appser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/pub/common/http"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
appListAPIPath = "/media/web/appinfo/applist"
|
||||
)
|
||||
|
||||
var jsonHeader = map[string]string{"Content-Type": "application/json"}
|
||||
|
||||
var (
|
||||
appListCron *cron.Cron
|
||||
appListCache struct {
|
||||
lock sync.RWMutex
|
||||
appList []proto.AppCategoryInfo
|
||||
categoryMaps map[int64]proto.AppCategoryInfo
|
||||
}
|
||||
)
|
||||
|
||||
type remoteAppListResp struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
List []proto.AppCategoryInfo `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Start 启动定时刷新应用列表缓存(每10分钟一次)
|
||||
func Start() {
|
||||
appListCron = cron.New(cron.WithSeconds())
|
||||
UpdateAppListCache(context.Background())
|
||||
_, _ = appListCron.AddFunc("0 */10 * * * ?", func() {
|
||||
UpdateAppListCache(context.Background())
|
||||
})
|
||||
appListCron.Start()
|
||||
}
|
||||
|
||||
// GetAppListCache 返回全部应用分类列表(带各分类下应用)
|
||||
func GetAppListCache() []proto.AppCategoryInfo {
|
||||
appListCache.lock.RLock()
|
||||
defer appListCache.lock.RUnlock()
|
||||
if len(appListCache.appList) == 0 {
|
||||
return make([]proto.AppCategoryInfo, 0)
|
||||
}
|
||||
result := make([]proto.AppCategoryInfo, len(appListCache.appList))
|
||||
copy(result, appListCache.appList)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCategoryCache 按分类ID返回单个分类(含应用列表);categoryId=0 返回第一个分类
|
||||
func GetCategoryCache(categoryId int64) proto.AppCategoryInfo {
|
||||
appListCache.lock.RLock()
|
||||
defer appListCache.lock.RUnlock()
|
||||
if categoryId == 0 && len(appListCache.appList) > 0 {
|
||||
return appListCache.appList[0]
|
||||
}
|
||||
if v, ok := appListCache.categoryMaps[categoryId]; ok {
|
||||
return v
|
||||
}
|
||||
return proto.AppCategoryInfo{AppList: make([]proto.AppInfoData, 0)}
|
||||
}
|
||||
|
||||
// UpdateAppListCache 从远端拉取应用列表并刷新内存缓存
|
||||
func UpdateAppListCache(ctx context.Context) {
|
||||
domain := proxy.DomainAddressRepair("", proxy.BucketVideoAddressV2)
|
||||
apiURL := strings.TrimSuffix(domain, "/") + appListAPIPath
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]string{"dc": "dc"})
|
||||
resp, err := http.Post(apiURL, jsonHeader, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
log.Warn("UpdateAppListCache http.Post failed", log.Any("url", apiURL), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
var remote remoteAppListResp
|
||||
if err = json.Unmarshal(resp.Data, &remote); err != nil {
|
||||
log.Warn("UpdateAppListCache unmarshal failed", log.Any("url", apiURL), log.E(err))
|
||||
return
|
||||
}
|
||||
if remote.Code != 200 {
|
||||
log.Warn("UpdateAppListCache bad code", log.Any("url", apiURL), log.Any("code", remote.Code))
|
||||
return
|
||||
}
|
||||
|
||||
config := configser.GetConfigInfo()
|
||||
advitise := configser.GetAdvitise()
|
||||
channelCode := config.ChannelCode
|
||||
selfAppName := strings.TrimSpace(config.WebSiteName)
|
||||
|
||||
categoryMaps := make(map[int64]proto.AppCategoryInfo)
|
||||
var appList []proto.AppCategoryInfo
|
||||
|
||||
for _, category := range remote.Data.List {
|
||||
var apps []proto.AppInfoData
|
||||
for _, app := range category.AppList {
|
||||
if selfAppName != "" && strings.HasPrefix(app.Name, selfAppName) {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, proto.AppInfoData{
|
||||
Id: app.Id,
|
||||
CategoryId: app.CategoryId,
|
||||
Name: app.Name,
|
||||
Intro: app.Intro,
|
||||
Description: app.Description,
|
||||
Logo: netutil.MergeURL(config.ImgCdn, app.Logo),
|
||||
Url: appendChannelCode(app.Url, channelCode),
|
||||
Sort: app.Sort,
|
||||
})
|
||||
}
|
||||
// 广告位 A(code=12)
|
||||
if category.Code == 12 {
|
||||
for _, ad := range advitise.AList {
|
||||
apps = append(apps, proto.AppInfoData{
|
||||
AdvitiseId: ad.Id,
|
||||
CategoryId: category.Id,
|
||||
Name: ad.Title,
|
||||
Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
|
||||
Url: ad.Link,
|
||||
Location: ad.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 广告位 B(code=13)
|
||||
if category.Code == 13 {
|
||||
for _, ad := range advitise.BList {
|
||||
apps = append(apps, proto.AppInfoData{
|
||||
AdvitiseId: ad.Id,
|
||||
CategoryId: category.Id,
|
||||
Name: ad.Title,
|
||||
Logo: netutil.MergeURL(config.ImgCdn, ad.Images),
|
||||
Url: ad.Link,
|
||||
Location: ad.Location,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(apps) == 0 {
|
||||
continue
|
||||
}
|
||||
category.AppList = apps
|
||||
categoryMaps[category.Id] = proto.AppCategoryInfo{
|
||||
Id: category.Id,
|
||||
Title: category.Title,
|
||||
Intro: category.Intro,
|
||||
Sort: category.Sort,
|
||||
Code: category.Code,
|
||||
}
|
||||
appList = append(appList, category)
|
||||
}
|
||||
|
||||
appListCache.lock.Lock()
|
||||
appListCache.appList = appList
|
||||
appListCache.categoryMaps = categoryMaps
|
||||
appListCache.lock.Unlock()
|
||||
}
|
||||
|
||||
// appendChannelCode 为应用 URL 拼接渠道码参数 dc=xxx;已含 dc 参数则不再拼接
|
||||
func appendChannelCode(rawURL, channelCode string) string {
|
||||
if len(channelCode) == 0 {
|
||||
channelCode = pubdef.DefaultChannelCode
|
||||
}
|
||||
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
channelCode = strings.TrimSpace(channelCode)
|
||||
if rawURL == "" || channelCode == "" {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
query := parsed.Query()
|
||||
if query.Has("dc") {
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
query.Set("dc", channelCode)
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package appser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
func TestAppendChannelCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rawURL string
|
||||
channelCode string
|
||||
wantContain string
|
||||
wantSkip bool
|
||||
}{
|
||||
{
|
||||
name: "append custom channel",
|
||||
rawURL: "https://example.com/app",
|
||||
channelCode: "custom",
|
||||
wantContain: "dc=custom",
|
||||
},
|
||||
{
|
||||
name: "fallback default channel",
|
||||
rawURL: "https://example.com/app",
|
||||
channelCode: "",
|
||||
wantContain: "dc=" + pubdef.DefaultChannelCode,
|
||||
},
|
||||
{
|
||||
name: "skip existing dc",
|
||||
rawURL: "https://example.com/app?dc=exists",
|
||||
channelCode: "custom",
|
||||
wantContain: "dc=exists",
|
||||
wantSkip: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := appendChannelCode(tt.rawURL, tt.channelCode)
|
||||
if !strings.Contains(got, tt.wantContain) {
|
||||
t.Fatalf("appendChannelCode() = %q, want contain %q", got, tt.wantContain)
|
||||
}
|
||||
if tt.wantSkip && strings.Contains(got, "dc=custom") {
|
||||
t.Fatalf("appendChannelCode() should keep existing dc: %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/app/service/reportser"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var tokener = utils.NewTokenRedis()
|
||||
|
||||
var defaultAvatars = []string{
|
||||
"v3/image/bo/ty/1016/girl1.jpeg",
|
||||
"v3/image/re/9y/1016/girl2.jpeg",
|
||||
"v3/image/5f/k4/1016/girl3.jpeg",
|
||||
"v3/image/ej/ss/1016/girl4.jpeg",
|
||||
"v3/image/yq/ys/1016/girl5.jpeg",
|
||||
"v3/image/9e/d3/1016/girl6.jpeg",
|
||||
}
|
||||
|
||||
func randomAvatar() string {
|
||||
return defaultAvatars[time.Now().UnixNano()%int64(len(defaultAvatars))]
|
||||
}
|
||||
|
||||
// generateUniqueInviteCode 生成 8 位唯一邀请码(纯大写字母),最多重试 10 次
|
||||
func generateUniqueInviteCode(ctx context.Context) string {
|
||||
for i := 0; i < 10; i++ {
|
||||
code := strings.ToUpper(utils.GetRandomLetters(8))
|
||||
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.InviteCode: code})
|
||||
if exist == nil {
|
||||
return code
|
||||
}
|
||||
log.Warn("authser:generateUniqueInviteCode collision, retrying", log.Any("code", code))
|
||||
}
|
||||
// 极小概率走到这里:用更长的随机串兜底
|
||||
return strings.ToUpper(utils.GetRandomLetters(12))
|
||||
}
|
||||
|
||||
// LoginResp 登录/注册成功后的响应数据
|
||||
type LoginResp struct {
|
||||
Token string `json:"token"`
|
||||
UserId int64 `json:"userId"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email"`
|
||||
NickName string `json:"nickName"`
|
||||
Introduction string `json:"introduction"` // 简介
|
||||
VipType pubdef.VipType `json:"vipType"`
|
||||
CurrentVipExpire time.Time `json:"currentVipExpire"` // VIP 过期时间,客户端需结合 vipType 判断是否真正有效
|
||||
ShowCardId int64 `json:"showCardId"` // 最新会员卡id
|
||||
ShowCardName string `json:"showCardName"` // 最新会员卡名称
|
||||
Status pubdef.UserStatus `json:"status"`
|
||||
Avatar string `json:"avatar"` // 头像
|
||||
InviteCode string `json:"inviteCode"` // 邀请码
|
||||
Type pubdef.UserType `json:"type"` // 身份类型:0游客 1普通用户
|
||||
LoginType pubdef.LoginType `json:"loginType"` // 登陆方式
|
||||
IsRegister bool `json:"isRegister"` // 是否已注册
|
||||
IsNewUser bool `json:"isNewUser"` // 本次是否新建游客
|
||||
PrivacyMode bool `json:"privacyMode"` // 隐私模式
|
||||
HasPassword bool `json:"hasPassword"` // 是否已设置密码
|
||||
Score int64 `json:"score"` // 积分
|
||||
Recharge int64 `json:"recharge"` // 金币余额
|
||||
}
|
||||
|
||||
// hashPassword 使用 bcrypt 对明文密码进行哈希
|
||||
func hashPassword(plain string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// checkPassword 验证明文密码与 bcrypt 哈希是否匹配
|
||||
func checkPassword(plain, hash string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
|
||||
}
|
||||
|
||||
// generateAndSaveToken 生成 JWT 并存入 Redis
|
||||
func generateAndSaveToken(userId int64) (string, *srvdef.SrvErr) {
|
||||
token, err := utils.GenerateUserToken(utils.UserClaims{
|
||||
UserID: userId,
|
||||
Expire: consts.UserJwtTokenExpire,
|
||||
}, appg.Conf.JwtKey)
|
||||
if err != nil {
|
||||
return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
|
||||
}
|
||||
if err = tokener.Save(userId, token); err != nil {
|
||||
return "", srvdef.New(srvdef.StoreLevel, "token存储失败", "")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// generateCrawlerToken 签发爬虫观看 JWT,不写入 Redis,避免占用真实用户槽位。
|
||||
func generateCrawlerToken() (string, *srvdef.SrvErr) {
|
||||
token, err := utils.GenerateUserToken(utils.UserClaims{
|
||||
UserID: pubdef.CrawlerUserId,
|
||||
Expire: consts.UserJwtTokenExpire,
|
||||
}, appg.Conf.JwtKey)
|
||||
if err != nil {
|
||||
return "", srvdef.New(srvdef.GenerateTokenFailed, srvdef.GenerateTokenFailed.Msg(), "")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// CrawlerLoginResp 爬虫虚拟游客资料,不对应任何数据库用户。
|
||||
func CrawlerLoginResp(token string) *LoginResp {
|
||||
return &LoginResp{
|
||||
Token: token,
|
||||
UserId: pubdef.CrawlerUserId,
|
||||
NickName: "游客",
|
||||
Status: pubdef.UserStatusNormal,
|
||||
Type: pubdef.UserTypeGuest,
|
||||
LoginType: pubdef.LoginTypeDevice,
|
||||
IsRegister: false,
|
||||
IsNewUser: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
// guestUserId:请求头 Authorization 解析出的用户ID;若为游客则升级该用户,否则新建
|
||||
// agentInviteCode:邀请人的邀请码,可为空
|
||||
func Register(ctx context.Context, guestUserId int64, accountNo, email, password, agentInviteCode string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) {
|
||||
accountNo = strings.TrimSpace(accountNo)
|
||||
email = strings.TrimSpace(email)
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
// 检查账号是否已存在
|
||||
exist, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.AccountNo: accountNo})
|
||||
if exist != nil {
|
||||
return nil, srvdef.New(srvdef.ErrDbInputExist, "账号已存在", "")
|
||||
}
|
||||
// 检查邮箱是否已存在
|
||||
exist, _ = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Email: email})
|
||||
if exist != nil {
|
||||
return nil, srvdef.New(srvdef.ErrDbInputExist, "邮箱已被注册", "")
|
||||
}
|
||||
|
||||
hashed, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.Failure, "密码处理失败", "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 请求头带游客 token:升级当前游客为普通用户
|
||||
if guestUserId > 0 {
|
||||
guest, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: guestUserId})
|
||||
if serr == nil && isGuestUser(guest) {
|
||||
setDoc := bson.M{
|
||||
usermod.FN.User.AccountNo: accountNo,
|
||||
usermod.FN.User.Email: email,
|
||||
usermod.FN.User.Password: hashed,
|
||||
usermod.FN.User.Type: pubdef.UserTypeNormal,
|
||||
usermod.FN.User.IsRegister: true,
|
||||
usermod.FN.User.LoginType: pubdef.LoginTypeAccount,
|
||||
usermod.FN.User.RegisterTime: now,
|
||||
usermod.FN.User.LastLoginAt: now,
|
||||
usermod.FN.User.UpdatedAt: now,
|
||||
usermod.FN.User.LastIp: ip,
|
||||
}
|
||||
if agentInviteCode != "" && guest.AgentInviteCode == "" {
|
||||
setDoc[usermod.FN.User.AgentInviteCode] = agentInviteCode
|
||||
setDoc[usermod.FN.User.IsDirect] = false
|
||||
guest.IsDirect = false
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevID); v != "" {
|
||||
setDoc[usermod.FN.User.DevID] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevType); v != "" {
|
||||
setDoc[usermod.FN.User.Device] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SysType); v != "" {
|
||||
setDoc[usermod.FN.User.SysType] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.Ver); v != "" {
|
||||
setDoc[usermod.FN.User.AppVer] = v
|
||||
}
|
||||
mergeUAExtFieldsToSet(setDoc, guest, ua)
|
||||
if serr = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: guest.Id}, bson.M{"$set": setDoc}); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
if agentInviteCode != "" && guest.AgentInviteCode == "" {
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.InviteCode: agentInviteCode},
|
||||
bson.M{
|
||||
"$inc": bson.M{usermod.FN.User.Invites: 1},
|
||||
"$set": bson.M{usermod.FN.User.UpdatedAt: now},
|
||||
},
|
||||
)
|
||||
}
|
||||
guest.AccountNo = accountNo
|
||||
guest.Email = email
|
||||
guest.Type = pubdef.UserTypeNormal
|
||||
guest.IsRegister = true
|
||||
guest.LoginType = pubdef.LoginTypeAccount
|
||||
guest.RegisterTime = now
|
||||
token, serr := generateAndSaveToken(guest.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
reportser.ReportUserRegister(guest, ua, ip, "username")
|
||||
reportser.ReportUserLogin(guest, ua, ip, pubdef.LoginTypeAccount)
|
||||
return toLoginResp(ctx, guest, token, true), nil
|
||||
}
|
||||
}
|
||||
|
||||
user := &usermod.User{
|
||||
Id: mongos.Lid.User.Get(),
|
||||
AccountNo: accountNo,
|
||||
Email: email,
|
||||
Password: hashed,
|
||||
NickName: appg.GenerateNickname(),
|
||||
Avatar: randomAvatar(),
|
||||
InviteCode: generateUniqueInviteCode(ctx),
|
||||
AgentInviteCode: agentInviteCode,
|
||||
Type: pubdef.UserTypeNormal,
|
||||
Status: pubdef.UserStatusNormal,
|
||||
IsRegister: true,
|
||||
LoginType: pubdef.LoginTypeAccount,
|
||||
RegIp: ip,
|
||||
LastIp: ip,
|
||||
DevID: ua.DevID,
|
||||
Device: ua.DevType,
|
||||
SysType: ua.SysType,
|
||||
AppVer: ua.Ver,
|
||||
OriVer: ua.Ver,
|
||||
SessionID: ua.SessionID,
|
||||
DeviceBrand: ua.DeviceBrand,
|
||||
DeviceModel: ua.DeviceModel,
|
||||
SystemName: ua.SystemName,
|
||||
SystemVersion: ua.SystemVersion,
|
||||
DistrictCode: normalizeChannel(channel),
|
||||
IsDirect: resolveUserDistrict(channel, agentInviteCode, ip, ua.SysType).IsDirect,
|
||||
LastLoginAt: now,
|
||||
RegisterTime: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
// 若填写了邀请码,给对应用户的 invites +1
|
||||
if agentInviteCode != "" {
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.InviteCode: agentInviteCode},
|
||||
bson.M{
|
||||
"$inc": bson.M{usermod.FN.User.Invites: 1},
|
||||
"$set": bson.M{usermod.FN.User.UpdatedAt: time.Now()},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
token, serr := generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
reportser.ReportUserRegister(user, ua, ip, "username")
|
||||
reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeAccount)
|
||||
return toLoginResp(ctx, user, token, false), nil
|
||||
}
|
||||
|
||||
// Login 用户登录(账号或邮箱均可)
|
||||
func Login(ctx context.Context, account, password string, ua useragent.UA, ip, channel string) (*LoginResp, *srvdef.SrvErr) {
|
||||
// 账号或邮箱均可登录
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{
|
||||
"$or": []bson.M{
|
||||
{usermod.FN.User.AccountNo: account},
|
||||
{usermod.FN.User.Email: account},
|
||||
},
|
||||
})
|
||||
if user == nil {
|
||||
return nil, srvdef.New(srvdef.AccountIsNotExists, srvdef.AccountIsNotExists.Msg(), "")
|
||||
}
|
||||
|
||||
if user.Status == pubdef.UserStatusDisable {
|
||||
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
|
||||
if !checkPassword(password, user.Password) {
|
||||
return nil, srvdef.New(srvdef.AccountErrPasswordError, srvdef.AccountErrPasswordError.Msg(), "")
|
||||
}
|
||||
|
||||
token, serr := generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
setDoc := bson.M{
|
||||
usermod.FN.User.LastLoginAt: now,
|
||||
usermod.FN.User.UpdatedAt: now,
|
||||
}
|
||||
if ip != "" {
|
||||
setDoc[usermod.FN.User.LastIp] = ip
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevID); v != "" {
|
||||
setDoc[usermod.FN.User.DevID] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevType); v != "" {
|
||||
setDoc[usermod.FN.User.Device] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SysType); v != "" {
|
||||
setDoc[usermod.FN.User.SysType] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.Ver); v != "" {
|
||||
setDoc[usermod.FN.User.AppVer] = v
|
||||
}
|
||||
// 已有账号的历史用户统一修正为普通用户
|
||||
if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal {
|
||||
setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal
|
||||
setDoc[usermod.FN.User.IsRegister] = true
|
||||
user.Type = pubdef.UserTypeNormal
|
||||
user.IsRegister = true
|
||||
}
|
||||
mergeUAExtFieldsToSet(setDoc, user, ua)
|
||||
_ = usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
)
|
||||
reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeAccount)
|
||||
return toLoginResp(ctx, user, token, true), nil
|
||||
}
|
||||
|
||||
// GetInfo 获取当前登录用户的最新信息(不刷新 token)
|
||||
func GetInfo(ctx context.Context, userId int64, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) {
|
||||
if pubdef.IsCrawlerUser(userId) {
|
||||
return CrawlerLoginResp(""), nil
|
||||
}
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
if user == nil {
|
||||
return nil, srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
|
||||
}
|
||||
now := time.Now()
|
||||
setDoc := bson.M{}
|
||||
if v := strings.TrimSpace(ua.DevID); v != "" && v != user.DevID {
|
||||
setDoc[usermod.FN.User.DevID] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevType); v != "" && v != user.Device {
|
||||
setDoc[usermod.FN.User.Device] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SysType); v != "" && v != user.SysType {
|
||||
setDoc[usermod.FN.User.SysType] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.Ver); v != "" && v != user.AppVer {
|
||||
setDoc[usermod.FN.User.AppVer] = v
|
||||
}
|
||||
if strings.TrimSpace(user.AccountNo) != "" && user.Type != pubdef.UserTypeNormal {
|
||||
setDoc[usermod.FN.User.Type] = pubdef.UserTypeNormal
|
||||
setDoc[usermod.FN.User.IsRegister] = true
|
||||
user.Type = pubdef.UserTypeNormal
|
||||
user.IsRegister = true
|
||||
}
|
||||
mergeUAExtFieldsToSet(setDoc, user, ua)
|
||||
if len(setDoc) > 0 {
|
||||
setDoc[usermod.FN.User.UpdatedAt] = now
|
||||
_ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
|
||||
}
|
||||
return toLoginResp(ctx, user, "", true), nil
|
||||
}
|
||||
|
||||
func mergeUAExtFieldsToSet(setDoc bson.M, user *usermod.User, ua useragent.UA) {
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SessionID); v != "" && v != user.SessionID {
|
||||
setDoc[usermod.FN.User.SessionID] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DeviceBrand); v != "" && v != user.DeviceBrand {
|
||||
setDoc[usermod.FN.User.DeviceBrand] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DeviceModel); v != "" && v != user.DeviceModel {
|
||||
setDoc[usermod.FN.User.DeviceModel] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SystemName); v != "" && v != user.SystemName {
|
||||
setDoc[usermod.FN.User.SystemName] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SystemVersion); v != "" && v != user.SystemVersion {
|
||||
setDoc[usermod.FN.User.SystemVersion] = v
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
|
||||
type UpdateProfileReq struct {
|
||||
UserId int64 // 用户ID(从 token 中获取)
|
||||
NickName *string // 新昵称(可选)
|
||||
Avatar *string // 新头像 URL(可选)
|
||||
Introduction *string // 简介(可选)
|
||||
PrivacyMode *bool // 隐私模式:true-开启,false-关闭(可选)
|
||||
OldPassword *string // 旧密码明文(已设置密码时修改必传)
|
||||
NewPassword *string // 新密码明文(设置或修改密码时必传)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改用户资料:支持修改头像、简介、密码,或组合修改
|
||||
func UpdateProfile(ctx context.Context, req *UpdateProfileReq) *srvdef.SrvErr {
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: req.UserId})
|
||||
if user == nil {
|
||||
return srvdef.New(srvdef.ErrUserNotExsit, srvdef.ErrUserNotExsit.Msg(), "")
|
||||
}
|
||||
|
||||
setDoc := bson.M{usermod.FN.User.UpdatedAt: time.Now()}
|
||||
|
||||
if req.NickName != nil && *req.NickName != "" {
|
||||
setDoc[usermod.FN.User.NickName] = *req.NickName
|
||||
}
|
||||
if req.Avatar != nil && *req.Avatar != "" {
|
||||
setDoc[usermod.FN.User.Avatar] = *req.Avatar
|
||||
}
|
||||
if req.Introduction != nil {
|
||||
setDoc[usermod.FN.User.Introduction] = strings.TrimSpace(*req.Introduction)
|
||||
}
|
||||
if req.PrivacyMode != nil {
|
||||
setDoc[usermod.FN.User.PrivacyMode] = *req.PrivacyMode
|
||||
}
|
||||
|
||||
// 设置或修改密码
|
||||
if req.OldPassword != nil || req.NewPassword != nil {
|
||||
if req.NewPassword == nil || strings.TrimSpace(*req.NewPassword) == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "新密码不能为空", "")
|
||||
}
|
||||
if strings.TrimSpace(user.Password) != "" {
|
||||
if req.OldPassword == nil || strings.TrimSpace(*req.OldPassword) == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "")
|
||||
}
|
||||
if !checkPassword(*req.OldPassword, user.Password) {
|
||||
return srvdef.New(srvdef.AccountErrPasswordError, "旧密码错误", "")
|
||||
}
|
||||
} else if req.OldPassword != nil && strings.TrimSpace(*req.OldPassword) != "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "修改密码时旧密码和新密码均需提供", "")
|
||||
}
|
||||
hashed, err := hashPassword(*req.NewPassword)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.Failure, "密码处理失败", "")
|
||||
}
|
||||
setDoc[usermod.FN.User.Password] = hashed
|
||||
}
|
||||
|
||||
// 没有任何需要更新的字段
|
||||
if len(setDoc) == 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "")
|
||||
}
|
||||
|
||||
return usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
)
|
||||
}
|
||||
|
||||
// ResetPassword 通过账号+邮箱双重校验后重置密码
|
||||
func ResetPassword(ctx context.Context, accountNo, email, newPassword string) *srvdef.SrvErr {
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{
|
||||
usermod.FN.User.AccountNo: accountNo,
|
||||
usermod.FN.User.Email: email,
|
||||
})
|
||||
if user == nil {
|
||||
return srvdef.New(srvdef.ErrUserNotExsit, "账号或邮箱不匹配", "")
|
||||
}
|
||||
|
||||
hashed, err := hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return srvdef.New(srvdef.Failure, "密码处理失败", "")
|
||||
}
|
||||
|
||||
return usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": bson.M{
|
||||
usermod.FN.User.Password: hashed,
|
||||
usermod.FN.User.UpdatedAt: time.Now(),
|
||||
}},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/statcenter"
|
||||
"videoweb/pub/common/utils"
|
||||
)
|
||||
|
||||
// UserDistrictInfo 统计中心返回的渠道/直推信息
|
||||
type UserDistrictInfo struct {
|
||||
IsDirect bool `json:"isDirect"`
|
||||
DistrictCode string `json:"districtCode"`
|
||||
ParentPromCode string `json:"parentPromCode"`
|
||||
ParentId uint64 `json:"parentId"`
|
||||
ParentInviteCount int64 `json:"parentInviteCount"`
|
||||
}
|
||||
|
||||
type userDistrictReq struct {
|
||||
AppId int32 `json:"appId"`
|
||||
DistrictCode string `json:"dc"`
|
||||
PromSeqe string `json:"pc"`
|
||||
IP string `json:"ip"`
|
||||
SysType string `json:"sysType"`
|
||||
}
|
||||
|
||||
// resolveUserDistrict 有邀请码视为非直推;否则向统计中心查询补量/直推。
|
||||
func resolveUserDistrict(channel, inviteCode, ip, sysType string) UserDistrictInfo {
|
||||
inviteCode = strings.TrimSpace(inviteCode)
|
||||
if inviteCode != "" {
|
||||
return UserDistrictInfo{IsDirect: false, ParentPromCode: inviteCode}
|
||||
}
|
||||
return GetUserDistictInfo(channel, inviteCode, ip, sysType)
|
||||
}
|
||||
|
||||
// GetUserDistictInfo 查询统计中心用户渠道信息,对齐 ksx_server。
|
||||
func GetUserDistictInfo(dc, pc, ip, sysType string) (districtInfo UserDistrictInfo) {
|
||||
base := strings.TrimSpace(appg.Conf.NewProductUrl)
|
||||
if base == "" {
|
||||
return
|
||||
}
|
||||
request := userDistrictReq{
|
||||
AppId: statcenter.STAT_APPID,
|
||||
SysType: sysType,
|
||||
DistrictCode: dc,
|
||||
PromSeqe: pc,
|
||||
IP: ip,
|
||||
}
|
||||
bodyStr, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("GetUserDistictInfo json.Marshal is fail error:%+v/data:%+v", err, request))
|
||||
return
|
||||
}
|
||||
url := strings.TrimRight(base, "/") + "/api/stat/userInvite/district"
|
||||
code, err := utils.POSTWithJResp(&districtInfo, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("GetUserDistictInfo POSTWithJResp", log.Any("url", url), log.E(err))
|
||||
return
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("GetUserDistictInfo response status", log.Any("code", code))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/appg"
|
||||
"videoweb/app/middleware/reptilerecord"
|
||||
"videoweb/app/middleware/visit"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/reportser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// GuestLoginReq 游客登录请求
|
||||
type GuestLoginReq struct {
|
||||
DevID string `json:"devID"` // 设备唯一标示(必填)
|
||||
Mac string `json:"mac"` // 设备 Mac(可选)
|
||||
DeviceId string `json:"deviceId"` // 备用设备ID(可选,写入 DevV2)
|
||||
Channel string `json:"channel"` // 渠道码(可选,用户无商区码时写入 DistrictCode)
|
||||
}
|
||||
|
||||
// isGuestUser 是否为可升级的游客身份
|
||||
func isGuestUser(user *usermod.User) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
if user.Type == pubdef.UserTypeNormal {
|
||||
return false
|
||||
}
|
||||
return !user.IsRegister && strings.TrimSpace(user.AccountNo) == ""
|
||||
}
|
||||
|
||||
// GuestLogin 游客登录:按 token / DevID / Mac / DeviceId 查找,不存在则创建游客
|
||||
func GuestLogin(ctx context.Context, userId int64, ip string, req *GuestLoginReq, ua useragent.UA) (*LoginResp, *srvdef.SrvErr) {
|
||||
if isBotRequest(ctx) {
|
||||
return crawlerGuestLogin()
|
||||
}
|
||||
if req == nil || strings.TrimSpace(req.DevID) == "" {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "devID不能为空", "")
|
||||
}
|
||||
ua.DevID = strings.TrimSpace(req.DevID)
|
||||
|
||||
var user *usermod.User
|
||||
var serr *srvdef.SrvErr
|
||||
reuseToken := false
|
||||
|
||||
if userId > 0 {
|
||||
user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
if serr != nil {
|
||||
if serr.Code != srvdef.ErrUserNotExsit {
|
||||
return nil, serr
|
||||
}
|
||||
userId = 0
|
||||
user = nil
|
||||
} else {
|
||||
reuseToken = true
|
||||
}
|
||||
}
|
||||
|
||||
isNewUser := false
|
||||
if userId == 0 {
|
||||
user, serr = findGuestUser(ctx, req)
|
||||
if serr != nil {
|
||||
if serr.Code != srvdef.ErrUserNotExsit {
|
||||
return nil, serr
|
||||
}
|
||||
user, serr = createGuestUser(ctx, ua, req, ip)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
isNewUser = true
|
||||
}
|
||||
}
|
||||
|
||||
if user.Status == pubdef.UserStatusDisable {
|
||||
return nil, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
setDoc := bson.M{
|
||||
usermod.FN.User.LastLoginAt: now,
|
||||
usermod.FN.User.UpdatedAt: now,
|
||||
usermod.FN.User.LastIp: ip,
|
||||
usermod.FN.User.LoginType: pubdef.LoginTypeDevice,
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevID); v != "" {
|
||||
setDoc[usermod.FN.User.DevID] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevType); v != "" {
|
||||
setDoc[usermod.FN.User.Device] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SysType); v != "" {
|
||||
setDoc[usermod.FN.User.SysType] = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.Ver); v != "" {
|
||||
setDoc[usermod.FN.User.AppVer] = v
|
||||
}
|
||||
if v := strings.TrimSpace(req.Mac); v != "" {
|
||||
setDoc[usermod.FN.User.Mac] = v
|
||||
}
|
||||
if v := strings.TrimSpace(req.DeviceId); v != "" {
|
||||
setDoc[usermod.FN.User.DevV2] = v
|
||||
}
|
||||
// 历史用户 Type 为空/旧值时,未注册则归为游客
|
||||
if isGuestUser(user) && user.Type != pubdef.UserTypeGuest {
|
||||
setDoc[usermod.FN.User.Type] = pubdef.UserTypeGuest
|
||||
user.Type = pubdef.UserTypeGuest
|
||||
}
|
||||
mergeUAExtFieldsToSet(setDoc, user, ua)
|
||||
_ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
|
||||
|
||||
var token string
|
||||
if reuseToken {
|
||||
if old, err := tokener.Get(user.Id); err == nil && len(old) > 0 {
|
||||
token = string(old)
|
||||
}
|
||||
}
|
||||
if token == "" {
|
||||
token, serr = generateAndSaveToken(user.Id)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
}
|
||||
|
||||
resp := toLoginResp(ctx, user, token, true)
|
||||
resp.IsNewUser = isNewUser
|
||||
if isNewUser {
|
||||
visit.RecordDailyVisit(ctx, user.Id, ua, ip)
|
||||
reportser.ReportUserRegister(user, ua, ip, "deviceid")
|
||||
}
|
||||
reportser.ReportUserLogin(user, ua, ip, pubdef.LoginTypeDevice)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func findGuestUser(ctx context.Context, req *GuestLoginReq) (*usermod.User, *srvdef.SrvErr) {
|
||||
user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: req.DevID})
|
||||
if serr == nil {
|
||||
return user, nil
|
||||
}
|
||||
if serr.Code != srvdef.ErrUserNotExsit {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
mac := strings.TrimSpace(req.Mac)
|
||||
if mac != "" && !strings.Contains(mac, "00:00:00:00") {
|
||||
user, serr = usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Mac: mac})
|
||||
if serr == nil {
|
||||
return user, nil
|
||||
}
|
||||
if serr.Code != srvdef.ErrUserNotExsit {
|
||||
return nil, serr
|
||||
}
|
||||
}
|
||||
|
||||
deviceId := strings.TrimSpace(req.DeviceId)
|
||||
if deviceId != "" && !strings.Contains(deviceId, "00:00:00:00") {
|
||||
return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevV2: deviceId})
|
||||
}
|
||||
return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
|
||||
}
|
||||
|
||||
func createGuestUser(ctx context.Context, ua useragent.UA, req *GuestLoginReq, ip string) (*usermod.User, *srvdef.SrvErr) {
|
||||
now := time.Now()
|
||||
uid := mongos.Lid.User.Get()
|
||||
districtInfo := resolveUserDistrict(req.Channel, "", ip, ua.SysType)
|
||||
user := &usermod.User{
|
||||
Id: uid,
|
||||
Type: pubdef.UserTypeGuest,
|
||||
Status: pubdef.UserStatusNormal,
|
||||
LoginType: pubdef.LoginTypeDevice,
|
||||
IsRegister: false,
|
||||
NickName: fmt.Sprintf("游客%d", uid),
|
||||
Avatar: randomAvatar(),
|
||||
InviteCode: generateUniqueInviteCode(ctx),
|
||||
DistrictCode: normalizeChannel(req.Channel),
|
||||
IsDirect: districtInfo.IsDirect,
|
||||
RegIp: ip,
|
||||
LastIp: ip,
|
||||
DevID: ua.DevID,
|
||||
Mac: strings.TrimSpace(req.Mac),
|
||||
DevV2: strings.TrimSpace(req.DeviceId),
|
||||
Device: ua.DevType,
|
||||
SysType: ua.SysType,
|
||||
AppVer: ua.Ver,
|
||||
OriVer: ua.Ver,
|
||||
SessionID: ua.SessionID,
|
||||
DeviceBrand: ua.DeviceBrand,
|
||||
DeviceModel: ua.DeviceModel,
|
||||
SystemName: ua.SystemName,
|
||||
SystemVersion: ua.SystemVersion,
|
||||
LastLoginAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if nick := appg.GenerateNickname(); nick != "" {
|
||||
user.NickName = nick
|
||||
}
|
||||
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
|
||||
log.Error("authser:createGuestUser InsertOneUser failed", log.E(serr), log.Any("devID", ua.DevID))
|
||||
return nil, serr
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func toLoginResp(ctx context.Context, user *usermod.User, token string, withCDN bool) *LoginResp {
|
||||
avatar := user.Avatar
|
||||
if withCDN {
|
||||
cdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
|
||||
avatar = netutil.MergeURL(cdn, user.Avatar)
|
||||
}
|
||||
return &LoginResp{
|
||||
Token: token,
|
||||
UserId: user.Id,
|
||||
AccountNo: user.AccountNo,
|
||||
Email: user.Email,
|
||||
NickName: user.NickName,
|
||||
Introduction: user.Introduction,
|
||||
VipType: user.VipType,
|
||||
CurrentVipExpire: user.CurrentVipExpire,
|
||||
ShowCardId: user.ShowCardId,
|
||||
ShowCardName: user.ShowCardName,
|
||||
Status: user.Status,
|
||||
Avatar: avatar,
|
||||
InviteCode: user.InviteCode,
|
||||
Type: user.Type,
|
||||
LoginType: user.LoginType,
|
||||
IsRegister: user.IsRegister,
|
||||
PrivacyMode: user.PrivacyMode,
|
||||
HasPassword: strings.TrimSpace(user.Password) != "",
|
||||
Score: user.Score,
|
||||
Recharge: user.Recharge,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeChannel(channel string) string {
|
||||
return strings.TrimSpace(channel)
|
||||
}
|
||||
|
||||
func isBotRequest(ctx context.Context) bool {
|
||||
gc, ok := ctx.(*gin.Context)
|
||||
return ok && reptilerecord.IsBot(gc)
|
||||
}
|
||||
|
||||
// crawlerGuestLogin 给爬虫签发可观看的虚拟游客 token,不建用户、不写日活、不上报。
|
||||
func crawlerGuestLogin() (*LoginResp, *srvdef.SrvErr) {
|
||||
token, serr := generateCrawlerToken()
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return CrawlerLoginResp(token), nil
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package coinser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/service/rechargeser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/models/eventmod"
|
||||
"videoweb/pub/models/rechargepackagemod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// BuyCoinResp 购买金币下单响应
|
||||
type BuyCoinResp struct {
|
||||
TradeNo string `json:"tradeNo"`
|
||||
PayUrl string `json:"payUrl"`
|
||||
PayUrlMode string `json:"payUrlMode"`
|
||||
IsOpenNewBrowser bool `json:"isOpenNewBrowser"`
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func GetUserInfo(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
|
||||
return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
}
|
||||
|
||||
// CreateCoinOrder 创建金币购买订单(统一写入 vip_order 表,OrderType=2)。
|
||||
// 防重复:15 秒内同一用户+同一套餐的 pending 订单直接返回原支付链接。
|
||||
func CreateCoinOrder(ctx context.Context,
|
||||
userId int64, userName string,
|
||||
packageId int64, payMode string,
|
||||
sysType string, devType string, deviceID string, ip string,
|
||||
districtCode string,
|
||||
) (*BuyCoinResp, *srvdef.SrvErr) {
|
||||
|
||||
pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Id: packageId,
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeCoin,
|
||||
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
|
||||
})
|
||||
if serr != nil || pkg == nil {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "金币套餐不存在或已下架", "")
|
||||
}
|
||||
coinAmount := pkg.BaseCoin + pkg.GiftCoin
|
||||
if coinAmount <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "金币套餐到账数量无效", "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
dupFilter := bson.M{
|
||||
vipordermod.FN.VipOrder.UserId: userId,
|
||||
vipordermod.FN.VipOrder.CoinCfgId: packageId,
|
||||
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
|
||||
vipordermod.FN.VipOrder.PayMode: payMode,
|
||||
vipordermod.FN.VipOrder.Status: pubdef.VipOrderPending,
|
||||
vipordermod.FN.VipOrder.CreatedAt: bson.M{"$gte": now.Add(-15 * time.Second)},
|
||||
}
|
||||
existing, _ := vipordermod.FindOneVipOrder(ctx, dupFilter)
|
||||
if existing != nil && existing.PayUrl != "" {
|
||||
return &BuyCoinResp{
|
||||
TradeNo: existing.TradeNo,
|
||||
PayUrl: existing.PayUrl,
|
||||
PayUrlMode: existing.PayUrlMode,
|
||||
IsOpenNewBrowser: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
tradeNo := pubdef.GetTrandNoWithUUID()
|
||||
|
||||
payInfo, serr := rechargeser.SubmitPayOrder(ctx, userId, userName, tradeNo, pkg.Price, payMode, sysType, devType, deviceID, ip)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
order := &vipordermod.VipOrder{
|
||||
Id: mongos.Lid.VipOrder.Get(),
|
||||
OrderType: vipordermod.OrderTypeCoin,
|
||||
UserId: userId,
|
||||
CoinCfgId: packageId, // 兼容历史字段,存购买套餐ID
|
||||
Coin: coinAmount,
|
||||
BaseCoin: pkg.BaseCoin,
|
||||
GiftCoin: pkg.GiftCoin,
|
||||
Amount: pkg.Price,
|
||||
PayMode: payMode,
|
||||
TradeNo: tradeNo,
|
||||
Oid: payInfo.Oid,
|
||||
Status: pubdef.VipOrderPending,
|
||||
PayUrl: payInfo.PayUrl,
|
||||
PayUrlMode: payInfo.PayUrlMode,
|
||||
PayChannel: payInfo.PayChannel,
|
||||
PayRate: payInfo.PayRate,
|
||||
IPAddr: ip,
|
||||
SysType: sysType,
|
||||
DistrictCode: strings.TrimSpace(districtCode),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if serr = vipordermod.InsertOneVipOrder(ctx, order); serr != nil {
|
||||
log.Error("coinser:CreateCoinOrder InsertOneVipOrder failed",
|
||||
log.Any("tradeNo", tradeNo), log.E(serr))
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
go reportCoinOrderCreated(order, pkg, ip)
|
||||
|
||||
return &BuyCoinResp{
|
||||
TradeNo: tradeNo,
|
||||
PayUrl: payInfo.PayUrl,
|
||||
PayUrlMode: payInfo.PayUrlMode,
|
||||
IsOpenNewBrowser: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func reportCoinOrderCreated(order *vipordermod.VipOrder, pkg *rechargepackagemod.RechargePackage, ip string) {
|
||||
ctx := context.Background()
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: order.UserId})
|
||||
districtCode := order.DistrictCode
|
||||
if user != nil && strings.TrimSpace(user.DistrictCode) != "" {
|
||||
districtCode = user.DistrictCode
|
||||
}
|
||||
|
||||
ua := useragent.UA{SysType: order.SysType}
|
||||
eventId := newEventId(pubdef.TrackerAppId(), order.UserId, order.TradeNo, order.CreatedAt.Unix())
|
||||
commonField := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderCreated, districtCode, ua, strconv.FormatInt(order.UserId, 10), ip)
|
||||
commonField.Payload = dataReport.JsonRawMessage(&dataReport.OrderCreatedEvent{
|
||||
OrderID: order.TradeNo,
|
||||
OrderType: "coin_purchase",
|
||||
ProductID: strconv.FormatInt(pkg.Id, 10),
|
||||
ProductName: pkg.Name,
|
||||
Amount: dataReport.JsonNumber(order.Amount),
|
||||
Currency: "CNY",
|
||||
CoinQuantity: dataReport.JsonNumber(order.Coin),
|
||||
CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
|
||||
})
|
||||
|
||||
eventData, _ := json.Marshal(commonField)
|
||||
if err := eventmod.InsertOneEvent(ctx, &eventmod.Event{
|
||||
EventId: commonField.EventID,
|
||||
EventType: commonField.Event,
|
||||
Data: string(eventData),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
log.Error("reportCoinOrderCreated InsertOneEvent failed",
|
||||
log.Any("tradeNo", order.TradeNo), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
func newEventId(appId string, uid int64, orderId interface{}, createTime int64) string {
|
||||
input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
|
||||
h := md5.New()
|
||||
h.Write([]byte(input))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// GetCoinOrderHistory 查询用户金币购买记录
|
||||
func GetCoinOrderHistory(ctx context.Context, userId int64, page, pageSize int64) (vipordermod.VipOrderList, int64, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
vipordermod.FN.VipOrder.UserId: userId,
|
||||
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
|
||||
}
|
||||
total, serr := vipordermod.CountVipOrder(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, 0, serr
|
||||
}
|
||||
skip := (page - 1) * pageSize
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{vipordermod.FN.VipOrder.CreatedAt: -1}).
|
||||
SetSkip(skip).
|
||||
SetLimit(pageSize)
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, opts)
|
||||
return list, total, serr
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package coinser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/pub/common/response"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/models/cointransmod"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
// coinTransDefaultPageSize 金币余额明细默认每页数量
|
||||
coinTransDefaultPageSize int64 = 10
|
||||
// coinTransMaxPageSize 金币余额明细最大每页数量,避免单次请求拉取过多流水
|
||||
coinTransMaxPageSize int64 = 100
|
||||
// coinTransTypeAll 全部交易
|
||||
coinTransTypeAll int64 = 0
|
||||
// coinTransTypeRecharge 充值交易
|
||||
coinTransTypeRecharge int64 = 1
|
||||
// coinTransTypeConsume 消费交易
|
||||
coinTransTypeConsume int64 = 2
|
||||
// coinTransTypeReward 奖励交易
|
||||
coinTransTypeReward int64 = 3
|
||||
)
|
||||
|
||||
var (
|
||||
// coinTransListProjection 余额明细只读取列表展示需要的字段,降低Mongo读取成本
|
||||
coinTransListProjection = bson.D{
|
||||
{Key: cointransmod.FN.CoinTrans.Id, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.FlowDir, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.TranType, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.Amount, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.BalanceBefore, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.BalanceAfter, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.OrderNo, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.RelatedId, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.Remark, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// coinTransListSort 按交易时间倒序展示余额明细
|
||||
coinTransListSort = bson.D{
|
||||
{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1},
|
||||
}
|
||||
|
||||
// coinTransListHint 匹配 coin_trans 的 userId+createdAt 复合索引
|
||||
coinTransListHint = bson.D{
|
||||
{Key: cointransmod.FN.CoinTrans.UserId, Value: 1},
|
||||
{Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1},
|
||||
}
|
||||
)
|
||||
|
||||
// GetCoinTransList 查询当前用户金币余额明细
|
||||
//
|
||||
// 只查询当前登录用户的流水,按交易时间倒序分页,pageSize 最大限制为100
|
||||
func GetCoinTransList(ctx context.Context, userId int64, page, pageSize int64, transType int64) (*response.BasePageResp, *srvdef.SrvErr) {
|
||||
if userId <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
|
||||
}
|
||||
page, pageSize = normalizeCoinTransPage(page, pageSize)
|
||||
filter := bson.M{
|
||||
cointransmod.FN.CoinTrans.UserId: userId,
|
||||
}
|
||||
if err := applyCoinTransTypeFilter(filter, transType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total, err := cointransmod.CountCoinTrans(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total <= 0 {
|
||||
return &response.BasePageResp{
|
||||
TotalCount: 0,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(page),
|
||||
List: make([]*proto.CoinTransListItemResp, 0),
|
||||
}, nil
|
||||
}
|
||||
list, err1 := cointransmod.FindManyCoinTrans(ctx, filter, options.Find().
|
||||
SetProjection(coinTransListProjection).
|
||||
SetSort(coinTransListSort).
|
||||
SetHint(coinTransListHint).
|
||||
SetSkip((page-1)*pageSize).
|
||||
SetLimit(pageSize))
|
||||
if err1 != nil {
|
||||
return nil, err1
|
||||
}
|
||||
resp := make([]*proto.CoinTransListItemResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp = append(resp, coinTransToListItemResp(item))
|
||||
}
|
||||
return &response.BasePageResp{
|
||||
TotalCount: total,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(page),
|
||||
List: resp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExportCoinTrans 导出当前用户金币余额明细CSV
|
||||
func ExportCoinTrans(ctx context.Context, userId int64, transType int64) ([]byte, string, *srvdef.SrvErr) {
|
||||
if userId <= 0 {
|
||||
return nil, "", srvdef.New(srvdef.ErrUserNotExsit, "", "")
|
||||
}
|
||||
filter := bson.M{
|
||||
cointransmod.FN.CoinTrans.UserId: userId,
|
||||
}
|
||||
if err := applyCoinTransTypeFilter(filter, transType); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
list, err := cointransmod.FindManyCoinTrans(ctx, filter, options.Find().
|
||||
SetProjection(coinTransListProjection).
|
||||
SetSort(coinTransListSort).
|
||||
SetHint(coinTransListHint))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
data, serr := buildCoinTransCSV(list)
|
||||
if serr != nil {
|
||||
return nil, "", serr
|
||||
}
|
||||
return data, coinTransExportFileName(transType), nil
|
||||
}
|
||||
|
||||
// buildCoinTransCSV 构建金币流水CSV文件内容
|
||||
func buildCoinTransCSV(list cointransmod.CoinTransList) ([]byte, *srvdef.SrvErr) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("\xEF\xBB\xBF")
|
||||
writer := csv.NewWriter(&buf)
|
||||
if err := writer.Write([]string{"类型", "说明", "时间", "金币变动"}); err != nil {
|
||||
return nil, srvdef.New(srvdef.ExcelExportErr, "文件导出错误", "文件导出错误")
|
||||
}
|
||||
for _, item := range list {
|
||||
resp := coinTransToListItemResp(item)
|
||||
if resp == nil {
|
||||
continue
|
||||
}
|
||||
if err := writer.Write([]string{
|
||||
resp.TranTypeName,
|
||||
resp.Title,
|
||||
resp.CreatedAt,
|
||||
formatCoinTransChange(resp.ChangeAmount),
|
||||
}); err != nil {
|
||||
return nil, srvdef.New(srvdef.ExcelExportErr, "文件导出错误", "文件导出错误")
|
||||
}
|
||||
}
|
||||
writer.Flush()
|
||||
if err := writer.Error(); err != nil {
|
||||
return nil, srvdef.New(srvdef.ExcelExportErr, "文件导出错误", "文件导出错误")
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// coinTransExportFileName 生成金币流水导出文件名
|
||||
func coinTransExportFileName(transType int64) string {
|
||||
name := "交易明细"
|
||||
switch transType {
|
||||
case coinTransTypeRecharge:
|
||||
name = "充值明细"
|
||||
case coinTransTypeConsume:
|
||||
name = "消费明细"
|
||||
case coinTransTypeReward:
|
||||
name = "奖励明细"
|
||||
}
|
||||
return name + "_" + time.Now().In(utils.LocBJ).Format("20060102150405") + ".csv"
|
||||
}
|
||||
|
||||
// formatCoinTransChange 格式化金币变动值
|
||||
func formatCoinTransChange(change int64) string {
|
||||
if change > 0 {
|
||||
return "+" + strconv.FormatInt(change, 10)
|
||||
}
|
||||
return strconv.FormatInt(change, 10)
|
||||
}
|
||||
|
||||
// applyCoinTransTypeFilter 按交易明细Tab追加筛选条件
|
||||
func applyCoinTransTypeFilter(filter bson.M, transType int64) *srvdef.SrvErr {
|
||||
switch transType {
|
||||
case coinTransTypeAll:
|
||||
return nil
|
||||
case coinTransTypeRecharge:
|
||||
filter[cointransmod.FN.CoinTrans.TranType] = cointransmod.TranTypeCoinRecharge
|
||||
case coinTransTypeConsume:
|
||||
filter[cointransmod.FN.CoinTrans.FlowDir] = cointransmod.FlowOut
|
||||
case coinTransTypeReward:
|
||||
filter[cointransmod.FN.CoinTrans.FlowDir] = cointransmod.FlowIn
|
||||
filter[cointransmod.FN.CoinTrans.TranType] = bson.M{"$ne": cointransmod.TranTypeCoinRecharge}
|
||||
default:
|
||||
return srvdef.New(srvdef.ErrParamError, "类型错误", "类型错误")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeCoinTransPage 规范化金币流水分页参数
|
||||
//
|
||||
// page 小于等于0时按第1页处理;pageSize 未传时使用默认值,超过上限时截断
|
||||
func normalizeCoinTransPage(page, pageSize int64) (int64, int64) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = coinTransDefaultPageSize
|
||||
}
|
||||
if pageSize > coinTransMaxPageSize {
|
||||
pageSize = coinTransMaxPageSize
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
// coinTransToListItemResp 转换金币流水响应项
|
||||
//
|
||||
// amount 保持数据库中的正数金额,changeAmount 根据收支方向转换为带正负号的变动金额
|
||||
func coinTransToListItemResp(item *cointransmod.CoinTrans) *proto.CoinTransListItemResp {
|
||||
title := item.Remark
|
||||
if title == "" {
|
||||
title = item.TranType.Desc()
|
||||
}
|
||||
changeAmount := item.Amount
|
||||
if item.FlowDir == cointransmod.FlowOut {
|
||||
changeAmount = -item.Amount
|
||||
}
|
||||
return &proto.CoinTransListItemResp{
|
||||
Id: item.Id,
|
||||
Title: title,
|
||||
FlowDir: item.FlowDir,
|
||||
FlowDirName: item.FlowDir.Desc(),
|
||||
TranType: item.TranType,
|
||||
TranTypeName: item.TranType.Desc(),
|
||||
Amount: item.Amount,
|
||||
ChangeAmount: changeAmount,
|
||||
BalanceBefore: item.BalanceBefore,
|
||||
BalanceAfter: item.BalanceAfter,
|
||||
OrderNo: item.OrderNo,
|
||||
RelatedId: item.RelatedId,
|
||||
Remark: item.Remark,
|
||||
CreatedAt: formatCoinTransTime(item.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func formatCoinTransTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.In(utils.LocBJ).Format(pubdef.TimeISO)
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
"videoweb/app/api/proto"
|
||||
"videoweb/app/service/configser"
|
||||
"videoweb/app/service/reportser"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/netutil"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/models/commentlikemod"
|
||||
"videoweb/pub/models/commentmod"
|
||||
"videoweb/pub/models/playletchaptermod"
|
||||
"videoweb/pub/models/playletmod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
ObjectTypePlaylet int64 = 10 // 短剧评论
|
||||
ObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
|
||||
commentLevelFirst int64 = 1 // 一级评论
|
||||
commentLevelSecond int64 = 2 // 二级评论
|
||||
commentReviewPending int = 0 // 待审核
|
||||
commentReviewPass int = 1 // 审核通过
|
||||
commentMaxTextLen int = 200 // 评论最大字数
|
||||
commentDefaultPageSize int64 = 20 // 默认分页条数
|
||||
commentMaxPageSize int64 = 100 // 最大分页条数
|
||||
commentSortHot int64 = 1 // 热门
|
||||
commentSortNewest int64 = 2 // 最新
|
||||
commentPlayletHeatInc int64 = 3 // 发布评论增加的短剧热度值
|
||||
operateStatusYes int64 = 1 // 已操作
|
||||
operateStatusNo int64 = 2 // 未操作
|
||||
)
|
||||
|
||||
var commentListProjection = bson.D{
|
||||
{Key: commentmod.FN.Comment.Id, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ObjectId, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ObjectType, Value: 1},
|
||||
{Key: commentmod.FN.Comment.UserId, Value: 1},
|
||||
{Key: commentmod.FN.Comment.UserName, Value: 1},
|
||||
{Key: commentmod.FN.Comment.UserAvatar, Value: 1},
|
||||
{Key: commentmod.FN.Comment.Text, Value: 1},
|
||||
{Key: commentmod.FN.Comment.Level, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ParentsId, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ReplyCommentId, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ReplyUserName, Value: 1},
|
||||
{Key: commentmod.FN.Comment.CommentCount, Value: 1},
|
||||
{Key: commentmod.FN.Comment.CountLike, Value: 1},
|
||||
{Key: commentmod.FN.Comment.CreatedAt, Value: 1},
|
||||
}
|
||||
|
||||
// SendComment 发布评论
|
||||
func SendComment(ctx context.Context, userId int64, ip string, req *proto.CommentSendReq) *srvdef.SrvErr {
|
||||
// 评论入口先做频控和基础参数校验,再校验短剧/剧集是否可评论,避免无效请求进入写库链路。
|
||||
if userId <= 0 {
|
||||
return srvdef.New(srvdef.InvalidToken, "请先登录", "")
|
||||
}
|
||||
if err := checkCommentLimit(ip, req); err != nil {
|
||||
return err
|
||||
}
|
||||
text, err := normalizeCommentText(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectName, playletId, err := validateCommentTarget(ctx, req.ObjectType, req.ObjectId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
level := commentLevelFirst
|
||||
parentsId := int64(0)
|
||||
replyCommentId := int64(0)
|
||||
replyUserName := ""
|
||||
if req.ParentsId > 0 {
|
||||
replyComment, err := findReplyTargetComment(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
level = commentLevelSecond
|
||||
parentsId = req.ParentsId
|
||||
replyCommentId = replyComment.Id
|
||||
replyUserName = replyComment.UserName
|
||||
}
|
||||
user, err := findCommentUser(ctx, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
info := commentmod.CommentModel{
|
||||
Id: mongos.Lid.Comment.Get(),
|
||||
Ip: ip,
|
||||
ObjectId: req.ObjectId,
|
||||
ObjectName: objectName,
|
||||
ObjectType: req.ObjectType,
|
||||
UserId: user.Id,
|
||||
UserName: commentUserName(user),
|
||||
UserAvatar: user.Avatar,
|
||||
Level: level,
|
||||
ParentsId: parentsId,
|
||||
ReplyCommentId: replyCommentId,
|
||||
ReplyUserName: replyUserName,
|
||||
Text: text,
|
||||
CommentCount: 0,
|
||||
CountLike: 0,
|
||||
ReviewStatus: commentReviewPending,
|
||||
UpdatedAt: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := commentmod.InsertOneComment(ctx, &info); err != nil {
|
||||
log.Error("插入评论失败", log.Any("评论信息", info), log.E(err))
|
||||
return err
|
||||
}
|
||||
if isPlayletCommentType(req.ObjectType) {
|
||||
if err := incrementCommentPlayletHeatValue(ctx, playletId, commentPlayletHeatInc); err != nil {
|
||||
log.Error("评论发布后更新短剧热度失败", log.Any("短剧ID", playletId), log.Any("评论信息", info), log.E(err))
|
||||
return err
|
||||
}
|
||||
chapterId := int64(0)
|
||||
if req.ObjectType == ObjectTypePlayletChapter {
|
||||
chapterId = req.ObjectId
|
||||
}
|
||||
reportser.ReportPlayletComment(ctx, user, playletId, chapterId, text)
|
||||
}
|
||||
// app端发布评论先进入待审核状态,不立即增加业务对象评论数;后台审核通过时再统一同步 countComment
|
||||
return nil
|
||||
}
|
||||
|
||||
func findCommentUser(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
|
||||
user, err := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
if err != nil {
|
||||
log.Error("查询评论用户失败", log.Any("用户ID", userId), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func commentUserName(user *usermod.User) string {
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
if name := strings.TrimSpace(user.NickName); name != "" {
|
||||
return name
|
||||
}
|
||||
if name := strings.TrimSpace(user.AccountNo); name != "" {
|
||||
return name
|
||||
}
|
||||
return "用户" + strconv.FormatInt(user.Id, 10)
|
||||
}
|
||||
|
||||
// GetCommentPage 分页查询评论列表
|
||||
func GetCommentPage(ctx context.Context, userId int64, req *proto.GetCommentListReq) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
||||
// 列表只查一级且已审核评论,并通过一次批量查询补齐当前用户点赞状态,避免列表 N+1 查询。
|
||||
if req == nil || req.ObjectId <= 0 || !isSupportedCommentType(req.ObjectType) {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "")
|
||||
}
|
||||
pageNum, pageSize := normalizeCommentPage(req.PageNum, req.PageSize)
|
||||
return getCommentPageData(ctx, userId, req.ObjectId, req.ObjectType, req.SortOrder, pageNum, pageSize)
|
||||
}
|
||||
|
||||
// GetCommentList 兼容旧详情接口的评论查询
|
||||
func GetCommentList(ctx context.Context, objectId, objectType, page int64) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
||||
pageNum, _ := normalizeCommentPage(page, commentDefaultPageSize)
|
||||
return getCommentPageData(ctx, 0, objectId, objectType, commentSortNewest, pageNum, commentDefaultPageSize)
|
||||
}
|
||||
|
||||
// SetCommentLike 设置当前用户评论点赞状态
|
||||
func SetCommentLike(ctx context.Context, userId int64, req *proto.CommentLikeSetReq) (*proto.CommentLikeSetResp, *srvdef.SrvErr) {
|
||||
// 使用评论点赞关系表做去重;只有关系真实变化时才更新 _comment.countLike,重复点赞/取消不会重复增减计数。
|
||||
if userId <= 0 {
|
||||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "")
|
||||
}
|
||||
if req == nil || req.CommentId <= 0 || req.IsLike == nil {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "")
|
||||
}
|
||||
comment, err := findLikeTargetComment(ctx, req.CommentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := bson.M{
|
||||
commentlikemod.FN.CommentLike.UserId: userId,
|
||||
commentlikemod.FN.CommentLike.CommentId: req.CommentId,
|
||||
}
|
||||
changed := false
|
||||
if *req.IsLike {
|
||||
inserted, err := commentlikemod.InsertOneCommentLike(ctx, &commentlikemod.CommentLike{
|
||||
UserId: userId,
|
||||
CommentId: req.CommentId,
|
||||
ObjectId: comment.ObjectId,
|
||||
ObjectType: comment.ObjectType,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("新增评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", req.CommentId), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
changed = inserted
|
||||
} else {
|
||||
deleted, err := commentlikemod.DeleteOneCommentLike(ctx, filter)
|
||||
if err != nil {
|
||||
log.Error("删除评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", req.CommentId), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
changed = deleted > 0
|
||||
}
|
||||
if changed {
|
||||
delta := int64(1)
|
||||
if !*req.IsLike {
|
||||
delta = -1
|
||||
}
|
||||
if err := incrementCommentLikeCount(ctx, req.CommentId, delta); err != nil {
|
||||
rollbackCommentLike(ctx, userId, comment, *req.IsLike)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &proto.CommentLikeSetResp{
|
||||
CommentId: req.CommentId,
|
||||
IsLike: operateStatusByFlag(*req.IsLike),
|
||||
CountLike: applyLocalCounter(comment.CountLike, *req.IsLike, changed),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func checkCommentLimit(ip string, req *proto.CommentSendReq) *srvdef.SrvErr {
|
||||
curValue, err := redis.Incr(redisconst.CommentLimitKey(ip), redisconst.CommentLimitKeyExpire)
|
||||
if err != nil {
|
||||
log.Error("评论频率限制写入Redis失败", log.E(err), log.Any("IP", ip))
|
||||
}
|
||||
if curValue > 1 {
|
||||
log.Error("评论操作过于频繁", log.Any("IP", ip), log.Any("请求参数", req))
|
||||
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
|
||||
}
|
||||
dayCurValue, err := redis.Incr(redisconst.CommentOneDayLimitKey(ip), redisconst.CommentDayLimitKeyExpire)
|
||||
if err != nil {
|
||||
log.Error("评论每日限制写入Redis失败", log.E(err), log.Any("IP", ip))
|
||||
}
|
||||
if dayCurValue > 50 {
|
||||
log.Error("评论每日次数超限", log.Any("IP", ip), log.Any("请求参数", req))
|
||||
return srvdef.New(srvdef.ErrOperationTooFrequently, "", "评论太频繁")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeCommentText(req *proto.CommentSendReq) (string, *srvdef.SrvErr) {
|
||||
if req == nil || req.ObjectId <= 0 || !isSupportedCommentType(req.ObjectType) {
|
||||
return "", srvdef.New(srvdef.ErrParamError, "参数错误", "")
|
||||
}
|
||||
text := strings.TrimSpace(req.Text)
|
||||
if text == "" {
|
||||
return "", srvdef.New(srvdef.CommentTextIsEmpty, "评论内容不能为空", "")
|
||||
}
|
||||
if utf8.RuneCountInString(text) > commentMaxTextLen {
|
||||
return "", srvdef.New(srvdef.ErrParamError, "评论内容不能超过200字", "")
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func validateCommentTarget(ctx context.Context, objectType, objectId int64) (string, int64, *srvdef.SrvErr) {
|
||||
switch objectType {
|
||||
case ObjectTypePlaylet:
|
||||
playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: objectId,
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询评论短剧目标失败", log.Any("短剧ID", objectId), log.E(err))
|
||||
return "", 0, srvdef.New(srvdef.ErrParamError, "短剧不存在或未上架", "")
|
||||
}
|
||||
return playlet.Title, playlet.Id, nil
|
||||
case ObjectTypePlayletChapter:
|
||||
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
||||
playletchaptermod.FN.PlayletChapter.Id: objectId,
|
||||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
||||
{Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
|
||||
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询评论剧集目标失败", log.Any("剧集ID", objectId), log.E(err))
|
||||
return "", 0, srvdef.New(srvdef.ErrParamError, "剧集不存在", "")
|
||||
}
|
||||
if _, err = playletmod.FindOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
||||
playletmod.FN.Playlet.Status: playletmod.StatusUp,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||||
})); err != nil {
|
||||
log.Error("查询剧集所属短剧失败", log.Any("剧集ID", objectId), log.Any("短剧ID", chapter.PlayletId), log.E(err))
|
||||
return "", 0, srvdef.New(srvdef.ErrParamError, "短剧不存在或未上架", "")
|
||||
}
|
||||
if chapter.Title != "" {
|
||||
return chapter.Title, chapter.PlayletId, nil
|
||||
}
|
||||
return "第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集", chapter.PlayletId, nil
|
||||
default:
|
||||
return "", 0, srvdef.New(srvdef.ErrParamError, "评论类型有误", "")
|
||||
}
|
||||
}
|
||||
|
||||
func findReplyTargetComment(ctx context.Context, req *proto.CommentSendReq) (*commentmod.CommentModel, *srvdef.SrvErr) {
|
||||
parentComment, err := commentmod.FindOneComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: req.ParentsId,
|
||||
commentmod.FN.Comment.ObjectId: req.ObjectId,
|
||||
commentmod.FN.Comment.ObjectType: req.ObjectType,
|
||||
commentmod.FN.Comment.Level: commentLevelFirst,
|
||||
commentmod.FN.Comment.ParentsId: 0,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: commentmod.FN.Comment.Id, Value: 1},
|
||||
{Key: commentmod.FN.Comment.UserName, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询回复父评论失败", log.Any("父评论ID", req.ParentsId), log.Any("请求参数", req), log.E(err))
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "父评论不存在或不可回复", "")
|
||||
}
|
||||
replyCommentId := req.ReplyCommentId
|
||||
if replyCommentId <= 0 {
|
||||
replyCommentId = req.ParentsId
|
||||
}
|
||||
if replyCommentId == parentComment.Id {
|
||||
return parentComment, nil
|
||||
}
|
||||
replyComment, err := commentmod.FindOneComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: replyCommentId,
|
||||
commentmod.FN.Comment.ObjectId: req.ObjectId,
|
||||
commentmod.FN.Comment.ObjectType: req.ObjectType,
|
||||
commentmod.FN.Comment.Level: commentLevelSecond,
|
||||
commentmod.FN.Comment.ParentsId: req.ParentsId,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: commentmod.FN.Comment.Id, Value: 1},
|
||||
{Key: commentmod.FN.Comment.UserName, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询被回复评论失败", log.Any("被回复评论ID", replyCommentId), log.Any("请求参数", req), log.E(err))
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "被回复评论不存在或不可回复", "")
|
||||
}
|
||||
return replyComment, nil
|
||||
}
|
||||
|
||||
func incrementTargetCommentCount(ctx context.Context, objectType, objectId int64, delta int64) *srvdef.SrvErr {
|
||||
switch objectType {
|
||||
case ObjectTypePlaylet:
|
||||
if err := playletmod.UpdateOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: objectId,
|
||||
}, bson.M{"$inc": bson.M{playletmod.FN.Playlet.CountComment: delta}}); err != nil {
|
||||
log.Error("更新短剧评论数失败", log.Any("短剧ID", objectId), log.Any("增量", delta), log.E(err))
|
||||
return err
|
||||
}
|
||||
case ObjectTypePlayletChapter:
|
||||
if err := playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
|
||||
playletchaptermod.FN.PlayletChapter.Id: objectId,
|
||||
}, bson.M{"$inc": bson.M{playletchaptermod.FN.PlayletChapter.CountComment: delta}}); err != nil {
|
||||
log.Error("更新剧集评论数失败", log.Any("剧集ID", objectId), log.Any("增量", delta), log.E(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func incrementCommentPlayletHeatValue(ctx context.Context, playletId int64, delta int64) *srvdef.SrvErr {
|
||||
if playletId <= 0 || delta == 0 {
|
||||
return nil
|
||||
}
|
||||
return playletmod.UpdateOnePlaylet(ctx, bson.M{
|
||||
playletmod.FN.Playlet.Id: playletId,
|
||||
}, bson.M{
|
||||
"$inc": bson.M{
|
||||
playletmod.FN.Playlet.HeatValue: delta,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func getCommentPageData(ctx context.Context, userId int64, objectId, objectType, sortOrder, pageNum, pageSize int64) (*proto.GetCommentListResp, *srvdef.SrvErr) {
|
||||
resp := &proto.GetCommentListResp{
|
||||
ObjectId: objectId,
|
||||
TotalCount: 0,
|
||||
PageSize: int(pageSize),
|
||||
PageNum: int(pageNum),
|
||||
List: make([]proto.CommentInfo, 0),
|
||||
}
|
||||
if objectId <= 0 || !isSupportedCommentType(objectType) {
|
||||
return resp, nil
|
||||
}
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.ObjectId: objectId,
|
||||
commentmod.FN.Comment.ObjectType: objectType,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
commentmod.FN.Comment.Level: commentLevelFirst,
|
||||
commentmod.FN.Comment.ParentsId: 0,
|
||||
}
|
||||
total, err := commentmod.CountComments(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("统计评论列表失败", log.Any("filter", filter), log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
resp.TotalCount = total
|
||||
if total == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
skip := (pageNum - 1) * pageSize
|
||||
if skip >= total {
|
||||
return resp, nil
|
||||
}
|
||||
opts := options.Find().
|
||||
SetProjection(commentListProjection).
|
||||
SetSkip(skip).
|
||||
SetLimit(pageSize).
|
||||
SetSort(commentListSort(sortOrder))
|
||||
list, err := commentmod.FindManyComment(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("查询评论列表失败", log.Any("filter", filter), log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
parentIds := commentIdsFromList(list)
|
||||
children, err := findChildComments(ctx, parentIds)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
allComments := make(commentmod.CommentModelList, 0, len(list)+len(children))
|
||||
allComments = append(allComments, list...)
|
||||
allComments = append(allComments, children...)
|
||||
likedCommentIds, err := findLikedCommentIdMap(ctx, userId, allComments)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
childrenByParent := make(map[int64][]proto.CommentInfo)
|
||||
for _, item := range children {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
_, liked := likedCommentIds[item.Id]
|
||||
childrenByParent[item.ParentsId] = append(childrenByParent[item.ParentsId], buildCommentInfo(ctx, item, liked))
|
||||
}
|
||||
resp.List = make([]proto.CommentInfo, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
_, liked := likedCommentIds[item.Id]
|
||||
info := buildCommentInfo(ctx, item, liked)
|
||||
info.Children = childrenByParent[item.Id]
|
||||
resp.List = append(resp.List, info)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func commentListSort(sortOrder int64) bson.D {
|
||||
switch sortOrder {
|
||||
case commentSortHot:
|
||||
return bson.D{
|
||||
{Key: commentmod.FN.Comment.CountLike, Value: -1},
|
||||
{Key: commentmod.FN.Comment.Id, Value: -1},
|
||||
}
|
||||
case commentSortNewest:
|
||||
fallthrough
|
||||
default:
|
||||
return bson.D{
|
||||
{Key: commentmod.FN.Comment.CreatedAt, Value: -1},
|
||||
{Key: commentmod.FN.Comment.Id, Value: -1},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func commentIdsFromList(list commentmod.CommentModelList) []int64 {
|
||||
commentIds := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item != nil {
|
||||
commentIds = append(commentIds, item.Id)
|
||||
}
|
||||
}
|
||||
return commentIds
|
||||
}
|
||||
|
||||
func findChildComments(ctx context.Context, parentIds []int64) (commentmod.CommentModelList, *srvdef.SrvErr) {
|
||||
if len(parentIds) == 0 {
|
||||
return make(commentmod.CommentModelList, 0), nil
|
||||
}
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.ParentsId: bson.M{"$in": parentIds},
|
||||
commentmod.FN.Comment.Level: commentLevelSecond,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
}
|
||||
opts := options.Find().
|
||||
SetProjection(commentListProjection).
|
||||
SetSort(bson.D{
|
||||
{Key: commentmod.FN.Comment.ParentsId, Value: 1},
|
||||
{Key: commentmod.FN.Comment.CreatedAt, Value: 1},
|
||||
{Key: commentmod.FN.Comment.Id, Value: 1},
|
||||
})
|
||||
children, err := commentmod.FindManyComment(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("查询二级评论列表失败", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
|
||||
func findLikedCommentIdMap(ctx context.Context, userId int64, list commentmod.CommentModelList) (map[int64]struct{}, *srvdef.SrvErr) {
|
||||
likedMap := make(map[int64]struct{})
|
||||
if userId <= 0 || len(list) == 0 {
|
||||
return likedMap, nil
|
||||
}
|
||||
commentIds := commentIdsFromList(list)
|
||||
if len(commentIds) == 0 {
|
||||
return likedMap, nil
|
||||
}
|
||||
likes, err := commentlikemod.FindManyCommentLike(ctx, bson.M{
|
||||
commentlikemod.FN.CommentLike.UserId: userId,
|
||||
commentlikemod.FN.CommentLike.CommentId: bson.M{"$in": commentIds},
|
||||
}, options.Find().SetProjection(bson.D{
|
||||
{Key: commentlikemod.FN.CommentLike.CommentId, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("批量查询评论点赞状态失败", log.Any("用户ID", userId), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range likes {
|
||||
if item != nil {
|
||||
likedMap[item.CommentId] = struct{}{}
|
||||
}
|
||||
}
|
||||
return likedMap, nil
|
||||
}
|
||||
|
||||
func buildCommentInfo(ctx context.Context, item *commentmod.CommentModel, liked bool) proto.CommentInfo {
|
||||
return proto.CommentInfo{
|
||||
Id: item.Id,
|
||||
ObjectId: item.ObjectId,
|
||||
ObjectType: item.ObjectType,
|
||||
UserId: item.UserId,
|
||||
UserName: item.UserName,
|
||||
UserAvatar: withCommentAvatarDomain(ctx, item.UserAvatar),
|
||||
Text: item.Text,
|
||||
Level: item.Level,
|
||||
ParentsId: item.ParentsId,
|
||||
ReplyCommentId: item.ReplyCommentId,
|
||||
ReplyUserName: item.ReplyUserName,
|
||||
CommentCount: item.CommentCount,
|
||||
CountLike: item.CountLike,
|
||||
IsLike: operateStatusByFlag(liked),
|
||||
CreatedAt: item.CreatedAt,
|
||||
Children: make([]proto.CommentInfo, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func withCommentAvatarDomain(ctx context.Context, path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
imgCdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
|
||||
if imgCdn == "" {
|
||||
return path
|
||||
}
|
||||
return netutil.MergeURL(imgCdn, path)
|
||||
}
|
||||
|
||||
func findLikeTargetComment(ctx context.Context, commentId int64) (*commentmod.CommentModel, *srvdef.SrvErr) {
|
||||
comment, err := commentmod.FindOneComment(ctx, bson.M{
|
||||
commentmod.FN.Comment.Id: commentId,
|
||||
commentmod.FN.Comment.ReviewStatus: commentReviewPass,
|
||||
commentmod.FN.Comment.ObjectType: bson.M{
|
||||
"$in": []int64{ObjectTypePlaylet, ObjectTypePlayletChapter},
|
||||
},
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: commentmod.FN.Comment.Id, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ObjectId, Value: 1},
|
||||
{Key: commentmod.FN.Comment.ObjectType, Value: 1},
|
||||
{Key: commentmod.FN.Comment.CountLike, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询评论点赞目标失败", log.Any("评论ID", commentId), log.E(err))
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "评论不存在", "")
|
||||
}
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
func incrementCommentLikeCount(ctx context.Context, commentId int64, delta int64) *srvdef.SrvErr {
|
||||
filter := bson.M{
|
||||
commentmod.FN.Comment.Id: commentId,
|
||||
}
|
||||
if delta < 0 {
|
||||
filter[commentmod.FN.Comment.CountLike] = bson.M{"$gt": 0}
|
||||
}
|
||||
err := commentmod.UpdateOneComment(ctx, filter, bson.M{
|
||||
"$inc": bson.M{
|
||||
commentmod.FN.Comment.CountLike: delta,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if delta < 0 && err.Code == srvdef.ErrNoData {
|
||||
log.Warn("评论点赞数已为0,跳过扣减", log.Any("评论ID", commentId))
|
||||
return nil
|
||||
}
|
||||
log.Error("更新评论点赞数失败", log.Any("评论ID", commentId), log.Any("增量", delta), log.E(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rollbackCommentLike(ctx context.Context, userId int64, comment *commentmod.CommentModel, enabled bool) {
|
||||
filter := bson.M{
|
||||
commentlikemod.FN.CommentLike.UserId: userId,
|
||||
commentlikemod.FN.CommentLike.CommentId: comment.Id,
|
||||
}
|
||||
if enabled {
|
||||
if _, err := commentlikemod.DeleteOneCommentLike(ctx, filter); err != nil {
|
||||
log.Error("回滚评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", comment.Id), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, err := commentlikemod.InsertOneCommentLike(ctx, &commentlikemod.CommentLike{
|
||||
UserId: userId,
|
||||
CommentId: comment.Id,
|
||||
ObjectId: comment.ObjectId,
|
||||
ObjectType: comment.ObjectType,
|
||||
}); err != nil {
|
||||
log.Error("恢复评论点赞关系失败", log.Any("用户ID", userId), log.Any("评论ID", comment.Id), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCommentPage(pageNum, pageSize int64) (int64, int64) {
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = commentDefaultPageSize
|
||||
}
|
||||
if pageSize > commentMaxPageSize {
|
||||
pageSize = commentMaxPageSize
|
||||
}
|
||||
return pageNum, pageSize
|
||||
}
|
||||
|
||||
func isPlayletCommentType(objectType int64) bool {
|
||||
return objectType == ObjectTypePlaylet || objectType == ObjectTypePlayletChapter
|
||||
}
|
||||
|
||||
func isSupportedCommentType(objectType int64) bool {
|
||||
return isPlayletCommentType(objectType)
|
||||
}
|
||||
|
||||
func applyLocalCounter(current int64, enabled bool, changed bool) int64 {
|
||||
if !changed {
|
||||
return current
|
||||
}
|
||||
if enabled {
|
||||
return current + 1
|
||||
}
|
||||
if current <= 0 {
|
||||
return 0
|
||||
}
|
||||
return current - 1
|
||||
}
|
||||
|
||||
func operateStatusByFlag(enabled bool) int64 {
|
||||
if enabled {
|
||||
return operateStatusYes
|
||||
}
|
||||
return operateStatusNo
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user