Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b80b32e4 | ||
|
|
e9f1b8b4c3 | ||
|
|
e7179da072 | ||
|
|
85c0071fd8 |
+17
@@ -0,0 +1,17 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.build/
|
||||
.local/
|
||||
*_test.go
|
||||
*.key
|
||||
*.pem
|
||||
*.tar.gz
|
||||
*.log
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
composed-anvil-*.json
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.sh
|
||||
@@ -0,0 +1,12 @@
|
||||
# 花魁 NET 官网后端
|
||||
|
||||
遵守 `/home/zhufei/code/AGENTS.md` 和花魁主入口 `/home/zhufei/code/hkdj-server/AGENTS.md`。项目身份 HKDJ / JHA-216,本仓库为独立官网后端 `hkdj-web-server`,不能使用原花魁短剧自动部署路线。
|
||||
|
||||
- 来源:黄果 NET `hgdjnet-server` 的 `9bfbafe10d417deea959bd53117bc88a10e60a83`。
|
||||
- 开发分支 `feat/net-import`;空仓库基线 `f7534bf`。测试交付版本、构建和接口证据见 `docs/测试交付记录-20261010.md`;不以本地构建成功冒充部署成功。
|
||||
- 测试目录 `/server/hkdj_net`,App/Web API 30331/30332;前端后台/H5 预留30333/30334,未部署页面产物。前端代码仅联调与只读核验。
|
||||
- 使用独立 `HKDJNET_*` 凭据、业务/日志库与缓存空间。黄果、现有花魁短剧数据和会话均不复用。
|
||||
- 本人2026-10-10已批准首次测试初始化;准确方案见 `deploy/test/index-plan.json`,SHA-256 `3f56441d0c496a9817c5d2f269084eab8450a2d73bcaec9f45a415cbd1751541`。只执行清单中的目标/keys/顺序/选项,改变方案需重新批准。部署守卫键 `hkdjnet` 对应独立Jenkins“花魁NET”,不套用原短剧 `hkdj` 路线。
|
||||
- 本地测试、构建产物及 `.local/` 审阅工具不得暂存或提交;不维护 Swagger。
|
||||
- 未配置的广告、媒资拉取和统计上报保持关闭;Web `mediaJobsEnabled=false`,不自动拉取黄果媒资或执行旧赤果回流。嘉华媒体库接入需另行核对花魁产品身份和接口。
|
||||
- 用户另行批准的空ES主分片恢复已执行一次并核验成功;该批准不能用于重复强制分配或其他索引。
|
||||
@@ -0,0 +1,12 @@
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help app web check
|
||||
|
||||
help:
|
||||
@printf '%s\n' 'make app / make web: build the selected backend' 'make check: compile both backends'
|
||||
|
||||
app web:
|
||||
./scripts/build.sh $@
|
||||
|
||||
check:
|
||||
./scripts/build.sh app
|
||||
./scripts/build.sh web
|
||||
@@ -0,0 +1,54 @@
|
||||
# 花魁 NET 官网后端
|
||||
|
||||
项目身份:HKDJ / 花魁短剧 / JHA-216。独立仓库 `hkdj-web-server`,不替代现有 `hkdj-server`。
|
||||
|
||||
复刻来源为黄果 NET 后端 `jha-1045-hgdjnet-server` 的 `main` 提交 `9bfbafe10d417deea959bd53117bc88a10e60a83`。首次初始化已按本人批准的准确索引方案适配花魁;MongoDB索引定义保持源项目原样,ES使用花魁独立名称。准确版本、构建/运行及接口证据见[测试交付记录](docs/测试交付记录-20261010.md)。
|
||||
|
||||
## 服务划分
|
||||
|
||||
| 服务 | 代码/责任 | 测试端口 | 目录 |
|
||||
| --- | --- | --- | --- |
|
||||
| H5 业务 API | 后端 `app` | 30331 | `/server/hkdj_net/app` |
|
||||
| 后台管理 API | 后端 `web` | 30332 | `/server/hkdj_net/web` |
|
||||
| 后台前端 | 前端静态页面,由 Nginx 提供入口 | 30333 | `/server/hkdj_net/houtai/dist` |
|
||||
| H5 前端 | 前端独立 H5/SSR 服务 | 30334 | `/server/hkdj_net/h5` |
|
||||
|
||||
2026-10-10 部署前已只读确认四个端口未监听、无 Nginx/Supervisor 配置引用。App/Web按测试部署守卫逐组件安装;前端两个入口保持预留。后台、H5 两个前端服务分别交付;不复用黄果的服务目录、前端产物、监听端口或业务数据库。
|
||||
|
||||
## 配置与构建
|
||||
|
||||
App/Web 配置只包含非敏感字段和 `${HKDJNET_*}` 环境变量。凭据由权限 600 的独立 `.env` 加载,不能提交。运行配置与测试模板见 `app/conf/app.yaml`、`web/conf/web.yaml`、`deploy/test/`。
|
||||
|
||||
```bash
|
||||
make app
|
||||
make web
|
||||
```
|
||||
|
||||
构建脚本通过工作区 Go 全局锁,产物写入忽略目录 `.build/`。业务包保留 NET 的 App/Web 能力;不生成、构建或部署 Swagger。
|
||||
|
||||
运行目录分别为 `app`、`web`,从 `conf/app.yaml` 或 `conf/web.yaml` 加载配置;缺少必要环境变量会停止。App 健康检查仍为 `POST /api/app/health/ping`,认证标识和签名密钥改为环境变量 `HKDJNET_HEALTH_GROUP_ID`、`HKDJNET_HEALTH_SECRET`,不沿用黄果值。H5播放票据使用本项目 `tsAuthKey`,不同项目票据互不通用。
|
||||
|
||||
IndexNow 在 Web 运行目录的 `conf/web.yaml` 中直接配置,默认留空且外部推送关闭:
|
||||
|
||||
```yaml
|
||||
indexNow:
|
||||
key: ""
|
||||
```
|
||||
|
||||
将 `indexNow.key` 填为本站的验证 key,并在 SEO 域名下提供同名 `{key}.txt` 文件,文件内容为该 key。SEO 域名优先取系统配置 `foreverUrlAddr`,为空时回退到 `webAddr`。修改 YAML 后需重启 Web 服务。不再读取 `HKDJNET_INDEXNOW_KEY` 环境变量,原环境变量的值需迁移到此配置项;留空或省略该项不影响启动,但无法提交 IndexNow。实际外部推送仍要求代码中的 `seoPushEnabled=true` 且 `runMod=prod`。
|
||||
|
||||
M3U8 解密密钥从各服务运行目录下的文件读取:普通密钥 `conf/m3u8.key`、新版密钥 `conf/newm3u8.key`、老司机密钥 `conf/lsjm3u8.key`。文件内容必须是原始 16 字节二进制密钥,不是十六进制文本,不添加换行;不再读取对应的 `HKDJNET_*M3U8_KEY_HEX` 环境变量。App/Web 的 `conf` 目录均需按需配置本项目密钥文件,权限设为 600,不提交仓库。文件缺失、不可读或长度错误时,密钥接口返回错误。
|
||||
|
||||
## 数据与外部服务
|
||||
|
||||
使用独立测试业务库 `hkdj_net`、日志库 `hkdj_net_log`、Redis DB24 和花魁搜索索引。已完成206项MongoDB索引请求和ES映射回读核验。本人已于2026-10-10批准独立测试库首次初始化;准确keys、顺序和unique/TTL/partial选项见 `deploy/test/index-plan.json`,清单SHA-256为 `3f56441d0c496a9817c5d2f269084eab8450a2d73bcaec9f45a415cbd1751541`。改变该准确方案需重新确认。
|
||||
|
||||
支付、短信、媒资、广告、统计上报和内容回流的黄果凭据没有复制。测试模板中的可选外部接入地址留空;按花魁实际产品标识和本项目凭据另行接入。花魁所属媒体库为嘉华,不能沿用黄果独立媒体库。支付与外部内容发布不在本次验证范围内。
|
||||
|
||||
未配置广告和媒资地址时跳过拉取;Web `mediaJobsEnabled=false`,媒资同步、封面/水印、AI回流等任务不注册,旧赤果自动回流已移除。未配置统计上报时不启动其定时任务。测试库没有迁移黄果内容,播放功能尚待嘉华媒资接入。
|
||||
|
||||
## 分支与交付状态
|
||||
|
||||
空仓库本地基线为 `f7534bf`;开发分支 `feat/net-import`。开发分支经检查后提交推送,并集成到独立`test`分支;Jenkins任务“花魁NET”只部署此仓库的准确`test`版本。未初始化正式环境,未发布生产版本。
|
||||
|
||||
Web、App 对接说明分别见[后台接入](docs/Web-后台接入.md)、[H5接入](docs/App-H5接入.md)。前端30333/30334尚未监听,Nginx配置仅为交接模板。
|
||||
@@ -0,0 +1,48 @@
|
||||
package appinfoapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/appser"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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,84 @@
|
||||
package authapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/service/authser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/useragent"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func requestUA(ctx *gin.Context) useragent.UA {
|
||||
ua, _ := common.GetUA(ctx)
|
||||
return ua
|
||||
}
|
||||
|
||||
type updateProfileReq struct {
|
||||
NickName *string `json:"nickName"` // 新昵称(可选)
|
||||
Avatar *string `json:"avatar"` // 新头像 URL(可选)
|
||||
Introduction *string `json:"introduction"` // 简介(可选)
|
||||
PrivacyMode *bool `json:"privacyMode"` // 隐私模式:true-开启,false-关闭(可选)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改用户资料
|
||||
// @Tags 用户认证
|
||||
// @Summary 修改用户资料(昵称 / 头像 / 简介 / 隐私模式,可任意组合)
|
||||
// @Description 按请求头 X-User-Agent 中的 devID 识别用户;nickName、avatar、introduction、privacyMode 均为可选,至少提供一项。
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body updateProfileReq true "修改资料参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/auth/profile [POST]
|
||||
func UpdateProfile(ctx *gin.Context) {
|
||||
var req updateProfileReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
userId := common.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
common.ServerJSON(ctx, srvdef.InvalidToken, "请先登录")
|
||||
return
|
||||
}
|
||||
serr := authser.UpdateProfile(ctx, &authser.UpdateProfileReq{
|
||||
UserId: userId,
|
||||
NickName: req.NickName,
|
||||
Avatar: req.Avatar,
|
||||
Introduction: req.Introduction,
|
||||
PrivacyMode: req.PrivacyMode,
|
||||
})
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, "")
|
||||
}
|
||||
|
||||
// GetInfo 获取当前用户信息
|
||||
// @Tags 用户认证
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 按请求头 X-User-Agent 中的 devID 识别用户,返回最新用户信息,包含金币余额、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, "请先登录")
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package coinapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hkdj-web-server/app/service/coinser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type buyCoinReq struct {
|
||||
PackageId int64 `json:"packageId" binding:"required"` // 购买套餐ID(金币充值)
|
||||
PayMode string `json:"payMode" binding:"required"` // 支付方式:alipay/wechat
|
||||
}
|
||||
|
||||
// BuyCoin 购买金币(发起第三方支付)
|
||||
// @Tags 金币
|
||||
// @Summary 购买金币
|
||||
// @Description 根据指定金币套餐发起第三方支付,返回支付链接
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body buyCoinReq true "购买参数"
|
||||
// @Success 200 {object} coinser.BuyCoinResp "支付链接"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/coin/buy [POST]
|
||||
func BuyCoin(ctx *gin.Context) {
|
||||
var req buyCoinReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
sysType := ctx.GetString(consts.CtxSysType)
|
||||
devType := ctx.GetString(consts.CtxDevType)
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
|
||||
user, serr := coinser.GetUserInfo(ctx, userId)
|
||||
if serr != nil || user == nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrUserNotExsit, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, serr := coinser.CreateCoinOrder(ctx, userId, user.NickName, req.PackageId, req.PayMode, sysType, devType, ip, user.DistrictCode)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/commentser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SendComment 发布评论
|
||||
// @Tags 评论APP
|
||||
// @Summary 发布评论 --by 王鸣
|
||||
// @Description 发布短剧、短剧剧集或视频评论,parentsId为0发布一级评论,大于0发布二级评论,replyCommentId表示实际回复的评论,objectType 10-短剧 11-短剧剧集 20-视频
|
||||
// @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-短剧剧集 20-视频
|
||||
// @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,63 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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(),
|
||||
}
|
||||
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 (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/contactusser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/feedbackser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/healthser"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/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,305 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/commentser"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/app/service/translationser"
|
||||
"hkdj-web-server/app/service/videoser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
v10 "hkdj-web-server/pub/common/v10"
|
||||
"hkdj-web-server/pub/models/videoinformationmod"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// VideoTagApi 标签搜索
|
||||
// @Tags 标签搜索
|
||||
// @Summary 标签搜索
|
||||
// @Description 标签搜索
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {object} proto.HomeHtmlData "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /richtext/:id [get]
|
||||
func RichtextApi(c *gin.Context) {
|
||||
data := proto.RichTextHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
richtextStr := c.Param("id")
|
||||
richtextId, err := strconv.ParseInt(richtextStr, 10, 64)
|
||||
if err != nil || richtextId < 1 {
|
||||
common.ServerJSON(c, srvdef.NotFound, "")
|
||||
return
|
||||
}
|
||||
richtextDate, serr := videoser.RichtextData(c, richtextId)
|
||||
if serr != nil {
|
||||
common.ServerJSON(c, serr.Code, "richtext param error")
|
||||
return
|
||||
}
|
||||
data.CompletePath = fmt.Sprintf("/richtext/%d", richtextId)
|
||||
data.RichTextInfo = richtextDate
|
||||
|
||||
common.JSONWriterCache(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// PageNotFound 404
|
||||
// @Tags 404
|
||||
// @Summary 404
|
||||
// @Description 404
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body string true "参数列表"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Failure 400 {string} string "获取失败的返回结果"
|
||||
// @Security ApiKeyAuth
|
||||
func PageNotFound(c *gin.Context) {
|
||||
data := proto.NotFoundHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
data.CompletePath = c.Request.URL.Path
|
||||
c.JSON(http.StatusOK, data)
|
||||
//common.ServerJSON(c, srvdef.Success, data)
|
||||
|
||||
//c.HTML(404, "404", gin.H{
|
||||
// "data": data,
|
||||
//})
|
||||
}
|
||||
|
||||
func UrlRedirect(c *gin.Context) {
|
||||
data := proto.UrlRedirectData{
|
||||
Path: c.Request.URL.Path,
|
||||
CompletePath: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
url := c.Query("url")
|
||||
data.UrlRedirect = url
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
}
|
||||
|
||||
func GetTagCategoryCache(ctx *gin.Context) {
|
||||
data := configser.GetVideoTagCategoryDataList()
|
||||
common.ServerJSON(ctx, srvdef.Success, data)
|
||||
}
|
||||
|
||||
// VideoDetailInfo 新版视频详情页
|
||||
func VideoDetailInfo(c *gin.Context) {
|
||||
data := proto.VideoDetailsHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
ua := ""
|
||||
if v, b := c.Get(consts.CtxUAH5); b {
|
||||
ua = v.(string)
|
||||
}
|
||||
ip := ipaddr.ResolveClientIP(c)
|
||||
idStr := c.Param("id")
|
||||
videoId, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil || videoId < 1 {
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
return
|
||||
}
|
||||
renderVideoDetailPage(c, data, videoId, ip, ua)
|
||||
}
|
||||
|
||||
func renderVideoDetailPage(c *gin.Context, data proto.VideoDetailsHtmlData, videoId int64, ip, ua string) {
|
||||
videoDate, serr := videoser.VideoDetails(c, videoId, common.GetUserId(c), ip, ua)
|
||||
if serr != nil {
|
||||
if c.Query("format") == "json" {
|
||||
common.ServerJSON(c, srvdef.ErrNoData, data)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
return
|
||||
}
|
||||
data.Video = videoDate
|
||||
data.Path = fmt.Sprintf("/videodetail/%d/", videoId)
|
||||
|
||||
data.Breadcrumbs = []proto.BreadcrumbItem{
|
||||
{Name: "首页", Url: "/"},
|
||||
{Name: "视频", Url: "/videoList"},
|
||||
}
|
||||
if len(videoDate.Categorys) > 0 {
|
||||
cat := videoDate.Categorys[0]
|
||||
data.Breadcrumbs = append(data.Breadcrumbs, proto.BreadcrumbItem{
|
||||
Name: cat.Name,
|
||||
Url: fmt.Sprintf("/category/%d/", cat.Id),
|
||||
})
|
||||
}
|
||||
data.Breadcrumbs = append(data.Breadcrumbs, proto.BreadcrumbItem{
|
||||
Name: videoDate.Title,
|
||||
Url: "",
|
||||
})
|
||||
|
||||
switch videoDate.Type {
|
||||
case proto.VideoTypeVideo, proto.VideoTypeVideoV2:
|
||||
comment, commentErr := commentser.GetCommentList(c, videoId, commentser.ObjectTypeVideo, 1)
|
||||
data.CommentList = proto.GetCommentListResp{
|
||||
ObjectId: videoId,
|
||||
TotalCount: 0,
|
||||
PageSize: 20,
|
||||
PageNum: 1,
|
||||
List: make([]proto.CommentInfo, 0),
|
||||
}
|
||||
if commentErr == nil {
|
||||
data.CommentList = *comment
|
||||
}
|
||||
if c.Query("format") == "json" {
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "videoDetail", gin.H{
|
||||
"data": data,
|
||||
})
|
||||
default:
|
||||
if c.Query("format") == "json" {
|
||||
common.ServerJSON(c, srvdef.ErrNoData, data)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
}
|
||||
}
|
||||
|
||||
// VideoDetailPage 视频详情预览入口(不走模板缓存)
|
||||
func VideoDetailPage(c *gin.Context) {
|
||||
id := c.DefaultQuery("id", "")
|
||||
if id == "" {
|
||||
c.HTML(http.StatusBadRequest, "404", gin.H{"data": proto.VideoDetailsHtmlData{}})
|
||||
return
|
||||
}
|
||||
c.Params = gin.Params{{Key: "id", Value: id}}
|
||||
c.Request.URL.Path = fmt.Sprintf("/videoDetail?id=%s", id)
|
||||
VideoDetailInfo(c)
|
||||
}
|
||||
|
||||
// VideoListPage 视频列表预览入口(不走模板缓存)
|
||||
func VideoListPage(c *gin.Context) {
|
||||
id := c.DefaultQuery("id", "0")
|
||||
sort := c.DefaultQuery("sort", "1")
|
||||
page := c.DefaultQuery("page", "1")
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: id},
|
||||
{Key: "sortType", Value: sort},
|
||||
{Key: "page", Value: page},
|
||||
}
|
||||
c.Request.URL.Path = fmt.Sprintf("/videoList?id=%s&sort=%s&page=%s", id, sort, page)
|
||||
VideoCategoryInfo(c)
|
||||
}
|
||||
|
||||
// VideoCategoryInfo 新版视频列表页
|
||||
func VideoCategoryInfo(c *gin.Context) {
|
||||
data := proto.VideoTopicHtmlData{
|
||||
Path: c.Request.URL.Path,
|
||||
Translations: translationser.GetTranslationData(),
|
||||
BottonTranslations: translationser.GetTranslationBottomList(),
|
||||
Config: configser.GetConfigInfo(),
|
||||
Advitise: configser.GetAdvitise(),
|
||||
}
|
||||
topicStr := c.Param("id")
|
||||
sortTypeStr := c.Param("sortType")
|
||||
pageStr := c.Param("page")
|
||||
|
||||
pageNum, err := strconv.ParseInt(pageStr, 10, 64)
|
||||
if err != nil || pageNum < 1 {
|
||||
pageNum = 1
|
||||
}
|
||||
sortType, err := strconv.ParseInt(sortTypeStr, 10, 64)
|
||||
if err != nil || sortType < 1 {
|
||||
sortType = 0
|
||||
}
|
||||
|
||||
if topicStr == "" {
|
||||
renderVideoListPage(c, data, "0", 0, sortType, pageNum)
|
||||
return
|
||||
}
|
||||
|
||||
matche := v10.ParamIsNumber(topicStr)
|
||||
if matche {
|
||||
publish, err := strconv.ParseInt(topicStr, 10, 64)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
return
|
||||
}
|
||||
if publish == 0 {
|
||||
renderVideoListPage(c, data, "0", 0, sortType, pageNum)
|
||||
return
|
||||
}
|
||||
if publish < 1 {
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
return
|
||||
}
|
||||
sort := videoTopicSortType(sortType)
|
||||
newPath := fmt.Sprintf("/videocategory/%d/%d/%d/", publish, sort, pageNum)
|
||||
c.Redirect(http.StatusMovedPermanently, newPath)
|
||||
return
|
||||
}
|
||||
|
||||
topicId, err2 := strconv.ParseInt(topicStr, 10, 64)
|
||||
if err2 != nil || topicId < 1 {
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
return
|
||||
}
|
||||
topicSegment := topicStr
|
||||
if topicSegment == "" {
|
||||
topicSegment = "0"
|
||||
}
|
||||
renderVideoListPage(c, data, topicSegment, topicId, sortType, pageNum)
|
||||
}
|
||||
|
||||
func videoTopicSortType(sortType int64) int64 {
|
||||
if sortType < 1 {
|
||||
return int64(videoinformationmod.VideoSortType_New)
|
||||
}
|
||||
return sortType
|
||||
}
|
||||
|
||||
func renderVideoListPage(c *gin.Context, data proto.VideoTopicHtmlData, topicSegment string, topicId, sortType, pageNum int64) {
|
||||
sort := videoTopicSortType(sortType)
|
||||
data.CompletePath = fmt.Sprintf("/videocategory/%s/%d/%d/", topicSegment, sort, pageNum)
|
||||
totalSize, videoInfos, _, serr := videoser.VideoTopicData(c, false, topicId, sortType, pageNum)
|
||||
data.VideoInfos = videoInfos
|
||||
data.PageNum = pageNum
|
||||
data.PageSize = pubdef.PageSize
|
||||
data.TotalSize = pubdef.GetTotalPage(totalSize, pubdef.PageSize)
|
||||
if data.VideoInfos == nil {
|
||||
data.VideoInfos = []proto.VideoInfoData{}
|
||||
}
|
||||
if serr != nil {
|
||||
if c.Query("format") == "json" {
|
||||
common.ServerJSON(c, srvdef.ErrNoData, data)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusNotFound, "404", gin.H{"data": data})
|
||||
return
|
||||
}
|
||||
if c.Query("format") == "json" {
|
||||
common.ServerJSON(c, srvdef.Success, data)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "videoList", gin.H{
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package navigationapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/service/navigationser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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,717 @@
|
||||
package playletapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/playletser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/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)
|
||||
}
|
||||
|
||||
// HomeConfigDetailList app端分页查询首页配置详情列表
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询首页配置详情列表--by 王鸣
|
||||
// @Description 根据首页配置ID或slug分页查询短剧、视频或题材;homeConfigSlug 优先。sortType=5(分类)时配置ID即为分类ID
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.HomeConfigDetailListReq true "app端分页查询首页配置详情列表请求"
|
||||
// @Success 200 {object} proto.HomeConfigDetailListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/home/config/detail/list [get]
|
||||
func HomeConfigDetailList(ctx *gin.Context) {
|
||||
var req proto.HomeConfigDetailListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.HomeConfigDetailList(ctx, &req)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Rank app端分页查询短剧排行榜
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary 短剧排行榜 --by 王鸣
|
||||
// @Description 分页查询已上架短剧排行榜;sortOrder=1按热度值倒序,2按上架时间倒序,3按收藏数倒序
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletRankReq true "分页查询短剧排行榜请求"
|
||||
// @Success 200 {object} proto.PlayletRankResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/rank [get]
|
||||
func Rank(ctx *gin.Context) {
|
||||
var req proto.PlayletRankReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.Rank(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// OperateList app端按运营类型分页查询短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端按运营类型分页查询短剧
|
||||
// @Description 按运营类型查询已上架短剧;operateType=1短剧 2漫剧;filterType=1最近更新(按上架时间) 2当前热播(按收藏数) 3独家原创(原创且按推荐排序)
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletOperateListReq true "按运营类型分页查询短剧请求"
|
||||
// @Success 200 {object} proto.PlayletOperateListResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/operate/list [get]
|
||||
func OperateList(ctx *gin.Context) {
|
||||
var req proto.PlayletOperateListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.OperateList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TodayUpdate app端分页查询今日更新短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端分页查询今日更新短剧 --by 王鸣
|
||||
// @Description 分页查询已上架短剧
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.PlayletTodayUpdateReq true "分页查询今日更新短剧请求"
|
||||
// @Success 200 {object} proto.PlayletTodayUpdateResp{list=[]proto.CommonData} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/playlet/today-update [get]
|
||||
func TodayUpdate(ctx *gin.Context) {
|
||||
var req proto.PlayletTodayUpdateReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := playletser.TodayUpdate(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TagVideoList app端根据标签ID分页查询短剧
|
||||
// @Tags app短剧相关接口
|
||||
// @Summary app端根据标签ID分页查询短剧 --by 王鸣
|
||||
// @Description 根据标签ID或标签slug分页查询已上架短剧;slug非空时优先按slug解析标签ID;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)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
"hkdj-web-server/app/service/playlettagser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/app/service/playletthemeser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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,17 @@
|
||||
package proto
|
||||
|
||||
import "hkdj-web-server/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
|
||||
}
|
||||
|
||||
// AdvitiseData 广告数据,直接使用媒资中心返回的结构
|
||||
type AdvitiseData = proxy.AdvitiseData
|
||||
@@ -0,0 +1,261 @@
|
||||
package proto
|
||||
|
||||
import "hkdj-web-server/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 "hkdj-web-server/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 (
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetCommentListReq 评论列表请求
|
||||
type GetCommentListReq struct {
|
||||
ObjectId int64 `form:"objectId" json:"objectId" binding:"required"` // 对象ID(短剧id 短剧剧集id 视频id)
|
||||
ObjectType int64 `form:"objectType" json:"objectType" binding:"required"` // 对象类型 10-短剧 11-短剧剧集 20-视频
|
||||
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,视频评论传视频ID
|
||||
ObjectType int64 `json:"objectType"` // 对象类型 10-短剧 11-短剧剧集 20-视频
|
||||
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-短剧剧集 20-视频
|
||||
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,122 @@
|
||||
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"` // 女优通用背景图
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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,5 @@
|
||||
package proto
|
||||
|
||||
type FindVideoData struct {
|
||||
RichText []RichTextInfo `json:"richText" form:"richText"` // 富文本内容
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package proto
|
||||
|
||||
type HealthCheckRequest struct {
|
||||
GroupID string `json:"groupId" binding:"required"`
|
||||
Timestamp int64 `json:"timestamp" binding:"required"`
|
||||
Sign string `json:"sign" binding:"required"`
|
||||
}
|
||||
|
||||
type ComponentStatus struct {
|
||||
Status string `json:"status"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Master string `json:"master,omitempty"`
|
||||
}
|
||||
|
||||
type HealthCPUMetrics struct {
|
||||
Cores int `json:"cores"`
|
||||
Used float64 `json:"used"`
|
||||
}
|
||||
|
||||
type HealthMemoryMetrics struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type HealthCheckData struct {
|
||||
ServerName string `json:"serverName,omitempty"`
|
||||
ServerIP string `json:"serverIp,omitempty"`
|
||||
CPU HealthCPUMetrics `json:"cpu"`
|
||||
Memory HealthMemoryMetrics `json:"memory"`
|
||||
Disk HealthMemoryMetrics `json:"disk"`
|
||||
Mysql *ComponentStatus `json:"mysql,omitempty"`
|
||||
Mongodb *ComponentStatus `json:"mongodb,omitempty"`
|
||||
Redis *ComponentStatus `json:"redis,omitempty"`
|
||||
Elasticsearch *ComponentStatus `json:"elasticsearch,omitempty"`
|
||||
Kafka *ComponentStatus `json:"kafka,omitempty"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
@@ -0,0 +1,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,617 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HomePlayletResponse 首页响应
|
||||
type HomePlayletResponse struct {
|
||||
HomeBannerData []*CommonData `json:"homeBannerData"` // 首页banner短剧信息列表
|
||||
HomeConfigData []*HomeConfigData `json:"homeConfigData"` // 首页配置集合
|
||||
Announcements []*AnnouncementItem `json:"announcements"` // 启用中的公告,来自独立缓存
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
|
||||
}
|
||||
|
||||
// HomeConfigData 首页配置数据
|
||||
type HomeConfigData struct {
|
||||
Id int64 `json:"id"` // ID
|
||||
Name string `json:"name"` // 名称
|
||||
Slug string `json:"slug"` // slug,H5 更多页优先用它
|
||||
Desc string `json:"desc"` // 描述
|
||||
Type int32 `json:"type"` // 类型 1-短剧 2-视频 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时返回
|
||||
VideoList []*VideoItemResp `json:"videoList"` // 视频集合,type=2时返回
|
||||
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信息
|
||||
}
|
||||
|
||||
// HomeConfigDetailListReq 首页配置详情分页请求
|
||||
type HomeConfigDetailListReq struct {
|
||||
pubdef.Page
|
||||
HomeConfigId int64 `form:"homeConfigId" json:"homeConfigId"` // 首页配置ID;sortType=5(分类)时即分类ID。与 homeConfigSlug 二选一
|
||||
HomeConfigSlug string `form:"homeConfigSlug" json:"homeConfigSlug"` // 首页配置 slug,非空时优先按 slug 查询
|
||||
OperateType int32 `form:"operateType" json:"operateType"` // type=1时生效,运营类型 0-全部 1-短剧 2-漫剧
|
||||
ThemeId int64 `form:"themeId" json:"themeId"` // type=1时生效,题材ID
|
||||
ThemeSlug string `form:"themeSlug" json:"themeSlug"` // type=1时生效,题材slug,非空时优先按slug解析题材ID
|
||||
MediaType int64 `form:"mediaType" json:"mediaType"` // type=2时生效,视频类型 0-全部 1-影集 2-动画
|
||||
TopicId int64 `form:"topicId" json:"topicId"` // type=2时生效,视频题材ID,对应video_topic
|
||||
TimeRange int64 `form:"timeRange" json:"timeRange"` // type=1/type=2时生效,时间范围 0-综合 1-本周 2-本月
|
||||
}
|
||||
|
||||
// HomeConfigDetailListResp 首页配置详情分页响应
|
||||
type HomeConfigDetailListResp struct {
|
||||
HomeConfigId int64 `json:"homeConfigId"` // 首页配置ID
|
||||
HomeConfigSlug string `json:"homeConfigSlug"` // 首页配置 slug
|
||||
Name string `json:"name"` // 名称
|
||||
Desc string `json:"desc"` // 描述
|
||||
Type int32 `json:"type"` // 类型 1-短剧 2-视频 3-题材
|
||||
SortType int32 `json:"sortType"` // 排序字段 1-推荐 2-热播 3-最近更新 4-新片预告 5-分类
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []*CommonData `json:"list"` // 短剧集合,type=1时返回
|
||||
VideoList []*VideoItemResp `json:"videoList"` // 视频集合,type=2时返回
|
||||
ThemeList []*HomeConfigThemeData `json:"themeList"` // 题材集合,type=3时返回
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告
|
||||
SeoInfo pubdef.SeoValue `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
|
||||
ThemeId int64 `form:"themeId" json:"themeId"` // 题材ID
|
||||
ThemeSlug string `form:"themeSlug" json:"themeSlug"` // 题材slug,非空时优先按slug解析题材ID
|
||||
UpdateStatus int `form:"updateStatus" json:"updateStatus"` // 更新状态 0-全部 1-连载中 2-已完结
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序 1-最新 2-最热 3-收藏最多
|
||||
}
|
||||
|
||||
// PlayletAllListResp 全部短剧分页响应
|
||||
type PlayletAllListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息
|
||||
}
|
||||
|
||||
// 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"` // 标题
|
||||
PlayName string `json:"playName"` // 剧名
|
||||
Description string `json:"description"` // SEO Description
|
||||
Slug string `json:"slug"` // slug
|
||||
Desc string `json:"desc"` // 简介
|
||||
Anchor string `json:"anchor"` // 作者/演员
|
||||
Cover string `json:"cover"` // 竖版封面,已拼接图片域名
|
||||
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相关信息
|
||||
}
|
||||
|
||||
// PlayletRankReq 短剧排行榜请求
|
||||
type PlayletRankReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
SortOrder int64 `form:"sortOrder" json:"sortOrder"` // 排序 1-热播榜,按热度值倒序 2-新剧榜,按上架时间倒序 3-追剧榜,按收藏数倒序
|
||||
}
|
||||
|
||||
// PlayletRankResp 短剧排行榜响应
|
||||
type PlayletRankResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
|
||||
}
|
||||
|
||||
// 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信息
|
||||
}
|
||||
|
||||
// PlayletOperateListReq 按运营类型分页查询短剧请求
|
||||
type PlayletOperateListReq struct {
|
||||
pubdef.Page
|
||||
OperateType int32 `form:"operateType" json:"operateType"` // 运营类型 1-短剧 2-漫剧
|
||||
FilterType int64 `form:"filterType" json:"filterType"` // 筛选类型 1-最近更新 2-当前热播 3-独家原创
|
||||
}
|
||||
|
||||
// PlayletOperateListResp 按运营类型分页查询短剧响应
|
||||
type PlayletOperateListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
OperateType int32 `json:"operateType"` // 运营类型 1-短剧 2-漫剧
|
||||
FilterType int64 `json:"filterType"` // 筛选类型 1-最近更新 2-当前热播 3-独家原创
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
}
|
||||
|
||||
// PlayletTodayUpdateReq 今日更新短剧请求
|
||||
type PlayletTodayUpdateReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// PlayletTodayUpdateResp 今日更新短剧响应
|
||||
type PlayletTodayUpdateResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []CommonData `json:"list"` // 短剧集合
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息(短剧列表页SEO)
|
||||
}
|
||||
|
||||
// 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"` // 短剧点赞数
|
||||
}
|
||||
|
||||
// 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"` // 标题
|
||||
PlayName string `json:"playName"` // 剧名
|
||||
Slug string `json:"slug"` // slug
|
||||
Cover string `json:"cover"` // 封面,已拼接图片域名
|
||||
ThemeIds []int64 `json:"themeIds"` // 题材ID集合
|
||||
ThemeNames []string `json:"themeNames"` // 题材名称集合
|
||||
ThemeSlugs []string `json:"themeSlugs"` // 题材slug集合
|
||||
TagIds []int64 `json:"tagIds"` // 标签ID集合
|
||||
TagNames []string `json:"tagNames"` // 标签名称集合
|
||||
TagSlugs []string `json:"tagSlugs"` // 标签slug集合
|
||||
TotalEpisode int64 `json:"totalEpisode"` // 总集数
|
||||
UpdateStatus int `json:"updateStatus"` // 更新状态 1-连载中 2-已完结
|
||||
CountBrowse int64 `json:"countBrowse"` // 观看量
|
||||
HeatValue int64 `json:"heatValue"` // 热度值
|
||||
CountCollect int64 `json:"countCollect"` // 收藏数
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态,收藏列表固定返回1-已收藏
|
||||
CollectedAt time.Time `json:"collectedAt"` // 收藏时间
|
||||
WatchedCount int64 `json:"watchedCount"` // 当前用户已看集数
|
||||
UnwatchedCount int64 `json:"unwatchedCount"` // 当前用户未看集数,按totalEpisode-watchedCount计算
|
||||
LastChapterId int64 `json:"lastChapterId"` // 最近观看剧集ID,无观看进度时为0
|
||||
LastEpisode int64 `json:"lastEpisode"` // 最近观看集数,无观看进度时为0
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
// PlayletCollectListResp 我的收藏列表响应
|
||||
type PlayletCollectListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总收藏数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []PlayletCollectData `json:"list"` // 收藏短剧集合
|
||||
}
|
||||
|
||||
// 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,90 @@
|
||||
package proto
|
||||
|
||||
import "hkdj-web-server/pub/pubdef"
|
||||
|
||||
// PlayletTopicPageListReq 专题剧单分页请求
|
||||
type PlayletTopicPageListReq struct {
|
||||
pubdef.Page
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序 1-最新 2-最热
|
||||
}
|
||||
|
||||
// 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
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序 1-编辑排序 2-最新 3-最热
|
||||
}
|
||||
|
||||
// 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 "hkdj-web-server/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 "hkdj-web-server/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 "hkdj-web-server/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,428 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"hkdj-web-server/pub/common/netutil"
|
||||
"hkdj-web-server/pub/common/utils"
|
||||
"hkdj-web-server/pub/models/videoinformationmod"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TagsInfoData struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"` // 帖子标题
|
||||
Slug string `json:"slug"` // slug
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Works int64 `json:"works" bson:"works,omitempty"` // 作品数量
|
||||
Images string `json:"images" bson:"images,omitempty"` // 背景
|
||||
}
|
||||
type CollectionInfoData struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"` // 帖子标题
|
||||
}
|
||||
type VideoType int64
|
||||
|
||||
const (
|
||||
VideoTypeVideo VideoType = 0 // 视频(帖子)
|
||||
VideoTypeCollection VideoType = 1 // 合集
|
||||
VideoTypeAdvitise VideoType = 2 // 广告
|
||||
VideoTypeVideoV2 VideoType = 3 // 视频
|
||||
)
|
||||
|
||||
type VideoInfoData struct {
|
||||
Id int64 `json:"id"`
|
||||
Type VideoType `json:"type"` // 类型
|
||||
Title string `json:"title"` // 帖子标题
|
||||
Slug string `json:"slug"` // slug
|
||||
Keyworld []string `json:"keyworld"` // 关键字
|
||||
AddedTime time.Time `json:"addedTime"` // 帖子上架时间
|
||||
CreatorId int64 `json:"creatorId"` // 作者Id
|
||||
CreatorName string `json:"creatorName"` // 作者名称
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
Nodes []videoinformationmod.Node `json:"nodes"` // 节点
|
||||
Video videoinformationmod.VideoInfo `json:"video"` // 视频
|
||||
Tags []TagsInfoData `json:"tags"` // 标签
|
||||
IsHot bool `json:"isHot"` // 是否热门
|
||||
AdvitiseId string `json:"advitiseId"` // 广告Id
|
||||
Link string `json:"link"` // 跳转链接
|
||||
Location int64 `json:"location" bson:"location"` // 位置id
|
||||
CategoryIds []int64 `json:"categoryIds"` // 分类信息
|
||||
Categorys []VideoCategoryInfo `json:"categorys"` // 分类信息
|
||||
TopicIds []int64 `json:"topicIds"` // 专题信息
|
||||
Topics []VideoTopicInfo `json:"topics"` // 专题信息
|
||||
WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
|
||||
ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
|
||||
UpdatedAt time.Time `json:"updatedAt"` //更新时间
|
||||
Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
|
||||
Content string `json:"content"` // 富文本
|
||||
ImgCdn string `json:"imgCdn" bson:"imgCdn"` // 图片CDN
|
||||
PreviewVtt string `json:"previewVtt,omitempty"` // 雪碧图 vtt
|
||||
}
|
||||
|
||||
type VideoHistoryInfo struct {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"` // 帖子标题
|
||||
AddedTime time.Time `json:"addedTime"` // 上架时间
|
||||
}
|
||||
|
||||
type VideoCategoryInfo struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"` // slug
|
||||
ContentType int64 `json:"contentType" bson:"contentType"` // 内容:0: 默认 1:首页 2:今日
|
||||
SortType int64 `json:"sortType" bson:"sortType"` // 排序方式 0:上线时间 1:观看次数 2排序
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Title string `json:"title" bson:"title"` // APP显示
|
||||
Desc string `json:"desc" bson:"desc"` // 描述
|
||||
}
|
||||
|
||||
// 面包屑
|
||||
type BreadcrumbItem struct {
|
||||
Name string `json:"name"` // 名称
|
||||
Url string `json:"url"` // 跳转链接
|
||||
}
|
||||
|
||||
// 详情页导航
|
||||
type DetailNavItem struct {
|
||||
Name string `json:"name"` // 导航名称
|
||||
Url string `json:"url"` // 导航链接
|
||||
}
|
||||
|
||||
type VideoPlayReq struct {
|
||||
VideoId int64 `json:"videoId" form:"videoId"` // 视频ID
|
||||
}
|
||||
|
||||
type VideoPlayResp struct {
|
||||
AuthKey string `json:"authKey"`
|
||||
}
|
||||
|
||||
type VideoCreaterInfo struct {
|
||||
Id int64 `json:"id" `
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"` // slug
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
}
|
||||
|
||||
func VideoInfoToData(videoInfo *videoinformationmod.VideoInformation, imgCdn string) VideoInfoData {
|
||||
t := VideoTypeVideo
|
||||
switch videoInfo.MediaType {
|
||||
case videoinformationmod.MediaType_Video:
|
||||
t = VideoTypeVideo
|
||||
case videoinformationmod.MediaType_Collection:
|
||||
t = VideoTypeCollection
|
||||
case videoinformationmod.MediaType_Advitise:
|
||||
t = VideoTypeAdvitise
|
||||
case videoinformationmod.MediaType_VideoV2:
|
||||
t = VideoTypeVideoV2
|
||||
}
|
||||
videotemp := VideoInfoData{
|
||||
Id: videoInfo.Id,
|
||||
Type: t,
|
||||
Title: videoInfo.Title,
|
||||
Slug: videoInfo.Slug,
|
||||
Keyworld: videoInfo.Keyworld,
|
||||
AddedTime: videoInfo.AddedTime,
|
||||
Desc: videoInfo.Desc,
|
||||
UpdatedAt: videoInfo.PageUpdatedAt,
|
||||
CreatorId: videoInfo.CreatorId,
|
||||
TopicIds: videoInfo.TopicIds,
|
||||
Topics: make([]VideoTopicInfo, 0),
|
||||
CreatorName: videoInfo.CreatorName,
|
||||
CoverImg: netutil.MergeURL(imgCdn, videoInfo.CoverImg),
|
||||
Nodes: make([]videoinformationmod.Node, 0),
|
||||
Video: videoInfo.Video,
|
||||
IsHot: videoInfo.IsHot,
|
||||
CategoryIds: videoInfo.CategoryIds,
|
||||
Categorys: make([]VideoCategoryInfo, 0),
|
||||
Tags: make([]TagsInfoData, 0),
|
||||
ShowTimes: videoInfo.Real.ShowTimes,
|
||||
WatchTimes: videoInfo.Real.WatchTimes,
|
||||
Content: videoInfo.Content,
|
||||
ImgCdn: imgCdn,
|
||||
PreviewVtt: videoInfo.PreviewVtt,
|
||||
}
|
||||
ti := time.Date(2020, 1, 1, 0, 0, 0, 0, utils.LocBJ)
|
||||
if videotemp.UpdatedAt.Before(ti) {
|
||||
videotemp.UpdatedAt = videoInfo.UpdatedAt
|
||||
}
|
||||
for _, nodeInfo := range videoInfo.Nodes {
|
||||
nodeTmp := videoinformationmod.Node{
|
||||
Images: make([]string, 0),
|
||||
Url: nodeInfo.Url,
|
||||
Text: nodeInfo.Text,
|
||||
Videos: nodeInfo.Videos,
|
||||
Title: nodeInfo.Title,
|
||||
}
|
||||
for _, image := range nodeInfo.Images {
|
||||
nodeTmp.Images = append(nodeTmp.Images, netutil.MergeURL(imgCdn, image))
|
||||
}
|
||||
videotemp.Nodes = append(videotemp.Nodes, nodeTmp)
|
||||
}
|
||||
return videotemp
|
||||
}
|
||||
|
||||
// StripH265Url 清空主视频与 nodes 内视频的 H265 地址。
|
||||
func StripH265Url(data *VideoInfoData) {
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
data.Video.H265Url = ""
|
||||
for i := range data.Nodes {
|
||||
for j := range data.Nodes[i].Videos {
|
||||
data.Nodes[i].Videos[j].H265Url = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VideoReq 普通分页查询视频请求
|
||||
type VideoReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// VideoAllListReq 视频分页列表请求
|
||||
type VideoAllListReq struct {
|
||||
pubdef.Page
|
||||
MediaType int64 `form:"mediaType" json:"mediaType"` // 类型 0-全部 1-影集 2-动画
|
||||
TopicId int64 `form:"topicId" json:"topicId"` // 题材ID,对应video_topic
|
||||
TopicSlug string `form:"topicSlug" json:"topicSlug"` // 题材slug,非空时优先按slug解析题材ID
|
||||
SortType int64 `form:"sortType" json:"sortType"` // 排序 1-最新 2-最热 3-收藏最多
|
||||
}
|
||||
|
||||
// VideoItemResp 查询视频列表项
|
||||
type VideoItemResp struct {
|
||||
// id
|
||||
Id int64 `json:"id"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// 标题
|
||||
Title string `json:"title"`
|
||||
// 上架时间
|
||||
AddedTime time.Time `json:"addedTime"`
|
||||
// 作者Id
|
||||
CreatorId int64 `json:"creatorId"`
|
||||
// 作者名称
|
||||
CreatorName string `json:"creatorName"`
|
||||
// 封面
|
||||
CoverImg string `json:"coverImg"`
|
||||
// 关键字
|
||||
Keyworld []string `json:"keyworld"`
|
||||
// 标签
|
||||
Tags []int64 `json:"tags"`
|
||||
// 观看数(真假之和)
|
||||
WatchTimes int64 `json:"watchTimes"`
|
||||
// 影片长度
|
||||
VideoTime int32 `json:"videoTime"`
|
||||
}
|
||||
|
||||
// VideoLikeSetReq 视频点赞设置请求
|
||||
type VideoLikeSetReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 视频ID
|
||||
IsLike *bool `json:"isLike" binding:"required"` // 是否点赞,true-点赞,false-取消点赞
|
||||
}
|
||||
|
||||
// VideoLikeSetResp 视频点赞设置响应
|
||||
type VideoLikeSetResp struct {
|
||||
Id int64 `json:"id"` // 视频ID
|
||||
IsLike int64 `json:"isLike"` // 点赞状态 1-已点赞 2-未点赞
|
||||
CountLike int64 `json:"countLike"` // 视频点赞数
|
||||
}
|
||||
|
||||
// VideoCollectSetReq 视频收藏设置请求
|
||||
type VideoCollectSetReq struct {
|
||||
Id int64 `json:"id" binding:"required"` // 视频ID
|
||||
IsCollect *bool `json:"isCollect" binding:"required"` // 是否收藏,true-收藏,false-取消收藏
|
||||
}
|
||||
|
||||
// VideoCollectSetResp 视频收藏设置响应
|
||||
type VideoCollectSetResp struct {
|
||||
Id int64 `json:"id"` // 视频ID
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
|
||||
CountCollect int64 `json:"countCollect"` // 视频收藏数
|
||||
}
|
||||
|
||||
// VideoHistoryListReq 视频观看历史列表请求
|
||||
type VideoHistoryListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// VideoHistoryData 视频观看历史列表项
|
||||
type VideoHistoryData struct {
|
||||
Id int64 `json:"id"` // 视频ID
|
||||
Slug string `json:"slug"` // slug
|
||||
Title string `json:"title"` // 标题
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
Keyworld []string `json:"keyworld"` // 关键字
|
||||
Tags []int64 `json:"tags"` // 标签
|
||||
WatchTimes int64 `json:"watchTimes"` // 观看数
|
||||
VideoTime int32 `json:"videoTime"` // 影片长度
|
||||
CountLike int64 `json:"countLike"` // 点赞数
|
||||
CountCollect int64 `json:"countCollect"` // 收藏数
|
||||
CountComment int64 `json:"countComment"` // 评论数
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态 1-已收藏 2-未收藏
|
||||
HistoryAt time.Time `json:"historyAt"` // 最近观看时间
|
||||
}
|
||||
|
||||
// VideoHistoryListResp 视频观看历史列表响应
|
||||
type VideoHistoryListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总历史数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []VideoHistoryData `json:"list"` // 观看历史集合
|
||||
}
|
||||
|
||||
// VideoHistoryDeleteReq 删除单条视频观看历史请求
|
||||
type VideoHistoryDeleteReq struct {
|
||||
VideoId int64 `json:"videoId" binding:"required"` // 视频ID
|
||||
}
|
||||
|
||||
// VideoHistoryDeleteResp 删除单条视频观看历史响应
|
||||
type VideoHistoryDeleteResp struct {
|
||||
DeletedCount int64 `json:"deletedCount"` // 删除记录数量
|
||||
}
|
||||
|
||||
// VideoHistoryClearResp 清空视频观看历史响应
|
||||
type VideoHistoryClearResp struct {
|
||||
DeletedCount int64 `json:"deletedCount"` // 删除记录数量
|
||||
}
|
||||
|
||||
// VideoCollectListReq 视频收藏列表请求
|
||||
type VideoCollectListReq struct {
|
||||
// 普通分页参数,pageNum默认1,pageSize默认10、最大100
|
||||
pubdef.Page
|
||||
}
|
||||
|
||||
// VideoCollectData 视频收藏列表项
|
||||
type VideoCollectData struct {
|
||||
Id int64 `json:"id"` // 视频ID
|
||||
Slug string `json:"slug"` // slug
|
||||
Title string `json:"title"` // 标题
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
Keyworld []string `json:"keyworld"` // 关键字
|
||||
Tags []int64 `json:"tags"` // 标签
|
||||
WatchTimes int64 `json:"watchTimes"` // 观看数
|
||||
VideoTime int32 `json:"videoTime"` // 影片长度
|
||||
CountLike int64 `json:"countLike"` // 点赞数
|
||||
CountCollect int64 `json:"countCollect"` // 收藏数
|
||||
CountComment int64 `json:"countComment"` // 评论数
|
||||
IsCollect int64 `json:"isCollect"` // 收藏状态,收藏列表固定返回1-已收藏
|
||||
CollectedAt time.Time `json:"collectedAt"` // 收藏时间
|
||||
}
|
||||
|
||||
// VideoCollectListResp 视频收藏列表响应
|
||||
type VideoCollectListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总收藏数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []VideoCollectData `json:"list"` // 收藏视频集合
|
||||
}
|
||||
|
||||
// VideoAllListItemResp 视频分页列表项
|
||||
type VideoAllListItemResp struct {
|
||||
Id int64 `json:"id"` // id
|
||||
Slug string `json:"slug"` // slug
|
||||
Title string `json:"title"` // 标题
|
||||
MediaType int64 `json:"mediaType"` // 类型 1-影集 2-动画
|
||||
AddedTime time.Time `json:"addedTime"` // 上架时间
|
||||
CreatorId int64 `json:"creatorId"` // 作者Id
|
||||
CreatorName string `json:"creatorName"` // 作者名称
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
Keyworld []string `json:"keyworld"` // 关键字
|
||||
Tags []int64 `json:"tags"` // 标签
|
||||
TopicIds []int64 `json:"topicIds"` // 题材ID集合
|
||||
TopicNames []string `json:"topicNames"` // 题材名称集合
|
||||
Topics []VideoTopicInfo `json:"topics"` // 题材集合
|
||||
WatchTimes int64 `json:"watchTimes"` // 观看数
|
||||
VideoTime int32 `json:"videoTime"` // 影片长度
|
||||
VideoCount int64 `json:"videoCount"` // 视频数量
|
||||
}
|
||||
|
||||
// VideoResp 普通分页查询视频响应
|
||||
type VideoResp struct {
|
||||
// 总共数量
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
// 每页数
|
||||
PageSize int `json:"pageSize"`
|
||||
// 页数
|
||||
PageNum int `json:"pageNum"`
|
||||
// 视频列表
|
||||
List []*VideoItemResp `json:"list"`
|
||||
}
|
||||
|
||||
// VideoAllListResp 视频分页列表响应
|
||||
type VideoAllListResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List []*VideoAllListItemResp `json:"list"` // 视频集合
|
||||
Advitise AdvitiseData `json:"advitise"` // 广告
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"` // 页面SEO信息
|
||||
}
|
||||
|
||||
// VideoInfoReq 查询视频详情请求
|
||||
type VideoInfoReq struct {
|
||||
Id int64 `json:"id" form:"id"` // 视频id
|
||||
Slug string `json:"slug" form:"slug"` // 视频slug,非空时优先按slug解析视频ID
|
||||
}
|
||||
|
||||
// VideoInfoResp 查询视频详情响应
|
||||
type VideoInfoResp struct {
|
||||
// id
|
||||
Id int64 `json:"id"`
|
||||
// slug
|
||||
Slug string `json:"slug"`
|
||||
// 标题
|
||||
Title string `json:"title"`
|
||||
// 封面
|
||||
CoverImg string `json:"coverImg"`
|
||||
// 关键字
|
||||
Keyworld []string `json:"keyworld"`
|
||||
// 标签
|
||||
Tags []int64 `json:"tags"`
|
||||
// 标签名称
|
||||
TagsName []string `json:"tagsName"`
|
||||
// 题材ID集合
|
||||
TopicIds []int64 `json:"topicIds"`
|
||||
// 题材集合
|
||||
Topics []VideoTopicInfo `json:"topics"`
|
||||
// 观看数(真假之和)
|
||||
WatchTimes int64 `json:"watchTimes"`
|
||||
// 点赞数
|
||||
CountLike int64 `json:"countLike"`
|
||||
// 收藏数
|
||||
CountCollect int64 `json:"countCollect"`
|
||||
// 评论数
|
||||
CountComment int64 `json:"countComment"`
|
||||
// 当前用户点赞状态 1-已点赞 2-未点赞
|
||||
IsLike int64 `json:"isLike"`
|
||||
// 当前用户收藏状态 1-已收藏 2-未收藏
|
||||
IsCollect int64 `json:"isCollect"`
|
||||
// 视频
|
||||
Video VideoData `json:"video"`
|
||||
// 广告
|
||||
Advitise AdvitiseData `json:"advitise"`
|
||||
// 页面SEO信息
|
||||
SeoInfo *SeoInfo `json:"seoInfo,omitempty"`
|
||||
}
|
||||
|
||||
type VideoData struct {
|
||||
// 视频ID(视频才会存在)
|
||||
TaskId string `json:"taskId"`
|
||||
// 媒资库ID
|
||||
FileId string `json:"fileId"`
|
||||
// 视频URl
|
||||
Url string `json:"url"`
|
||||
// 视频封面
|
||||
Cover string `json:"cover"`
|
||||
// 分辨率高(视频才会存在)
|
||||
Height int64 `json:"height"`
|
||||
// 分辨率宽(视频才会存在)
|
||||
Width int64 `json:"width"`
|
||||
// 影片长度
|
||||
VideoTime int32 `json:"videoTime"`
|
||||
// 预览MP4文件
|
||||
PreMp4FileNameFs string `json:"preMp4FileNameFs"`
|
||||
// 不加密的预览MP4
|
||||
UnencryptedPreMp4 string `json:"unencryptedPreMp4"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
)
|
||||
|
||||
// VideoTopicListReq 视频题材列表请求
|
||||
type VideoTopicListReq struct {
|
||||
MediaType int64 `form:"mediaType" json:"mediaType"` // 类型 0-全部 1-影集 2-动画
|
||||
}
|
||||
|
||||
// VideoTopicListResp 视频题材列表响应
|
||||
type VideoTopicListResp struct {
|
||||
List []VideoTopicInfo `json:"list"` // 题材集合
|
||||
}
|
||||
|
||||
type VideoTopicInfo struct {
|
||||
Id int64 `json:"id" bson:"_id,omitempty"` // Id
|
||||
Name string `json:"name" bson:"name"` // 专题名称
|
||||
Slug string `json:"slug" bson:"slug"` // slug
|
||||
Title string `json:"title" bson:"title"` // APP显示
|
||||
Desc string `json:"desc" bson:"desc"` // 描述
|
||||
Image string `json:"image" bson:"image"` // 背景
|
||||
VideoCategoryId int64 `json:"videoCategoryId" bson:"videoCategoryId"` // 视频分类id
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Works int64 `json:"works" bson:"works"` // 作品数
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
)
|
||||
|
||||
// HomeHtmlData 首页
|
||||
type HomeHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"` // 请求路径
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
}
|
||||
|
||||
type VideoCategoryData struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"`
|
||||
TopicList []VideoTopicData `json:"topicList" form:"topicList"` // 专题列表
|
||||
}
|
||||
|
||||
type VideoTopicData struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Image string `json:"image" form:"image"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
|
||||
Words int64 `json:"words" form:"words"`
|
||||
ShowType int64 `json:"showType" bson:"showType"` // 展示类型 0=默认 1=横版左右滑动 2=竖版左右滑动
|
||||
TopicType int64 `json:"topicType" bson:"topicType"` // 专题类型 0视频专题 1绑定专题 2绑定标签 3=绑定女优 4=随便看
|
||||
LinkUrl string `json:"linkUrl" bson:"linkUrl"` // 跳转URL
|
||||
}
|
||||
|
||||
type VideoCategoryHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
VideoCategory VideoCategoryData `json:"videoCategory" form:"videoCategory"` // 当前分类数据
|
||||
}
|
||||
|
||||
type VideoTopicHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
VideoInfos []VideoInfoData `json:"videoInfos" form:"videoInfos"` // 视频列表
|
||||
}
|
||||
|
||||
type CategoryVideoListHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
|
||||
CategoryList []VideoCategoryData `json:"categoryList" form:"categoryList"`
|
||||
}
|
||||
|
||||
type VideoCategoryListHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Translation TranslationInfo `json:"translation" form:"translation"` // 当前导航数据
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
|
||||
type VideoDetailsHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
Video VideoInfoData `json:"video" form:"video"` // 视频详情
|
||||
Breadcrumbs []BreadcrumbItem `json:"breadcrumbs" form:"breadcrumbs"` // 面包屑
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
|
||||
}
|
||||
|
||||
type PostDetailsHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
CommentList GetCommentListResp `json:"commentList" form:"commentList"` // 评论
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜标签
|
||||
CreateInfo PostCreateInfo `json:"createInfo" form:"createInfo"` // 作者信息
|
||||
}
|
||||
|
||||
type PostCreateInfo struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Image string `json:"image" form:"image"` //头像
|
||||
Works int64 `json:"works" form:"works"`
|
||||
CreateType int64 `json:"createType"` // 创作者类型 0=普通 1=官方
|
||||
Desc string `json:"desc" form:"desc"` // 作者描述
|
||||
Background string `json:"background"` // 背景
|
||||
}
|
||||
|
||||
// TagHtmlData 首页
|
||||
type TagHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
type VideoTagHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
type VideoTagCategoryHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
AllCategory []VideoTagCategoryData `json:"allCategory" form:"allCategory"` //所有分类
|
||||
|
||||
}
|
||||
type VideoTagCategoryData struct {
|
||||
Id int64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
SeoInfo pubdef.SeoValue `json:"seoInfo" form:"seoInfo"`
|
||||
Image string `json:"image" form:"image"`
|
||||
TotalTags int64 `json:"totalTags" form:"totalTags"` // 总标签数量
|
||||
}
|
||||
type VideoTagCategoryInfoData struct {
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
type VideoHistoryHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
PageNum int64 `json:"pageNum" form:"pageNum"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
TotalSize int64 `json:"totalSize" form:"totalSize"`
|
||||
}
|
||||
|
||||
type RichTextHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
type NotFoundHtmlData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
RichTextInfo RichTextInfo `json:"richTextInfo" form:"richTextInfo"` // 富文本
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
}
|
||||
|
||||
type UrlRedirectData struct {
|
||||
Path string `json:"path"`
|
||||
CompletePath string `json:"completePath" form:"completePath"`
|
||||
Translations AppTranslationInfo `json:"translations" form:"translations"` // 导航
|
||||
BottonTranslations AppTranslationInfo `json:"bottonTranslations" form:"bottonTranslations"` // 底部导航
|
||||
Config ConfigInfo `json:"config" form:"config"` // 配置文件
|
||||
Advitise AdvitiseData `json:"advitise" form:"advitise"` // 广告
|
||||
HotSearch []*HotSearchItem `json:"hotSearch" form:"hotSearch"` // 热搜
|
||||
UrlRedirect string `json:"urlRedirect" form:"urlRedirect"`
|
||||
}
|
||||
|
||||
type ContactUsReq struct {
|
||||
Name string `json:"name" form:"name"` // 姓名
|
||||
Email string `json:"email" form:"email"` // 邮件
|
||||
Content string `json:"content" form:"content"` // 内容
|
||||
CategoryName string `json:"categoryName" form:"categoryName"` // 主旨分类
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package rechargeapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/service/rechargeser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type submitOrderReq struct {
|
||||
PackageId int64 `json:"packageId" binding:"required"` // 购买套餐ID(会员畅享)
|
||||
PayMode string `json:"payMode" binding:"required"` // 支付方式:alipay/wechat
|
||||
}
|
||||
|
||||
// SubmitOrder 发起第三方支付下单
|
||||
// @Tags 充值
|
||||
// @Summary 创建 VIP 充值订单(第三方支付)
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body submitOrderReq true "下单参数"
|
||||
// @Success 200 {object} rechargeser.SubmitOrderResp "支付链接"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/vip/pay [POST]
|
||||
func SubmitOrder(ctx *gin.Context) {
|
||||
var req submitOrderReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
sysType := ctx.GetString(consts.CtxSysType)
|
||||
devType := ctx.GetString(consts.CtxDevType)
|
||||
ip := ipaddr.ResolveClientIP(ctx)
|
||||
|
||||
user, serr := rechargeser.GetUserInfo(ctx, userId)
|
||||
if serr != nil || user == nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrUserNotExsit, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, serr := rechargeser.CreateOrder(ctx, userId, user.NickName, req.PackageId, req.PayMode, sysType, devType, ip, user.DistrictCode)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
type orderHistoryReq struct {
|
||||
Page int64 `json:"page" form:"page"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize"`
|
||||
}
|
||||
|
||||
// GetOrderHistory 查询当前用户 VIP 订单历史
|
||||
// @Tags 充值
|
||||
// @Summary 查询 VIP 充值记录
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码(默认1)"
|
||||
// @Param pageSize query int false "每页数量(默认10)"
|
||||
// @Success 200 {array} vipordermod.VipOrder "订单列表"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/app/vip/orders [GET]
|
||||
func GetOrderHistory(ctx *gin.Context) {
|
||||
var req orderHistoryReq
|
||||
_ = ctx.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
userId := common.GetUserId(ctx)
|
||||
list, total, serr := rechargeser.GetOrderHistory(ctx, userId, req.Page, req.PageSize)
|
||||
if serr != nil {
|
||||
common.ServerJSON(ctx, serr.Code, serr.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, gin.H{"total": total, "list": list})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package rechargepackageapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/service/rechargepackageser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// List 查询购买套餐列表
|
||||
// @Tags 金币与会员
|
||||
// @Summary 查询购买套餐列表 -- by 王鸣
|
||||
// @Description 返回会员畅享和金币充值两个列表,并附带各套餐可用支付方式
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.RechargePackageListResp "成功"
|
||||
// @Router /api/app/recharge/package/list [GET]
|
||||
func List(ctx *gin.Context) {
|
||||
sysType := ctx.GetString(consts.CtxSysType)
|
||||
if sysType == "" {
|
||||
sysType = "android"
|
||||
}
|
||||
resp, err := rechargepackageser.GetRechargePackageList(ctx, sysType)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package statcenterapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/statcenter"
|
||||
"hkdj-web-server/pub/models/cointransmod"
|
||||
"hkdj-web-server/pub/models/usermod"
|
||||
"hkdj-web-server/pub/models/uservisitlog"
|
||||
"hkdj-web-server/pub/models/vipordermod"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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 (
|
||||
"hkdj-web-server/app/service/translationser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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"
|
||||
|
||||
"hkdj-web-server/app/service/uploadser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/usercenterser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/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,246 @@
|
||||
package videoapi
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/videoser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AllList app端分页查询视频列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端分页查询视频列表 --by 王鸣
|
||||
// @Description 分页查询已上架视频,支持类型、题材和排序筛选
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoAllListReq true "app端分页查询视频列表请求"
|
||||
// @Success 200 {object} proto.VideoAllListResp{list=[]proto.VideoAllListItemResp} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/all/list [get]
|
||||
func AllList(ctx *gin.Context) {
|
||||
var req proto.VideoAllListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.VideoAllList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// TopicList app端查询视频题材列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端查询视频题材列表 --by 王鸣
|
||||
// @Description 查询已上架视频题材列表,mediaType=0全部 1影集 2动画
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoTopicListReq true "app端查询视频题材列表请求"
|
||||
// @Success 200 {object} proto.VideoTopicListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/topic/list [get]
|
||||
func TopicList(ctx *gin.Context) {
|
||||
var req proto.VideoTopicListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.VideoTopicList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// List 普通分页查询视频
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 普通分页查询视频 --by 旺鸣
|
||||
// @Description 按上架时间倒序普通分页查询已上架视频
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoReq true "普通分页查询视频请求"
|
||||
// @Success 200 {object} proto.VideoResp{list=[]proto.VideoItemResp} "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/list [get]
|
||||
func List(ctx *gin.Context) {
|
||||
var req proto.VideoReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.VideoList(ctx, &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SetLike app端视频点赞/取消点赞
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端视频点赞/取消点赞 --by 王鸣
|
||||
// @Description 设置当前用户视频点赞状态,点赞数只在状态真实变化时增减
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.VideoLikeSetReq true "视频点赞设置请求"
|
||||
// @Success 200 {object} proto.VideoLikeSetResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/like/set [post]
|
||||
func SetLike(ctx *gin.Context) {
|
||||
var req proto.VideoLikeSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.SetLike(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// SetCollect app端视频收藏/取消收藏
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端视频收藏/取消收藏 --by 王鸣
|
||||
// @Description 设置当前用户视频收藏状态,收藏数只在状态真实变化时增减
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.VideoCollectSetReq true "视频收藏设置请求"
|
||||
// @Success 200 {object} proto.VideoCollectSetResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/collect/set [post]
|
||||
func SetCollect(ctx *gin.Context) {
|
||||
var req proto.VideoCollectSetReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.SetCollect(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 视频相关接口
|
||||
// @Summary app端我的视频观看历史列表 --by 王鸣
|
||||
// @Description 按当前用户最近观看时间倒序分页返回视频观看历史
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoHistoryListReq true "视频观看历史列表请求"
|
||||
// @Success 200 {object} proto.VideoHistoryListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/history/list [get]
|
||||
func HistoryList(ctx *gin.Context) {
|
||||
var req proto.VideoHistoryListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.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 视频相关接口
|
||||
// @Summary app端删除单条视频观看历史 --by 王鸣
|
||||
// @Description 删除当前用户指定视频观看历史,不影响收藏和点赞
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body proto.VideoHistoryDeleteReq true "删除单条视频观看历史请求"
|
||||
// @Success 200 {object} proto.VideoHistoryDeleteResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/history/delete [post]
|
||||
func DeleteHistory(ctx *gin.Context) {
|
||||
var req proto.VideoHistoryDeleteReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.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 视频相关接口
|
||||
// @Summary app端清空我的视频观看历史 --by 王鸣
|
||||
// @Description 清空当前用户全部视频观看历史,不影响收藏和点赞
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} proto.VideoHistoryClearResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/history/clear [post]
|
||||
func ClearHistory(ctx *gin.Context) {
|
||||
resp, err := videoser.ClearHistory(ctx, common.GetUserId(ctx))
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// CollectList app端我的视频收藏列表
|
||||
// @Tags 视频相关接口
|
||||
// @Summary app端我的视频收藏列表 --by 王鸣
|
||||
// @Description 按当前用户收藏时间倒序分页返回视频收藏列表
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoCollectListReq true "视频收藏列表请求"
|
||||
// @Success 200 {object} proto.VideoCollectListResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/collect/list [get]
|
||||
func CollectList(ctx *gin.Context) {
|
||||
var req proto.VideoCollectListReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
resp, err := videoser.CollectList(ctx, common.GetUserId(ctx), &req)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
|
||||
// Detail 查询视频详情
|
||||
// @Tags 视频相关接口
|
||||
// @Summary 查询视频详情 --by 旺鸣
|
||||
// @Description 查询已上架视频详情
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query proto.VideoInfoReq true "查询视频详情请求"
|
||||
// @Success 200 {object} proto.VideoInfoResp "成功"
|
||||
// @Failure 400 {string} string "失败"
|
||||
// @Router /api/app/video/detail [get]
|
||||
func Detail(ctx *gin.Context) {
|
||||
var req proto.VideoInfoReq
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
common.ServerJSON(ctx, srvdef.ErrParamError, nil)
|
||||
return
|
||||
}
|
||||
ua, _ := common.GetUA(ctx)
|
||||
resp, err := videoser.VideoInfo(ctx, &req, common.GetUserId(ctx), ipaddr.ResolveClientIP(ctx), ua.UserAgent)
|
||||
if err != nil {
|
||||
common.ServerJSON(ctx, err.Code, err.Tips)
|
||||
return
|
||||
}
|
||||
common.ServerJSON(ctx, srvdef.Success, resp)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package appg
|
||||
|
||||
import (
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
const EsIndex = "sz"
|
||||
|
||||
var (
|
||||
Conf GlobalCfgEx
|
||||
Cache *cache.Cache
|
||||
DetectorV6 *ipaddr.XDBDetector
|
||||
DetectorV4 *ipaddr.XDBDetector
|
||||
)
|
||||
|
||||
type NameSource struct {
|
||||
FamilyNames []string `yaml:"familyNames"`
|
||||
MiddleNames []string `yaml:"middleNames"`
|
||||
AfterNames []string `yaml:"afterNames"`
|
||||
}
|
||||
type PictureSource struct {
|
||||
Portrait []string `yaml:"portrait"`
|
||||
UserbackGround []string `yaml:"userbackground"`
|
||||
ActressBackground []string `yaml:"actressbackground"`
|
||||
}
|
||||
type GlobalCfgEx struct {
|
||||
Redis struct {
|
||||
Addr string `yaml:"addr"`
|
||||
Db int `yaml:"db"`
|
||||
Passwd string `yaml:"passwd"`
|
||||
Subscribe bool `yaml:"subscribe"`
|
||||
}
|
||||
|
||||
Mongodb struct {
|
||||
Addr string `yaml:"addr"`
|
||||
LogAddr string `yaml:"logaddr"`
|
||||
RecomAddr string `yaml:"recomaddr"`
|
||||
AIOrderCenter string `yaml:"aiordercenter"`
|
||||
}
|
||||
Es struct {
|
||||
Addr string `yaml:"addr"`
|
||||
User string `yaml:"user"`
|
||||
Pass string `yaml:"pass"`
|
||||
} `yaml:"es"`
|
||||
RecomSys struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Addr string `yaml:"addr"`
|
||||
ApiKey string `yaml:"apikey"`
|
||||
} `yaml:"recomsys"`
|
||||
HttpOptions struct {
|
||||
DcCtxTimeOut uint64 `yaml:"dcCtxTimeOut"` //代充http请求 超时ctx取消时间
|
||||
} `yaml:"httpOptions"`
|
||||
Kafka struct {
|
||||
Url string `yaml:"url"`
|
||||
Group string `yaml:"group"`
|
||||
} `yaml:"kafka"`
|
||||
JwtKey string `yaml:"jwtKey"`
|
||||
RunMod string `yaml:"runMod"`
|
||||
ResKey string `yaml:"resKey"`
|
||||
CdnKey string `yaml:"cdnKey"`
|
||||
Pprof bool `yaml:"pprof"`
|
||||
Master bool `yaml:"master"` //是否是主节点
|
||||
Host string `yaml:"host"`
|
||||
ListenAddr string `yaml:"listenAddr"`
|
||||
KfAppId string `yaml:"kfAppId"`
|
||||
KfSecret string `yaml:"kfSecret"`
|
||||
KfApiUrlPrefix string `yaml:"kfApiUrlPrefix"`
|
||||
VersionUrl string `yaml:"versionUrl"`
|
||||
KafkaUrl string `yaml:"kafkaUrl"`
|
||||
CenterDomain string `yaml:"centerDomain"`
|
||||
TgToken string `yaml:"tgToken"`
|
||||
ReplayAttackConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
AppKey string `yaml:"appkey"`
|
||||
GuestKey string `yaml:"guestKey"`
|
||||
} `yaml:"attack"`
|
||||
ReplayH5AttackConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
AppKey string `yaml:"appkey"`
|
||||
GuestKey string `yaml:"guestKey"`
|
||||
} `yaml:"h5attack"`
|
||||
PayCenter struct {
|
||||
Addr string `yaml:"addr"`
|
||||
AppId int64 `yaml:"appId"`
|
||||
Serect string `yaml:"serect"`
|
||||
} `yaml:"payCenter"`
|
||||
AiCenterAppId int64 `yaml:"aiCenterAppId"`
|
||||
AiUndressingCallbackUrl string `yaml:"aiUndressingCallbackUrl"`
|
||||
NavigateUrl string `yaml:"navigateUrl"`
|
||||
NewProductUrl string `yaml:"newProductUrl"`
|
||||
TransactionUrl string `yaml:"transactionUrl"` // 支付下单地址
|
||||
LocalWebUrl string `yaml:"localWebUrl"`
|
||||
JituanProductUrl string `yaml:"jituanProductUrl"`
|
||||
AiImageToVideoUrl string `yaml:"aiImageToVideoUrl"` // AI图生视频请求地址
|
||||
AiTextToImageUrl string `yaml:"aiTextToImageUrl"` // Ai文生图请求地址
|
||||
AiMateH5 string `yaml:"aiMateH5"`
|
||||
OrigDomain string `yaml:"origDomain"` // M3U8下载地址
|
||||
CdnAuthKey string `yaml:"cdnAuthKey"`
|
||||
TsAuthKey string `yaml:"tsAuthKey"`
|
||||
TrustedProxies string `yaml:"trustedProxies"`
|
||||
BucketVideoAddressV2 string `yaml:"bucketVideoAddressV2"`
|
||||
AdCenter struct {
|
||||
ApiDomain string `yaml:"apiDomain"` // api地址 如:https://adopenapi.adcs01.top
|
||||
MerchantCode string `yaml:"merchantCode"` // 商户 code
|
||||
AppCode string `yaml:"appCode"` // 应用 code
|
||||
AesKey string `yaml:"aesKey"` // 数据解密KEY
|
||||
} `yaml:"adCenter"`
|
||||
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,33 @@
|
||||
redis:
|
||||
addr: ${HKDJNET_REDIS_ADDR}
|
||||
db: 24
|
||||
passwd: ${HKDJNET_REDIS_PASSWORD}
|
||||
mongodb:
|
||||
addr: ${HKDJNET_MONGODB_URI}
|
||||
logaddr: ${HKDJNET_LOG_MONGODB_URI}
|
||||
es:
|
||||
addr: ${HKDJNET_ES_URL}
|
||||
user: ${HKDJNET_ES_USERNAME}
|
||||
pass: ${HKDJNET_ES_PASSWORD}
|
||||
jwtKey: ${HKDJNET_APP_JWT_KEY}
|
||||
resKey: ${HKDJNET_RES_KEY}
|
||||
tsAuthKey: ${HKDJNET_TS_AUTH_KEY}
|
||||
runMod: test
|
||||
host: "http://172.104.181.99:30334"
|
||||
listenAddr: "30331"
|
||||
trustedProxies: "127.0.0.1"
|
||||
pprof: false
|
||||
master: true
|
||||
httpOptions:
|
||||
dcCtxTimeOut: 30
|
||||
attack:
|
||||
enable: false
|
||||
h5attack:
|
||||
enable: false
|
||||
tgToken: ""
|
||||
dataReport:
|
||||
url: ""
|
||||
lsjMediaConf:
|
||||
apiUrl: ""
|
||||
adCenter:
|
||||
apiDomain: ""
|
||||
Binary file not shown.
Binary file not shown.
+54
@@ -0,0 +1,54 @@
|
||||
//go:generate mtool replicate-module -m contest
|
||||
package main
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/server"
|
||||
"os"
|
||||
)
|
||||
|
||||
// @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,45 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"hkdj-web-server/app/middleware/reptilerecord"
|
||||
"hkdj-web-server/app/service/authser"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// gin 用户识别中间件:不再校验 token,按 X-User-Agent 中的 devID 识别用户,
|
||||
// 该设备首次访问时自动创建用户。没有 devID 的请求按匿名处理,
|
||||
// 需要用户身份的接口自行返回"请先登录"。
|
||||
func Auth(ctx *gin.Context) {
|
||||
if reptilerecord.IsBot(ctx) {
|
||||
ctx.Set(consts.CtxUserID, pubdef.CrawlerUserId)
|
||||
return
|
||||
}
|
||||
|
||||
ua, err := common.GetUA(ctx)
|
||||
if err != nil || strings.TrimSpace(ua.DevID) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
userId, serr := authser.ResolveDeviceUser(ctx, ua, ipaddr.ResolveClientIP(ctx), ctx.GetHeader("channel"))
|
||||
if serr != nil {
|
||||
if serr.Code == srvdef.AccountErrAccessForbid {
|
||||
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
|
||||
"code": serr.Code,
|
||||
"msg": serr.Code.Msg(),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Auth:ResolveDeviceUser", log.Any("devID", ua.DevID), log.Any("path", ctx.Request.URL.Path), log.E(serr))
|
||||
return
|
||||
}
|
||||
ctx.Set(consts.CtxUserID, userId)
|
||||
}
|
||||
@@ -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"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"net/http"
|
||||
|
||||
"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 (
|
||||
"hkdj-web-server/pub/common/enum/redisconst"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/utils"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"hkdj-web-server/pub/redis"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/enum/redisconst"
|
||||
"hkdj-web-server/pub/common/godef"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/tgbot"
|
||||
"hkdj-web-server/pub/redis"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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 (
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"hkdj-web-server/app/middleware/reptilerecord"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/enum/redisconst"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/useragent"
|
||||
"hkdj-web-server/pub/common/utils"
|
||||
"hkdj-web-server/pub/models/usermod"
|
||||
"hkdj-web-server/pub/models/uservisitlog"
|
||||
"hkdj-web-server/pub/redis"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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 (
|
||||
"hkdj-web-server/app/api/appinfoapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func appInfoRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/applist/list", appinfoapi.QueryAppList)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/authapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func authRouter(r *gin.Engine) {
|
||||
auth := r.Group("/api/app/auth")
|
||||
{
|
||||
auth.GET("/getInfo", authapi.GetInfo)
|
||||
auth.POST("/profile", authapi.UpdateProfile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 "hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/app/api/feedbackapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func feedbackRouter(r *gin.Engine) {
|
||||
r.POST("/api/app/feedback/add", feedbackapi.AddFeedback)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/home"
|
||||
"hkdj-web-server/app/appg"
|
||||
"hkdj-web-server/app/middleware/auth"
|
||||
"hkdj-web-server/app/middleware/cors"
|
||||
"hkdj-web-server/app/middleware/ipblacklist"
|
||||
"hkdj-web-server/app/middleware/ipstat"
|
||||
"hkdj-web-server/app/middleware/reptilerecord"
|
||||
"hkdj-web-server/app/middleware/templatecache"
|
||||
"hkdj-web-server/app/middleware/visit"
|
||||
"hkdj-web-server/pub/common"
|
||||
"hkdj-web-server/pub/common/enum/consts"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/useragent"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeanThompson/ginpprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
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.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", "channel",
|
||||
"sid",
|
||||
"device_brand", "device-brand",
|
||||
"device_model", "device-model",
|
||||
"system_name", "system-name",
|
||||
"system_version", "system-version",
|
||||
useragent.SDKCommonHeader,
|
||||
},
|
||||
ExposedHeaders: []string{"Content-Disposition"},
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/app/api/navigationapi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func navigationRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/navigation/header", navigationapi.HeaderNavigation)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/playletapi"
|
||||
"hkdj-web-server/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("/home/config/detail/list", playletapi.HomeConfigDetailList)
|
||||
group.GET("/all/list", playletapi.AllList)
|
||||
group.GET("/topic/list", playletapi.TopicList)
|
||||
group.GET("/topic/detail", playletapi.TopicDetail)
|
||||
group.GET("/operate/list", playletapi.OperateList)
|
||||
group.GET("/rank", playletapi.Rank)
|
||||
group.GET("/hot/rank/list", playletapi.HotRankList)
|
||||
group.GET("/today-update", playletapi.TodayUpdate)
|
||||
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.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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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 (
|
||||
"hkdj-web-server/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,40 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"hkdj-web-server/app/api/videoapi"
|
||||
"hkdj-web-server/app/middleware/templatecache"
|
||||
"hkdj-web-server/app/service/videoser"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"hkdj-web-server/app/api/home"
|
||||
)
|
||||
|
||||
func videoRouter(r *gin.Engine) {
|
||||
r.GET("/api/app/video/all/list", videoapi.AllList)
|
||||
r.GET("/api/app/video/topic/list", videoapi.TopicList)
|
||||
r.GET("/api/app/video/list", videoapi.List)
|
||||
r.POST("/api/app/video/like/set", videoapi.SetLike)
|
||||
r.POST("/api/app/video/collect/set", videoapi.SetCollect)
|
||||
r.GET("/api/app/video/history/list", videoapi.HistoryList)
|
||||
r.POST("/api/app/video/history/delete", videoapi.DeleteHistory)
|
||||
r.POST("/api/app/video/history/clear", videoapi.ClearHistory)
|
||||
r.GET("/api/app/video/collect/list", videoapi.CollectList)
|
||||
r.GET("/api/app/video/detail", videoapi.Detail)
|
||||
|
||||
r.GET("/tagcategory/cache", templatecache.WithCache(time.Minute*5), home.GetTagCategoryCache)
|
||||
|
||||
r.GET("/richtext/:id/", templatecache.WithCache(time.Minute*1), home.RichtextApi) // 富文本
|
||||
|
||||
r.GET("/h5/m3u8/*key", videoser.GetM3U8Ex)
|
||||
r.GET("/h5/vtt/*key", videoser.GetVTTEx)
|
||||
r.GET("/enkey/", videoser.Enkey)
|
||||
r.GET("/newenkey/", videoser.NewEnkey)
|
||||
r.GET("/lsjenkey", videoser.LsjEnkey)
|
||||
|
||||
r.GET("/urlRedirect/", templatecache.WithCache(time.Minute*1), home.UrlRedirect)
|
||||
|
||||
r.GET("/videoList", home.VideoListPage)
|
||||
r.GET("/videoDetail", home.VideoDetailPage)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hkdj-web-server/app/appg"
|
||||
"hkdj-web-server/app/router"
|
||||
"hkdj-web-server/app/service/appser"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/app/service/playletcache"
|
||||
"hkdj-web-server/app/service/translationser"
|
||||
ipstatsync "hkdj-web-server/app/skd/ipstat"
|
||||
"hkdj-web-server/app/skd/seo"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/srvversion"
|
||||
"hkdj-web-server/pub/common/tgbot"
|
||||
"hkdj-web-server/pub/esmanager"
|
||||
"hkdj-web-server/pub/models"
|
||||
"hkdj-web-server/pub/mongos"
|
||||
"hkdj-web-server/pub/redis"
|
||||
"hkdj-web-server/pub/runtimecfg"
|
||||
"hkdj-web-server/pub/storage/proxy"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
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()
|
||||
if err := initRedis(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := initElasticSearch(); err != nil {
|
||||
log.Error("init ES failed", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
initData()
|
||||
ipstatsync.StartSync()
|
||||
|
||||
initHttpServer()
|
||||
//tgbot.SendToProgram("启动", "app启动完毕")
|
||||
shutdownHandler()
|
||||
}
|
||||
|
||||
func initConfig() error {
|
||||
return runtimecfg.Load("conf/app.yaml", &appg.Conf)
|
||||
}
|
||||
|
||||
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"
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/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,199 @@
|
||||
package appser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/pub/common/http"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/netutil"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"hkdj-web-server/pub/storage/proxy"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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)
|
||||
if strings.TrimSpace(domain) == "" {
|
||||
return
|
||||
}
|
||||
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.AppInfoAList {
|
||||
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.AppInfoBList {
|
||||
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,178 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/useragent"
|
||||
"hkdj-web-server/pub/common/utils"
|
||||
"hkdj-web-server/pub/models/usermod"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
var defaultAvatars = []string{
|
||||
"v3/image/bo/ty/1016/girl1.jpeg",
|
||||
"v3/image/re/9y/1016/girl2.jpeg",
|
||||
"v3/image/wr/1t/1016/girl3.jpeg",
|
||||
"v3/image/8l/26/1016/girl4.jpeg",
|
||||
"v3/image/22/6l/1016/girl5.jpeg",
|
||||
"v3/image/8h/16/1016/girl6.jpeg",
|
||||
"v3/image/wj/15/1016/girl7.jpeg",
|
||||
"v3/image/t2/13/1016/girl8.jpeg",
|
||||
"v3/image/4r/j/1016/girl9.jpeg",
|
||||
"v3/image/16c/9/1016/girl10.jpeg",
|
||||
}
|
||||
|
||||
func randomAvatar() string {
|
||||
idx := time.Now().UnixNano() % int64(len(defaultAvatars))
|
||||
return defaultAvatars[idx]
|
||||
}
|
||||
|
||||
// generateUniqueInviteCode 生成不与已有用户冲突的 12 位邀请码(简化实现)
|
||||
func generateUniqueInviteCode(ctx context.Context) string {
|
||||
return strings.ToUpper(utils.GetRandomLetters(12))
|
||||
}
|
||||
|
||||
// LoginResp 用户信息
|
||||
type LoginResp struct {
|
||||
UserId int64 `json:"userId"`
|
||||
AccountNo string `json:"accountNo"`
|
||||
Email string `json:"email"`
|
||||
NickName string `json:"nickName"`
|
||||
Avatar string `json:"avatar"`
|
||||
Introduction string `json:"introduction"`
|
||||
DevID string `json:"devID"`
|
||||
Mac string `json:"mac"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
InviteCode string `json:"inviteCode"`
|
||||
AgentInviteCode string `json:"agentInviteCode"`
|
||||
Invites int64 `json:"invites"`
|
||||
Status pubdef.UserStatus `json:"status"`
|
||||
Type pubdef.UserType `json:"type"` // 身份类型:0游客 1普通用户
|
||||
LoginType pubdef.LoginType `json:"loginType"` // 登陆方式
|
||||
IsRegister bool `json:"isRegister"` // 是否已注册
|
||||
PrivacyMode bool `json:"privacyMode"` // 隐私模式
|
||||
Score int64 `json:"score"` // 积分
|
||||
Recharge int64 `json:"recharge"` // 金币余额
|
||||
}
|
||||
|
||||
// CrawlerLoginResp 爬虫虚拟游客资料,不对应任何数据库用户。
|
||||
func CrawlerLoginResp() *LoginResp {
|
||||
return &LoginResp{
|
||||
UserId: pubdef.CrawlerUserId,
|
||||
NickName: "游客",
|
||||
Status: pubdef.UserStatusNormal,
|
||||
Type: pubdef.UserTypeGuest,
|
||||
LoginType: pubdef.LoginTypeDevice,
|
||||
IsRegister: false,
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo 获取当前用户的最新信息
|
||||
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
|
||||
user.DevID = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.DevType); v != "" && v != user.Device {
|
||||
setDoc[usermod.FN.User.Device] = v
|
||||
user.Device = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.SysType); v != "" && v != user.SysType {
|
||||
setDoc[usermod.FN.User.SysType] = v
|
||||
user.SysType = v
|
||||
}
|
||||
if v := strings.TrimSpace(ua.Ver); v != "" && v != user.AppVer {
|
||||
setDoc[usermod.FN.User.AppVer] = v
|
||||
user.AppVer = v
|
||||
}
|
||||
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 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
|
||||
}
|
||||
if v := strings.TrimSpace(ua.Ver); v != "" && user.OriVer == "" {
|
||||
setDoc[usermod.FN.User.OriVer] = v
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateProfileReq 修改用户资料的参数(所有字段均为可选)
|
||||
type UpdateProfileReq struct {
|
||||
UserId int64 // 用户ID(按设备ID识别)
|
||||
NickName *string // 新昵称(可选)
|
||||
Avatar *string // 新头像 URL(可选)
|
||||
Introduction *string // 简介(可选)
|
||||
PrivacyMode *bool // 隐私模式:true-开启,false-关闭(可选)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
nick := strings.TrimSpace(*req.NickName)
|
||||
if nick == "" {
|
||||
return srvdef.New(srvdef.ErrParamError, "昵称不能为空", "")
|
||||
}
|
||||
setDoc[usermod.FN.User.NickName] = nick
|
||||
}
|
||||
if req.Avatar != nil {
|
||||
setDoc[usermod.FN.User.Avatar] = strings.TrimSpace(*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 len(setDoc) == 1 {
|
||||
return srvdef.New(srvdef.ErrParamError, "至少需要修改一项资料", "")
|
||||
}
|
||||
|
||||
if serr := usermod.UpdateOneUser(ctx,
|
||||
bson.M{usermod.FN.User.Id: user.Id},
|
||||
bson.M{"$set": setDoc},
|
||||
); serr != nil {
|
||||
log.Error("UpdateProfile UpdateOneUser failed", log.Any("userId", user.Id), log.E(serr))
|
||||
return serr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hkdj-web-server/app/appg"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/statcenter"
|
||||
"hkdj-web-server/pub/common/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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,194 @@
|
||||
package authser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"hkdj-web-server/app/appg"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/pub/common/enum/redisconst"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/netutil"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/useragent"
|
||||
"hkdj-web-server/pub/models/usermod"
|
||||
"hkdj-web-server/pub/mongos"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"hkdj-web-server/pub/redis"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
const (
|
||||
deviceUserLockRetry = 10
|
||||
deviceUserLockInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// ResolveDeviceUser 按设备ID识别用户:先查缓存,再查库,都没有则自动创建
|
||||
func ResolveDeviceUser(ctx context.Context, ua useragent.UA, ip, channel string) (int64, *srvdef.SrvErr) {
|
||||
devID := strings.TrimSpace(ua.DevID)
|
||||
if devID == "" {
|
||||
return 0, srvdef.New(srvdef.ErrParamError, "devID不能为空", "")
|
||||
}
|
||||
ua.DevID = devID
|
||||
|
||||
cacheKey := redisconst.AppDeviceUserKey(devID)
|
||||
if v, err := redis.Get(cacheKey); err == nil && v != nil {
|
||||
if userId, perr := strconv.ParseInt(*v, 10, 64); perr == nil && userId > 0 {
|
||||
return userId, nil
|
||||
}
|
||||
}
|
||||
|
||||
user, serr := findOrCreateDeviceUser(ctx, ua, ip, channel)
|
||||
if serr != nil {
|
||||
return 0, serr
|
||||
}
|
||||
if user.Status == pubdef.UserStatusDisable {
|
||||
return 0, srvdef.New(srvdef.AccountErrAccessForbid, srvdef.AccountErrAccessForbid.Msg(), "")
|
||||
}
|
||||
if err := redis.Set(cacheKey, user.Id, redisconst.AppDeviceUserExpire); err != nil {
|
||||
log.Warn("authser:ResolveDeviceUser cache set failed", log.Any("devID", devID), log.E(err))
|
||||
}
|
||||
return user.Id, nil
|
||||
}
|
||||
|
||||
// findOrCreateDeviceUser 按设备ID查找用户,不存在时加锁创建,避免同一设备并发请求建出多个用户
|
||||
func findOrCreateDeviceUser(ctx context.Context, ua useragent.UA, ip, channel string) (*usermod.User, *srvdef.SrvErr) {
|
||||
lockKey := redisconst.AppDeviceUserLockKey(ua.DevID)
|
||||
for i := 0; i < deviceUserLockRetry; i++ {
|
||||
user, serr := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.DevID: ua.DevID})
|
||||
if serr == nil {
|
||||
touchDeviceUser(ctx, user, ua, ip, channel)
|
||||
return user, nil
|
||||
}
|
||||
if serr.Code != srvdef.ErrUserNotExsit {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
ok, err := redis.Setnx_NewOK(lockKey, 1, redisconst.AppDeviceUserLockExpire)
|
||||
if err != nil {
|
||||
return nil, srvdef.New(srvdef.StoreLevel, "用户创建失败", "")
|
||||
}
|
||||
if ok {
|
||||
defer func() { _ = redis.Del(lockKey) }()
|
||||
return createGuestUser(ctx, ua, ip, channel)
|
||||
}
|
||||
time.Sleep(deviceUserLockInterval)
|
||||
}
|
||||
return nil, srvdef.New(srvdef.StoreLevel, "用户创建失败", "")
|
||||
}
|
||||
|
||||
// touchDeviceUser 更新最近访问信息(缓存未命中时执行,频率受缓存时长限制)
|
||||
func touchDeviceUser(ctx context.Context, user *usermod.User, ua useragent.UA, ip, channel string) {
|
||||
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.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, user, ua)
|
||||
applyChannelIfMissing(setDoc, user, channel)
|
||||
_ = usermod.UpdateOneUser(ctx, bson.M{usermod.FN.User.Id: user.Id}, bson.M{"$set": setDoc})
|
||||
}
|
||||
|
||||
func createGuestUser(ctx context.Context, ua useragent.UA, ip, channel string) (*usermod.User, *srvdef.SrvErr) {
|
||||
now := time.Now()
|
||||
uid := mongos.Lid.User.Get()
|
||||
districtInfo := resolveUserDistrict(channel, "", ip, ua.SysType)
|
||||
user := &usermod.User{
|
||||
Id: uid,
|
||||
Type: pubdef.UserTypeGuest,
|
||||
Status: pubdef.UserStatusNormal,
|
||||
IsRegister: false,
|
||||
LoginType: pubdef.LoginTypeDevice,
|
||||
NickName: fmt.Sprintf("游客%d", uid),
|
||||
Avatar: randomAvatar(),
|
||||
InviteCode: generateUniqueInviteCode(ctx),
|
||||
DistrictCode: normalizeChannel(channel),
|
||||
IsDirect: districtInfo.IsDirect,
|
||||
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,
|
||||
LastLoginAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if user.NickName == "" {
|
||||
user.NickName = appg.GenerateNickname()
|
||||
}
|
||||
if serr := usermod.InsertOneUser(ctx, user); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func toLoginResp(ctx context.Context, user *usermod.User, withCDN bool) *LoginResp {
|
||||
avatar := user.Avatar
|
||||
if withCDN {
|
||||
cdn := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
|
||||
avatar = netutil.MergeURL(cdn, user.Avatar)
|
||||
}
|
||||
return &LoginResp{
|
||||
UserId: user.Id,
|
||||
AccountNo: user.AccountNo,
|
||||
Email: user.Email,
|
||||
NickName: user.NickName,
|
||||
Avatar: avatar,
|
||||
Introduction: user.Introduction,
|
||||
DevID: user.DevID,
|
||||
Mac: user.Mac,
|
||||
DeviceId: user.DevV2,
|
||||
InviteCode: user.InviteCode,
|
||||
AgentInviteCode: user.AgentInviteCode,
|
||||
Invites: user.Invites,
|
||||
Status: user.Status,
|
||||
Type: user.Type,
|
||||
LoginType: user.LoginType,
|
||||
IsRegister: user.IsRegister,
|
||||
PrivacyMode: user.PrivacyMode,
|
||||
Score: user.Score,
|
||||
Recharge: user.Recharge,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeChannel(channel string) string {
|
||||
return strings.TrimSpace(channel)
|
||||
}
|
||||
|
||||
func hasDistrictCode(code string) bool {
|
||||
return strings.TrimSpace(code) != ""
|
||||
}
|
||||
|
||||
func applyChannelIfMissing(setDoc bson.M, user *usermod.User, channel string) {
|
||||
if user == nil || hasDistrictCode(user.DistrictCode) {
|
||||
return
|
||||
}
|
||||
code := normalizeChannel(channel)
|
||||
if code == "" {
|
||||
return
|
||||
}
|
||||
setDoc[usermod.FN.User.DistrictCode] = code
|
||||
districtInfo := resolveUserDistrict(channel, user.AgentInviteCode, user.LastIp, user.SysType)
|
||||
setDoc[usermod.FN.User.IsDirect] = districtInfo.IsDirect
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package coinser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkdj-web-server/app/service/rechargeser/jiahua"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/models/rechargepackagemod"
|
||||
"hkdj-web-server/pub/models/usermod"
|
||||
"hkdj-web-server/pub/models/vipordermod"
|
||||
"hkdj-web-server/pub/mongos"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// BuyCoinResp 购买金币下单响应
|
||||
type BuyCoinResp struct {
|
||||
TradeNo string `json:"tradeNo"`
|
||||
PayUrl string `json:"payUrl"`
|
||||
PayUrlMode string `json:"payUrlMode"`
|
||||
IsOpenNewBrowser bool `json:"isOpenNewBrowser"`
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func GetUserInfo(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
|
||||
return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
}
|
||||
|
||||
// CreateCoinOrder 创建金币购买订单(统一写入 vip_order 表,OrderType=2)。
|
||||
// 防重复:15 秒内同一用户+同一套餐的 pending 订单直接返回原支付链接。
|
||||
func CreateCoinOrder(ctx context.Context,
|
||||
userId int64, userName string,
|
||||
packageId int64, payMode string,
|
||||
sysType string, devType string, ip string,
|
||||
districtCode string,
|
||||
) (*BuyCoinResp, *srvdef.SrvErr) {
|
||||
|
||||
pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Id: packageId,
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeCoin,
|
||||
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
|
||||
})
|
||||
if serr != nil || pkg == nil {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "金币套餐不存在或已下架", "")
|
||||
}
|
||||
coinAmount := pkg.BaseCoin + pkg.GiftCoin
|
||||
if coinAmount <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "金币套餐到账数量无效", "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
dupFilter := bson.M{
|
||||
vipordermod.FN.VipOrder.UserId: userId,
|
||||
vipordermod.FN.VipOrder.CoinCfgId: packageId,
|
||||
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
|
||||
vipordermod.FN.VipOrder.PayMode: payMode,
|
||||
vipordermod.FN.VipOrder.Status: pubdef.VipOrderPending,
|
||||
vipordermod.FN.VipOrder.CreatedAt: bson.M{"$gte": now.Add(-15 * time.Second)},
|
||||
}
|
||||
existing, _ := vipordermod.FindOneVipOrder(ctx, dupFilter)
|
||||
if existing != nil && existing.PayUrl != "" {
|
||||
return &BuyCoinResp{
|
||||
TradeNo: existing.TradeNo,
|
||||
PayUrl: existing.PayUrl,
|
||||
PayUrlMode: existing.PayUrlMode,
|
||||
IsOpenNewBrowser: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
tradeNo := pubdef.GetTrandNoWithUUID()
|
||||
|
||||
payInfo, serr := jiahua.SubmitOrder(ctx, userId, userName, tradeNo, pkg.Price, payMode, sysType, devType, ip)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
order := &vipordermod.VipOrder{
|
||||
Id: mongos.Lid.VipOrder.Get(),
|
||||
OrderType: vipordermod.OrderTypeCoin,
|
||||
UserId: userId,
|
||||
CoinCfgId: packageId, // 兼容历史字段,存购买套餐ID
|
||||
Coin: coinAmount,
|
||||
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
|
||||
}
|
||||
|
||||
return &BuyCoinResp{
|
||||
TradeNo: tradeNo,
|
||||
PayUrl: payInfo.PayUrl,
|
||||
PayUrlMode: payInfo.PayUrlMode,
|
||||
IsOpenNewBrowser: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCoinOrderHistory 查询用户金币购买记录
|
||||
func GetCoinOrderHistory(ctx context.Context, userId int64, page, pageSize int64) (vipordermod.VipOrderList, int64, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
vipordermod.FN.VipOrder.UserId: userId,
|
||||
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
|
||||
}
|
||||
total, serr := vipordermod.CountVipOrder(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, 0, serr
|
||||
}
|
||||
skip := (page - 1) * pageSize
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{vipordermod.FN.VipOrder.CreatedAt: -1}).
|
||||
SetSkip(skip).
|
||||
SetLimit(pageSize)
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, opts)
|
||||
return list, total, serr
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package coinser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/pub/common/response"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/common/utils"
|
||||
"hkdj-web-server/pub/models/cointransmod"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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,697 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/configser"
|
||||
"hkdj-web-server/pub/common/enum/redisconst"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/netutil"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/models/commentlikemod"
|
||||
"hkdj-web-server/pub/models/commentmod"
|
||||
"hkdj-web-server/pub/models/playletchaptermod"
|
||||
"hkdj-web-server/pub/models/playletmod"
|
||||
"hkdj-web-server/pub/models/usermod"
|
||||
"hkdj-web-server/pub/models/videoinformationmod"
|
||||
"hkdj-web-server/pub/mongos"
|
||||
"hkdj-web-server/pub/redis"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
ObjectTypePlaylet int64 = 10 // 短剧评论
|
||||
ObjectTypePlayletChapter int64 = 11 // 短剧剧集评论
|
||||
ObjectTypeVideo int64 = 20 // 视频评论
|
||||
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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
case ObjectTypeVideo:
|
||||
video, err := videoinformationmod.FindOneVideoInformationWithOptions(ctx, bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: objectId,
|
||||
videoinformationmod.FN.VideoInformation.Status: videoinformationmod.InformationSatusVerifyOK,
|
||||
}, options.FindOne().SetProjection(bson.D{
|
||||
{Key: videoinformationmod.FN.VideoInformation.Id, Value: 1},
|
||||
{Key: videoinformationmod.FN.VideoInformation.Title, Value: 1},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Error("查询评论视频目标失败", log.Any("视频ID", objectId), log.E(err))
|
||||
return "", 0, srvdef.New(srvdef.ErrParamError, "视频不存在或未上架", "")
|
||||
}
|
||||
return video.Title, 0, 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
|
||||
}
|
||||
case ObjectTypeVideo:
|
||||
filter := bson.M{
|
||||
videoinformationmod.FN.VideoInformation.Id: objectId,
|
||||
}
|
||||
if delta < 0 {
|
||||
filter[videoinformationmod.FN.VideoInformation.Real.Comments] = bson.M{"$gt": 0}
|
||||
}
|
||||
if err := videoinformationmod.UpdateOneVideoInformation(ctx, filter, bson.M{
|
||||
"$inc": bson.M{videoinformationmod.FN.VideoInformation.Real.Comments: delta},
|
||||
}); err != nil {
|
||||
if delta < 0 && err.Code == srvdef.ErrNoData {
|
||||
log.Warn("视频评论数已为0,跳过扣减", log.Any("视频ID", objectId))
|
||||
return 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, ObjectTypeVideo},
|
||||
},
|
||||
}, 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) || objectType == ObjectTypeVideo
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/netutil"
|
||||
"hkdj-web-server/pub/pubdef"
|
||||
"hkdj-web-server/pub/storage"
|
||||
"hkdj-web-server/pub/storage/proxy"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UpdateAdvitiseData 从媒资中心按 AppId 拉取广告,图片拼接 CDN 后写入缓存
|
||||
func UpdateAdvitiseData(ctx context.Context) {
|
||||
config := GetConfigInfo()
|
||||
queryAdvitiseReq := &proxy.AppAdvitiseInfoReq{
|
||||
Id: pubdef.AppId,
|
||||
}
|
||||
advitiseResult, err := storage.QueryAdvitiseList(ctx, queryAdvitiseReq, "")
|
||||
if err != nil || advitiseResult.Code != http.StatusOK {
|
||||
if advitiseResult != nil {
|
||||
log.Warn("UpdateAdvitiseData.QueryAdvitiseList failed", log.Any("filter", queryAdvitiseReq), log.E(err), log.Any("respCode", advitiseResult.Code))
|
||||
} else {
|
||||
log.Warn("UpdateAdvitiseData.QueryAdvitiseList failed", log.Any("filter", queryAdvitiseReq), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
data := advitiseResult.Data
|
||||
for _, list := range []*[]proxy.AdvitiseInfo{
|
||||
&data.DialogList,
|
||||
&data.HomeList,
|
||||
&data.VideoTopList,
|
||||
&data.VideoDownList,
|
||||
&data.HotAppList,
|
||||
&data.NewAppList,
|
||||
&data.MustAppList,
|
||||
&data.TitleAppList,
|
||||
&data.FriendList,
|
||||
&data.NineGridList,
|
||||
&data.VideoList,
|
||||
&data.StopList,
|
||||
&data.ForcedJumpList,
|
||||
&data.AppInfoAList,
|
||||
&data.AppInfoBList,
|
||||
} {
|
||||
mergeAdvitiseImages(*list, config.ImgCdn)
|
||||
}
|
||||
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Advitise = data
|
||||
}
|
||||
|
||||
func mergeAdvitiseImages(list []proxy.AdvitiseInfo, cdn string) {
|
||||
for i := range list {
|
||||
if len(list[i].Images) > 0 {
|
||||
list[i].Images = netutil.MergeURL(cdn, list[i].Images)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkdj-web-server/app/api/proto"
|
||||
"hkdj-web-server/app/service/translationser"
|
||||
"hkdj-web-server/pub/common/log"
|
||||
"hkdj-web-server/pub/common/netutil"
|
||||
"hkdj-web-server/pub/common/srvdef"
|
||||
"hkdj-web-server/pub/models/announcementmod"
|
||||
"hkdj-web-server/pub/models/cfg"
|
||||
"hkdj-web-server/pub/models/ipblackmod"
|
||||
"hkdj-web-server/pub/models/richtextcontentmod"
|
||||
"hkdj-web-server/pub/models/videotagmod"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheMgr CacheInfo //
|
||||
skdCron *cron.Cron
|
||||
)
|
||||
|
||||
// richTextImgSrcRe 匹配富文本HTML里 img 标签的 src 图片地址,兼容单引号、双引号和JSON转义后的双引号。
|
||||
var richTextImgSrcRe = regexp.MustCompile(`(?i)(<img\b[^>]*?\s*)src\s*=\s*(\\?"([^"\\]*)\\?"|'([^']*)')`)
|
||||
|
||||
type CacheInfo struct {
|
||||
Lock sync.RWMutex // 读写锁
|
||||
Config proto.ConfigInfo // 系统配置缓存
|
||||
Advitise proto.AdvitiseData
|
||||
TagCategoryList []proto.VideoTagCategoryData
|
||||
TotalHistory int64
|
||||
IpBlackMaps map[string]struct{}
|
||||
TagMaps map[int64]proto.TagsInfoData
|
||||
NewAddressInfo proto.NewAddressInfo
|
||||
AppRichTexts []proto.AppRichTextItemResp
|
||||
Announcements []*proto.AnnouncementItem
|
||||
}
|
||||
|
||||
type appRichTextCacheItem struct {
|
||||
Id int64 `bson:"_id,omitempty"`
|
||||
Title string `bson:"title"`
|
||||
Content string `bson:"content"`
|
||||
Desc string `bson:"desc"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
skdCron = cron.New(cron.WithSeconds()) // 秒级定时任务
|
||||
CacheMgr.Config = proto.ConfigInfo{}
|
||||
CacheMgr.IpBlackMaps = make(map[string]struct{})
|
||||
}
|
||||
|
||||
func Start() {
|
||||
ctx := context.Background()
|
||||
UpdateConfigInfo(ctx)
|
||||
UpdateAdvitiseData(ctx)
|
||||
updateIpBlack()
|
||||
updateTagsCache()
|
||||
updateAppRichTexts()
|
||||
updateAnnouncements()
|
||||
_, _ = skdCron.AddFunc("0 */5 * * * ?", func() {
|
||||
UpdateConfigInfo(ctx)
|
||||
UpdateAdvitiseData(ctx)
|
||||
updateIpBlack()
|
||||
updateTagsCache()
|
||||
updateAppRichTexts()
|
||||
})
|
||||
_, _ = skdCron.AddFunc("0 */10 * * * ?", func() {
|
||||
updateAnnouncements()
|
||||
})
|
||||
|
||||
skdCron.Start()
|
||||
}
|
||||
|
||||
func updateTagsCache() {
|
||||
filter := bson.M{videotagmod.FN.VideoTagInfo.Status: true}
|
||||
opts := &options.FindOptions{}
|
||||
opts.SetLimit(5000)
|
||||
opts.SetSort(bson.M{videotagmod.FN.VideoTagInfo.Sort: -1})
|
||||
list, err := videotagmod.FindManyVideoTagInfos(context.Background(), filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("updateTaggCache.FFindManyVideoTagInfos", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
tempMaps := make(map[int64]proto.TagsInfoData)
|
||||
for _, v := range list {
|
||||
tempMaps[v.Id] = proto.TagsInfoData{
|
||||
Id: v.Id,
|
||||
Name: v.Title,
|
||||
Slug: v.Slug,
|
||||
SeoInfo: v.SeoInfo,
|
||||
}
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.TagMaps = tempMaps
|
||||
}
|
||||
|
||||
func UpdateConfigInfo(ctx context.Context) {
|
||||
config := proto.ConfigInfo{}
|
||||
newAddressInfo := proto.NewAddressInfo{}
|
||||
|
||||
filter := bson.M{cfg.FN.SysCfg.Id: 1}
|
||||
sysCfg, _ := cfg.FindOneSysCfg(context.Background(), filter)
|
||||
if sysCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
config.NewUrlAddr = sysCfg.NewUrlAddr
|
||||
config.NewUrlAddr1 = sysCfg.NewUrlAddr1
|
||||
config.NewUrlAddr2 = sysCfg.NewUrlAddr2
|
||||
config.ForeverUrlAddr = sysCfg.ForeverUrlAddr
|
||||
config.GmailAddr = sysCfg.GmailAddr
|
||||
config.AppAddr = sysCfg.AppAddr
|
||||
config.OfficialAppAddr = sysCfg.OfficialAppAddr
|
||||
config.WeiChart = sysCfg.WeiChart
|
||||
config.QQ = sysCfg.QQ
|
||||
config.TgGroup = sysCfg.TgGroup
|
||||
config.PotaoGroup = sysCfg.PotaoGroup
|
||||
config.GitAddr = sysCfg.GitAddr
|
||||
config.TwitterAddr = sysCfg.TwitterAddr
|
||||
config.Telegram = sysCfg.Telegram
|
||||
config.TelegramAdd = sysCfg.TelegramAdd
|
||||
config.TranslationAddr = sysCfg.TranslationAddr
|
||||
config.ImgCdn = sysCfg.ImgAppCdn
|
||||
config.TsCdn = sysCfg.MediaAppCdn
|
||||
config.ImgAppUsCdn = sysCfg.ImgAppUsCdn
|
||||
config.MediaAppUsCdn = sysCfg.MediaAppUsCdn
|
||||
config.PdfAddr = sysCfg.PdfAddr
|
||||
|
||||
config.WebTitle = sysCfg.WebTitle
|
||||
config.WebKeyworld = sysCfg.WebKeyworld
|
||||
config.WebDescription = sysCfg.WebDescription
|
||||
config.WebSiteName = sysCfg.WebSiteName
|
||||
config.WebAddr = sysCfg.WebAddr
|
||||
config.DHNewUrlAddr1 = sysCfg.DHNewUrlAddr1
|
||||
config.DHNewUrlAddr2 = sysCfg.DHNewUrlAddr2
|
||||
config.DHNewUrlAddr3 = sysCfg.DHNewUrlAddr3
|
||||
config.UseIngAddress = sysCfg.UseAddress
|
||||
config.FreeVPN = sysCfg.FreeVPN
|
||||
config.AdvitiesTopicNum = sysCfg.AdvitiesTopicNum
|
||||
|
||||
config.DHKeyworld = sysCfg.DHKeyworld
|
||||
config.DHDescription = sysCfg.DHDescription
|
||||
config.DHSiteName = sysCfg.DHSiteName
|
||||
config.DHTitle = sysCfg.DHTitle
|
||||
config.ActressBackGround = netutil.MergeURL(sysCfg.ImgAppCdn, sysCfg.ActressBackGround)
|
||||
config.WatermarkImage = sysCfg.WatermarkImage
|
||||
config.CustomerServiceHost = sysCfg.CustomerServiceHost
|
||||
config.ChannelCode = sysCfg.ChannelCode
|
||||
config.EnableH265 = sysCfg.EnableH265
|
||||
|
||||
// NewAddressInfo 来源于系统配置 sys_cfg,用于只返回前端需要展示的最新地址信息。
|
||||
newAddressInfo.NewUrlAddr = sysCfg.NewUrlAddr
|
||||
newAddressInfo.NewUrlAddr1 = sysCfg.NewUrlAddr1
|
||||
newAddressInfo.NewUrlAddr2 = sysCfg.NewUrlAddr2
|
||||
newAddressInfo.ForeverUrlAddr = sysCfg.ForeverUrlAddr
|
||||
newAddressInfo.GmailAddr = sysCfg.GmailAddr
|
||||
newAddressInfo.TgGroup = sysCfg.TgGroup
|
||||
newAddressInfo.Telegram = sysCfg.Telegram
|
||||
newAddressInfo.CustomerServiceHost = sysCfg.CustomerServiceHost
|
||||
newAddressInfo.TwitterAddr = sysCfg.TwitterAddr
|
||||
newAddressInfo.PdfAddr = sysCfg.PdfAddr
|
||||
newAddressInfo.AppAddr = sysCfg.AppAddr
|
||||
newAddressInfo.OfficialAppAddr = sysCfg.OfficialAppAddr
|
||||
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Config = config
|
||||
CacheMgr.NewAddressInfo = newAddressInfo
|
||||
|
||||
}
|
||||
func GetConfigInfo() proto.ConfigInfo {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
|
||||
return CacheMgr.Config
|
||||
}
|
||||
func GetAdvitise() proto.AdvitiseData {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
|
||||
return CacheMgr.Advitise
|
||||
}
|
||||
|
||||
func GetNewAddress() proto.NewAddressInfo {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
return CacheMgr.NewAddressInfo
|
||||
}
|
||||
|
||||
// GetFooterInfo 查询APP底部信息
|
||||
func GetFooterInfo() proto.FooterInfo {
|
||||
// 底部富文本和系统配置都来自缓存,接口请求不直接访问Mongo。
|
||||
config := GetConfigInfo()
|
||||
return proto.FooterInfo{
|
||||
Brand: buildFooterBrand(config),
|
||||
NavList: buildFooterNavList(),
|
||||
ContactList: buildFooterContactList(config),
|
||||
}
|
||||
}
|
||||
|
||||
func getFooterRichTextMap() map[string]proto.AppRichTextItemResp {
|
||||
data := getCachedAppRichTexts()
|
||||
if len(data) == 0 {
|
||||
updateAppRichTexts()
|
||||
data = getCachedAppRichTexts()
|
||||
}
|
||||
result := make(map[string]proto.AppRichTextItemResp, len(data))
|
||||
for _, item := range data {
|
||||
title := strings.TrimSpace(item.Title)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
result[title] = item
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildFooterBrand(config proto.ConfigInfo) proto.FooterBrand {
|
||||
return proto.FooterBrand{
|
||||
Title: config.WebSiteName,
|
||||
Content: config.WebDescription,
|
||||
}
|
||||
}
|
||||
|
||||
func buildFooterNavList() []proto.TranslationCategory {
|
||||
bottom := translationser.GetTranslationBottomList()
|
||||
return append([]proto.TranslationCategory{}, bottom.CategoryList...)
|
||||
}
|
||||
|
||||
func buildFooterContactList(config proto.ConfigInfo) []proto.FooterContact {
|
||||
// 联系方式名称按页面截图固定返回,右侧只包含 E-mail、商务合作、官方群 三项。
|
||||
return []proto.FooterContact{
|
||||
{Name: "E-mail", Value: config.GmailAddr},
|
||||
{Name: "商务合作:立即联系", Value: config.Telegram},
|
||||
{Name: "官方群:立即加入", Value: firstNotEmpty(config.TgGroup, config.TelegramAdd)},
|
||||
}
|
||||
}
|
||||
|
||||
func firstNotEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAppRichTextList 查询APP富文本内容列表
|
||||
func GetAppRichTextList(rawIds string, rawTitles string) proto.AppRichTextListResp {
|
||||
ids := parseInt64Set(rawIds)
|
||||
titles := parseStringSet(rawTitles)
|
||||
data := getCachedAppRichTexts()
|
||||
if len(data) == 0 {
|
||||
updateAppRichTexts()
|
||||
data = getCachedAppRichTexts()
|
||||
}
|
||||
return proto.AppRichTextListResp{List: filterAppRichTexts(data, ids, titles)}
|
||||
}
|
||||
|
||||
func getCachedAppRichTexts() []proto.AppRichTextItemResp {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
return append([]proto.AppRichTextItemResp{}, CacheMgr.AppRichTexts...)
|
||||
}
|
||||
|
||||
func filterAppRichTexts(data []proto.AppRichTextItemResp, ids map[int64]struct{}, titles map[string]struct{}) []proto.AppRichTextItemResp {
|
||||
list := make([]proto.AppRichTextItemResp, 0, len(data))
|
||||
for _, item := range data {
|
||||
if len(ids) > 0 {
|
||||
if _, ok := ids[item.Id]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(titles) > 0 {
|
||||
if _, ok := titles[item.Title]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func parseInt64Set(raw string) map[int64]struct{} {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
resp := make(map[int64]struct{})
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
id, err := strconv.ParseInt(strings.TrimSpace(item), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
continue
|
||||
}
|
||||
resp[id] = struct{}{}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func parseStringSet(raw string) map[string]struct{} {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
resp := make(map[string]struct{})
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
resp[item] = struct{}{}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// GetSiteSeoInfo 返回全站SEO配置,供统一渲染 title、meta keywords、description 和 canonical。
|
||||
func GetSiteSeoInfo() proto.SeoInfo {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
webAddr := normalizeWebAddr(CacheMgr.Config.WebAddr)
|
||||
return proto.SeoInfo{
|
||||
Title: CacheMgr.Config.WebTitle,
|
||||
Keywords: append([]string{}, CacheMgr.Config.WebKeyworld...),
|
||||
Description: CacheMgr.Config.WebDescription,
|
||||
WebSiteName: CacheMgr.Config.WebSiteName,
|
||||
CanonicalURL: webAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNavSeoInfo 返回导航页SEO配置
|
||||
func GetNavSeoInfo() proto.SeoNavInfo {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
return proto.SeoNavInfo{
|
||||
DHTitle: CacheMgr.Config.DHTitle,
|
||||
DHKeyworld: append([]string{}, CacheMgr.Config.DHKeyworld...),
|
||||
DHDescription: CacheMgr.Config.DHDescription,
|
||||
DHSiteName: CacheMgr.Config.DHSiteName,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNavPageList 返回导航页线路集合。
|
||||
func GetNavPageList() []proto.NavPageItem {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
list := make([]proto.NavPageItem, 0, 3)
|
||||
for _, item := range []proto.NavPageItem{
|
||||
{Line: 1, Url: strings.TrimSpace(CacheMgr.Config.DHNewUrlAddr1)},
|
||||
{Line: 2, Url: strings.TrimSpace(CacheMgr.Config.DHNewUrlAddr2)},
|
||||
{Line: 3, Url: strings.TrimSpace(CacheMgr.Config.DHNewUrlAddr3)},
|
||||
} {
|
||||
if item.Url == "" {
|
||||
continue
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func normalizeWebAddr(webAddr string) string {
|
||||
webAddr = strings.TrimSpace(webAddr)
|
||||
webAddr = strings.TrimRight(webAddr, "/")
|
||||
if webAddr == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(webAddr, "http://") && !strings.HasPrefix(webAddr, "https://") {
|
||||
return "https://" + webAddr
|
||||
}
|
||||
return webAddr
|
||||
}
|
||||
|
||||
func GetRandCDN() string {
|
||||
config := GetConfigInfo()
|
||||
return config.TsCdn
|
||||
}
|
||||
|
||||
func updateIpBlack() {
|
||||
ctx := context.Background()
|
||||
list, err := ipblackmod.FindManyIPBlackModel(ctx, bson.M{})
|
||||
if err != nil {
|
||||
log.Error("updateIpBlack FindManyIPBlackModel error", log.E(err))
|
||||
return
|
||||
}
|
||||
tempMaps := make(map[string]struct{})
|
||||
for _, v := range list {
|
||||
tempMaps[v.Ip] = struct{}{}
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.IpBlackMaps = tempMaps
|
||||
}
|
||||
|
||||
func updateAppRichTexts() {
|
||||
resp, err := queryAppRichTexts(context.Background())
|
||||
if err != nil {
|
||||
log.Warn("查询APP富文本内容失败", log.E(err))
|
||||
return
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.AppRichTexts = resp
|
||||
}
|
||||
|
||||
// queryAppRichTexts 查询启用状态的APP富文本内容,统一供定时缓存和接口兜底加载使用。
|
||||
func queryAppRichTexts(ctx context.Context) ([]proto.AppRichTextItemResp, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
richtextcontentmod.FN.RichTextContentModel.Status: true,
|
||||
}
|
||||
opts := options.Find().
|
||||
SetProjection(bson.M{
|
||||
richtextcontentmod.FN.RichTextContentModel.Id: 1,
|
||||
richtextcontentmod.FN.RichTextContentModel.Title: 1,
|
||||
richtextcontentmod.FN.RichTextContentModel.Content: 1,
|
||||
richtextcontentmod.FN.RichTextContentModel.Desc: 1,
|
||||
}).
|
||||
SetSort(bson.D{
|
||||
{Key: richtextcontentmod.FN.RichTextContentModel.Sort, Value: -1},
|
||||
{Key: richtextcontentmod.FN.RichTextContentModel.UpdatedAt, Value: -1},
|
||||
}).
|
||||
SetLimit(100)
|
||||
list := make([]appRichTextCacheItem, 0)
|
||||
err := richtextcontentmod.FindManyRichTextContentModelModelWithBind(ctx, &list, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("查询APP富文本内容失败", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
imgCdn := GetConfigInfo().ImgCdn
|
||||
resp := make([]proto.AppRichTextItemResp, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp = append(resp, proto.AppRichTextItemResp{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Content: applyRichTextImgSrcCdn(imgCdn, item.Content),
|
||||
Desc: item.Desc,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// applyRichTextImgSrcCdn 给富文本 content 中 img 标签的 src 相对图片地址拼接图片CDN
|
||||
func applyRichTextImgSrcCdn(imgCdn string, content string) string {
|
||||
if imgCdn == "" || content == "" {
|
||||
return content
|
||||
}
|
||||
return richTextImgSrcRe.ReplaceAllStringFunc(content, func(match string) string {
|
||||
parts := richTextImgSrcRe.FindStringSubmatch(match)
|
||||
if len(parts) < 5 {
|
||||
return match
|
||||
}
|
||||
prefix := parts[1]
|
||||
quote := `"`
|
||||
src := parts[3]
|
||||
if src == "" {
|
||||
quote = `'`
|
||||
src = parts[4]
|
||||
} else if strings.HasPrefix(parts[2], `\"`) {
|
||||
quote = `\"`
|
||||
}
|
||||
src = strings.TrimSpace(src)
|
||||
if src == "" || isAbsoluteRichTextSrc(src) {
|
||||
return match
|
||||
}
|
||||
return prefix + "src=" + quote + netutil.MergeURL(imgCdn, src) + quote
|
||||
})
|
||||
}
|
||||
|
||||
// isAbsoluteRichTextSrc 判断图片地址是否已经是完整地址或特殊协议,避免重复拼接CDN。
|
||||
func isAbsoluteRichTextSrc(src string) bool {
|
||||
lowerSrc := strings.ToLower(src)
|
||||
return strings.HasPrefix(lowerSrc, "http://") ||
|
||||
strings.HasPrefix(lowerSrc, "https://") ||
|
||||
strings.HasPrefix(lowerSrc, "data:") ||
|
||||
strings.HasPrefix(lowerSrc, "blob:") ||
|
||||
strings.HasPrefix(src, "//")
|
||||
}
|
||||
|
||||
func updateAnnouncements() {
|
||||
list, err := queryAnnouncements(context.Background())
|
||||
if err != nil {
|
||||
log.Warn("查询公告失败", log.E(err))
|
||||
return
|
||||
}
|
||||
CacheMgr.Lock.Lock()
|
||||
defer CacheMgr.Lock.Unlock()
|
||||
CacheMgr.Announcements = list
|
||||
}
|
||||
|
||||
func queryAnnouncements(ctx context.Context) ([]*proto.AnnouncementItem, *srvdef.SrvErr) {
|
||||
filter := bson.M{announcementmod.FN.Announcement.Status: true}
|
||||
opts := options.Find().
|
||||
SetProjection(bson.M{
|
||||
announcementmod.FN.Announcement.Id: 1,
|
||||
announcementmod.FN.Announcement.Title: 1,
|
||||
announcementmod.FN.Announcement.Content: 1,
|
||||
}).
|
||||
SetSort(bson.D{
|
||||
{Key: announcementmod.FN.Announcement.Sort, Value: -1},
|
||||
{Key: announcementmod.FN.Announcement.UpdatedAt, Value: -1},
|
||||
}).
|
||||
SetLimit(50)
|
||||
list, err := announcementmod.FindManyAnnouncement(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Warn("查询公告失败", log.Any("filter", filter), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*proto.AnnouncementItem, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
resp = append(resp, &proto.AnnouncementItem{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetAnnouncements 返回启用中的公告缓存,每10分钟刷新。
|
||||
func GetAnnouncements() []*proto.AnnouncementItem {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
if len(CacheMgr.Announcements) == 0 {
|
||||
return make([]*proto.AnnouncementItem, 0)
|
||||
}
|
||||
resp := make([]*proto.AnnouncementItem, len(CacheMgr.Announcements))
|
||||
copy(resp, CacheMgr.Announcements)
|
||||
return resp
|
||||
}
|
||||
|
||||
func CheckIpBlack(ip string) bool {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
if _, ok := CacheMgr.IpBlackMaps[ip]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetVideoTagCategoryDataList() []proto.VideoTagCategoryData {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
rsl := make([]proto.VideoTagCategoryData, 0)
|
||||
for _, v := range CacheMgr.TagCategoryList {
|
||||
rsl = append(rsl, v)
|
||||
}
|
||||
return rsl
|
||||
}
|
||||
func Stop() {
|
||||
skdCron.Stop()
|
||||
}
|
||||
|
||||
func GetTag(tagId int64) *proto.TagsInfoData {
|
||||
CacheMgr.Lock.RLock()
|
||||
defer CacheMgr.Lock.RUnlock()
|
||||
tag, ok := CacheMgr.TagMaps[tagId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &tag
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package configser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkdj-web-server/app/appg"
|
||||
"hkdj-web-server/pub/common/ipaddr"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ClientIPFromCtx 从 gin 请求上下文解析客户端 IP。
|
||||
func ClientIPFromCtx(ctx context.Context) string {
|
||||
c, ok := ctx.(*gin.Context)
|
||||
if !ok || c == nil {
|
||||
return ""
|
||||
}
|
||||
return ipaddr.ResolveClientIP(c)
|
||||
}
|
||||
|
||||
// IsOverseasIP 与 m3u8 TS 域名判断一致:按 v4/v6 库识别是否海外。
|
||||
func IsOverseasIP(ip string) bool {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip == "" {
|
||||
return false
|
||||
}
|
||||
detector := appg.DetectorV6
|
||||
if ipaddr.IsIPv4(ip) {
|
||||
detector = appg.DetectorV4
|
||||
}
|
||||
if detector == nil {
|
||||
return false
|
||||
}
|
||||
ok, _ := detector.IsOverseasIP(ip)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetAppImageCdn 海外 IP 且配置了 imgAppUsCdn 时用海外图床,否则用 imgAppCdn。
|
||||
func GetAppImageCdn(ip string) string {
|
||||
cfg := GetConfigInfo()
|
||||
if IsOverseasIP(ip) {
|
||||
if us := strings.TrimSpace(cfg.ImgAppUsCdn); us != "" {
|
||||
return us
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(cfg.ImgCdn)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user