commit bef9f8b4ed0d3a5dbcb9614fa8738a7468358755
Author: fish03
Date: Wed Sep 23 14:43:16 2026 +0700
Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3e1096c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+~$*
+outputs/
+.codex/
+app/docs/
+*.tar.gz
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..589d3fe
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+include tool/build/Makefile.help
+
+## 更新APP文档
+update_app_doc:
+ cd app && swag init --parseDependency --parseDepth 4
+
+## 更新Web文档
+update_h5_doc:
+ cd h5app && swag init --parseDependency --parseDepth 4
+
+## 更新Web文档
+update_web_doc:
+ cd web && swag init --parseDependency --parseDepth 4
+
+## 更新所有项目文档
+update_all_docs: update_app_doc update_h5_doc update_web_doc
+
+## 更新测试服Web
+deploy_test_web:
+ cd web && ./deploy_test.sh
+
+## 更新测试服H5
+deploy_test_h5:
+ cd h5app && ./deploy_test.sh
+
+## 更新测试服APP
+deploy_test_app:
+ cd app && ./deploy_test.sh
+
+## 更新测试服APP + H5
+deploy_test_h5_and_app: deploy_test_app deploy_test_h5
+
+test:
+ ./tool/build/check_syntax.sh
+
+.DEFAULT_GOAL := help
\ No newline at end of file
diff --git a/app/Makefile b/app/Makefile
new file mode 100755
index 0000000..28a033d
--- /dev/null
+++ b/app/Makefile
@@ -0,0 +1,30 @@
+GOPATH:=$(shell go env GOPATH)
+
+GIT_BRANCH:=$(shell git symbolic-ref --short -q HEAD)
+GIT_VERSION:=$(shell git log --pretty=oneline -p -1 | head -n 1 | awk '{print $$1}')
+BUILD_DATE:=$(shell date '+%Y-%m-%d %H:%M:%S %z')
+BUILD_IP:=$(shell ifconfig | grep inet | grep -v grep | grep -v "inet6" | grep -v "127.0.0.1" | awk '{print $$2}')
+VERSON_PATH:=pfshop/pub/srvversion
+
+APP_BUILD_FLAG:=-ldflags "-X '$(VERSON_PATH).GIT_BRANCH=$(GIT_BRANCH)' -X '$(VERSON_PATH).GIT_VERSION=$(GIT_VERSION)' -X '$(VERSON_PATH).BUILD_DATE=$(BUILD_DATE)' -X '$(VERSON_PATH).BUILD_IP=$(BUILD_IP)'"
+
+
+.PHONY: test deploy
+deploy:
+ @echo "deploy, GOPATH=$(GOPATH)"
+ go clean
+ rm -f web_mdfls_h5
+ env GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_mdfls_h5 -v
+ tar czf web_mdfls_h5.tar.gz web_mdfls_h5 templates/ static/ conf/m3u8.key
+ rm -f web_mdfls_h5
+
+test:
+ @echo "test, GOPATH=$(GOPATH)"
+ GOOS=linux GOARCH=amd64 go build $(APP_BUILD_FLAG) -o web_mdfls_h5
+ tar czf web_mdfls_h5.tar.gz web_mdfls_h5 templates/ static/ conf/m3u8.key
+ rm -f web_mdfls_h5
+
+
+localtest:
+ @echo "test, GOPATH=$(GOPATH)"
+ go build $(APP_BUILD_FLAG) -o app
\ No newline at end of file
diff --git a/app/api/.gitkeep b/app/api/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/api/web/application.go b/app/api/web/application.go
new file mode 100644
index 0000000..dd0d26b
--- /dev/null
+++ b/app/api/web/application.go
@@ -0,0 +1,22 @@
+package web
+
+import (
+ "videoweb/app/service/applistser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Application 福利应用页 SSR:拉取 /api/app/applist/list。
+func Application(c *gin.Context) {
+ raw := applistser.FetchListOrEmpty(c, 0)
+ vm := applistser.BuildPageVM(raw)
+ data := gin.H{
+ "title": "福利应用",
+ "navActive": "application",
+ "categories": vm.Categories,
+ }
+ if len(vm.Categories) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "application.html", data)
+}
diff --git a/app/api/web/auth.go b/app/api/web/auth.go
new file mode 100644
index 0000000..62a53ee
--- /dev/null
+++ b/app/api/web/auth.go
@@ -0,0 +1,548 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+ "videoweb/app/service/authser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type registerBody struct {
+ AccountNo string `json:"accountNo"`
+ Email string `json:"email"`
+ Password string `json:"password"`
+ AgentInviteCode string `json:"agentInviteCode"`
+ DevID string `json:"devID"`
+ Channel string `json:"channel"`
+}
+
+type loginBody struct {
+ Account string `json:"account"`
+ Password string `json:"password"`
+ DevID string `json:"devID"`
+ Channel string `json:"channel"`
+}
+
+type resetPasswordBody struct {
+ AccountNo string `json:"accountNo"`
+ Email string `json:"email"`
+ NewPassword string `json:"newPassword"`
+}
+
+type updateProfileBody struct {
+ Avatar string `json:"avatar"`
+ Introduction string `json:"introduction"`
+ NickName string `json:"nickName"`
+ OldPassword string `json:"oldPassword"`
+ NewPassword string `json:"newPassword"`
+ PrivacyMode *bool `json:"privacyMode"`
+}
+
+// Register 代理上游注册,成功后写入登录 cookie。
+func Register(c *gin.Context) {
+ var body registerBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.AccountNo = strings.TrimSpace(body.AccountNo)
+ body.Email = strings.TrimSpace(body.Email)
+ body.AgentInviteCode = strings.TrimSpace(body.AgentInviteCode)
+ body.DevID = strings.TrimSpace(body.DevID)
+ body.Channel = strings.TrimSpace(body.Channel)
+
+ if body.AccountNo == "" || body.Password == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请填写账号和密码",
+ "data": nil,
+ })
+ return
+ }
+
+ // 上游 registerReq 仍要求 email;前端已去掉邮箱输入时,用账号生成唯一占位邮箱。
+ if body.Email == "" {
+ body.Email = body.AccountNo + "@qmdj.local"
+ }
+
+ env, user, err := authser.Register(c, authser.RegisterReq{
+ AccountNo: body.AccountNo,
+ Email: body.Email,
+ Password: body.Password,
+ AgentInviteCode: body.AgentInviteCode,
+ DevID: body.DevID,
+ Channel: body.Channel,
+ })
+ if err != nil {
+ log.Error("register upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "register failed",
+ "tip": "注册失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+
+ if env.Code != 200 || user == nil {
+ tip := strings.TrimSpace(env.Tip)
+ if tip == "" {
+ tip = strings.TrimSpace(env.Msg)
+ }
+ if tip == "" {
+ tip = "注册失败"
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": env.Code,
+ "msg": env.Msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ authser.SetAuthCookies(c, user)
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": env.Msg,
+ "tip": firstNonEmpty(env.Tip, "注册成功"),
+ "data": user,
+ })
+}
+
+// Login 代理上游登录,成功后写入登录 cookie。
+func Login(c *gin.Context) {
+ var body loginBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.Account = strings.TrimSpace(body.Account)
+ body.DevID = strings.TrimSpace(body.DevID)
+ body.Channel = strings.TrimSpace(body.Channel)
+ if body.Account == "" || body.Password == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请填写账号和密码",
+ "data": nil,
+ })
+ return
+ }
+
+ env, user, err := authser.Login(c, authser.LoginReq{
+ Account: body.Account,
+ Password: body.Password,
+ DevID: body.DevID,
+ Channel: body.Channel,
+ })
+ if err != nil {
+ log.Error("login upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "login failed",
+ "tip": "登录失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+
+ if env.Code != 200 || user == nil {
+ tip := strings.TrimSpace(env.Tip)
+ if tip == "" {
+ tip = strings.TrimSpace(env.Msg)
+ }
+ if tip == "" {
+ tip = "登录失败"
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": env.Code,
+ "msg": env.Msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ authser.SetAuthCookies(c, user)
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": env.Msg,
+ "tip": firstNonEmpty(env.Tip, "登录成功"),
+ "data": user,
+ })
+}
+
+// ResetPassword 代理上游找回密码接口。
+func ResetPassword(c *gin.Context) {
+ var body resetPasswordBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"code": 1001, "msg": "参数错误", "tip": "参数错误", "data": nil})
+ return
+ }
+ body.AccountNo = strings.TrimSpace(body.AccountNo)
+ body.Email = strings.TrimSpace(body.Email)
+ if body.AccountNo == "" || body.Email == "" || body.NewPassword == "" {
+ c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "请填写账号、邮箱和新密码", "data": nil})
+ return
+ }
+
+ env, err := authser.ResetPassword(c, authser.ResetPasswordReq{
+ AccountNo: body.AccountNo,
+ Email: body.Email,
+ NewPassword: body.NewPassword,
+ })
+ if err != nil {
+ log.Error("reset password upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "reset password failed", "tip": "重置密码失败,请稍后重试", "data": nil})
+ return
+ }
+ tip := firstNonEmpty(env.Tip, env.Msg)
+ if env.Code == 200 {
+ tip = firstNonEmpty(tip, "密码重置成功")
+ } else {
+ tip = firstNonEmpty(tip, "密码重置失败")
+ }
+ c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
+}
+
+// Guest 代理上游游客登录,成功后写入登录 cookie。
+// 对应 POST /api/app/auth/guest;需传 devID,上游要求 X-User-Agent(由 BFF 组装)。
+func Guest(c *gin.Context) {
+ var body struct {
+ DevID string `json:"devID"`
+ DeviceID string `json:"deviceId"`
+ Mac string `json:"mac"`
+ Channel string `json:"channel"`
+ }
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.DevID = strings.TrimSpace(body.DevID)
+ body.DeviceID = strings.TrimSpace(body.DeviceID)
+ body.Mac = strings.TrimSpace(body.Mac)
+ body.Channel = strings.TrimSpace(body.Channel)
+ if body.DevID == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少设备标识",
+ "data": nil,
+ })
+ return
+ }
+ if body.DeviceID == "" {
+ body.DeviceID = body.DevID
+ }
+
+ env, user, err := authser.GuestLogin(c, authser.GuestLoginReq{
+ DevID: body.DevID,
+ DeviceID: body.DeviceID,
+ Mac: body.Mac,
+ Channel: body.Channel,
+ })
+ if err != nil {
+ log.Error("guest login upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "guest login failed",
+ "tip": "游客登录失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+
+ if env.Code != 200 || user == nil {
+ tip := strings.TrimSpace(env.Tip)
+ if tip == "" {
+ tip = strings.TrimSpace(env.Msg)
+ }
+ if tip == "" {
+ tip = "游客登录失败"
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": env.Code,
+ "msg": env.Msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ authser.SetAuthCookies(c, user)
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "游客登录成功"),
+ "data": user,
+ })
+}
+
+// UpdateProfile 代理上游修改资料,成功后刷新 getInfo 并写 cookie。
+func UpdateProfile(c *gin.Context) {
+ var body updateProfileBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.Avatar = strings.TrimSpace(body.Avatar)
+ body.Introduction = strings.TrimSpace(body.Introduction)
+ body.NickName = strings.TrimSpace(body.NickName)
+ body.OldPassword = strings.TrimSpace(body.OldPassword)
+ body.NewPassword = strings.TrimSpace(body.NewPassword)
+
+ req := authser.UpdateProfileReq{
+ Avatar: body.Avatar,
+ Introduction: body.Introduction,
+ NickName: body.NickName,
+ OldPassword: body.OldPassword,
+ NewPassword: body.NewPassword,
+ PrivacyMode: body.PrivacyMode,
+ }
+ if req.Avatar == "" && req.Introduction == "" && req.NickName == "" && req.OldPassword == "" && req.NewPassword == "" && req.PrivacyMode == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请至少修改一项资料",
+ "data": nil,
+ })
+ return
+ }
+ if (req.OldPassword == "") != (req.NewPassword == "") {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "修改密码需同时填写旧密码和新密码",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := authser.UpdateProfile(c, req)
+ if err != nil {
+ log.Error("update profile upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "update profile failed",
+ "tip": "保存失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = authser.UpdateProfile(c, req)
+ if err != nil {
+ log.Error("update profile retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "update profile failed",
+ "tip": "保存失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "保存失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ // 上游成功后拉最新资料写 cookie
+ _, user, infoErr := authser.GetInfo(c)
+ if infoErr != nil || user == nil {
+ log.Error("update profile getInfo failed", log.E(infoErr))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": env.Msg,
+ "tip": firstNonEmpty(env.Tip, "保存成功"),
+ "data": nil,
+ })
+ return
+ }
+ authser.SetAuthCookies(c, user)
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": env.Msg,
+ "tip": firstNonEmpty(env.Tip, "保存成功"),
+ "data": user,
+ })
+}
+
+// UploadImage 代理上游图片上传(头像不加 watermark)。
+func UploadImage(c *gin.Context) {
+ fileHeader, err := c.FormFile("file")
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请选择图片文件",
+ "data": nil,
+ })
+ return
+ }
+ f, err := fileHeader.Open()
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "open file failed",
+ "tip": "读取图片失败",
+ "data": nil,
+ })
+ return
+ }
+ defer f.Close()
+
+ resp, err := authser.UploadImage(c, fileHeader.Filename, f)
+ if err != nil {
+ log.Error("upload image upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "upload failed",
+ "tip": "上传失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if resp == nil || resp.Code != 200 || resp.Data == nil || strings.TrimSpace(resp.Data.SourceNameFs) == "" {
+ tip := "上传失败"
+ msg := ""
+ code := 500
+ if resp != nil {
+ code = resp.Code
+ msg = resp.Msg
+ tip = firstNonEmpty(resp.Tip, resp.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(resp.Msg, "success"),
+ "tip": firstNonEmpty(resp.Tip, "上传成功"),
+ "data": gin.H{
+ "id": resp.Data.ID,
+ "sourceNameFs": resp.Data.SourceNameFs,
+ "url": resp.Data.SourceNameFs,
+ },
+ })
+}
+
+// GetUserInfo 代理上游 GET /api/app/auth/getInfo。
+func GetUserInfo(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录",
+ "data": nil,
+ })
+ return
+ }
+
+ env, user, err := authser.GetInfo(c)
+ if err != nil {
+ log.Error("getInfo upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "getInfo failed",
+ "tip": "获取用户信息失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, user, err = authser.GetInfo(c)
+ if err != nil {
+ log.Error("getInfo retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "getInfo failed",
+ "tip": "获取用户信息失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 || user == nil {
+ tip := "获取用户信息失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ authser.SetAuthCookies(c, user)
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": user,
+ })
+}
+
+func firstNonEmpty(values ...string) string {
+ for _, v := range values {
+ if strings.TrimSpace(v) != "" {
+ return v
+ }
+ }
+ return ""
+}
diff --git a/app/api/web/auth_redirect.go b/app/api/web/auth_redirect.go
new file mode 100644
index 0000000..2f5464a
--- /dev/null
+++ b/app/api/web/auth_redirect.go
@@ -0,0 +1,57 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// LoginEntryURL 首页弹窗登录入口;redirectPath 为登录成功后回跳路径(须为站内相对路径)。
+func LoginEntryURL(redirectPath string) string {
+ redirectPath = strings.TrimSpace(redirectPath)
+ if i := strings.IndexByte(redirectPath, '?'); i >= 0 {
+ redirectPath = redirectPath[:i]
+ }
+ if !isSafeRedirect(redirectPath) || redirectPath == "/" {
+ return "/login/"
+ }
+ return "/login/return" + ensureRedirectTrailingSlash(redirectPath)
+}
+
+// RegisterEntryURL 首页弹窗注册入口。
+func RegisterEntryURL() string {
+ return "/register/"
+}
+
+func ensureRedirectTrailingSlash(path string) string {
+ if i := strings.IndexByte(path, '?'); i >= 0 {
+ path = path[:i]
+ }
+ if path != "/" && !strings.HasSuffix(path, "/") {
+ path += "/"
+ }
+ return path
+}
+
+// RedirectToLogin 未登录访问受保护页时,跳到首页并打开登录弹窗。
+func RedirectToLogin(c *gin.Context) {
+ path := ""
+ if c != nil && c.Request != nil && c.Request.URL != nil {
+ path = c.Request.URL.RequestURI()
+ }
+ if path == "" || path == "/login" || strings.HasPrefix(path, "/login/") {
+ path = "/"
+ }
+ c.Redirect(http.StatusFound, LoginEntryURL(path))
+}
+
+func isSafeRedirect(path string) bool {
+ if path == "" || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") {
+ return false
+ }
+ if strings.ContainsAny(path, "\r\n") {
+ return false
+ }
+ return true
+}
diff --git a/app/api/web/category_detail.go b/app/api/web/category_detail.go
new file mode 100644
index 0000000..c16a2f3
--- /dev/null
+++ b/app/api/web/category_detail.go
@@ -0,0 +1,72 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// CategoryDetail 分类详情页:按分类 slug 查询,slug 缺失时兼容数值 ID。
+func CategoryDetail(c *gin.Context) {
+ identifier := strings.TrimSpace(c.Param("slug"))
+ if identifier == "" {
+ NotFound(c, "分类不存在")
+ return
+ }
+
+ q := playletser.ParseCategoryDetailQuery(identifier, routeOrQuery(c, "page", "page"))
+ raw := playletser.FetchCategoryDetailOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+ if emptyScopedPage(true, raw.TotalCount, len(raw.List)) {
+ NotFound(c, "分类不存在")
+ return
+ }
+
+ // 接口返回了规范 slug 时优先使用 slug;没有 slug 才继续使用 ID。
+ if slug := strings.TrimSpace(raw.CategorySlug); slug != "" {
+ q.CategorySlug = slug
+ q.CategoryID = raw.CategoryID
+ } else if raw.CategoryID > 0 {
+ q.CategoryID = raw.CategoryID
+ }
+ if redirectCanonical(c, playletser.CategoryDetailCanonicalPath(q)) {
+ return
+ }
+
+ pageName := strings.TrimSpace(raw.CategoryName)
+ if pageName == "" {
+ pageName = strings.TrimSpace(raw.SeoInfo.Title)
+ }
+ if pageName == "" {
+ pageName = identifier
+ }
+ description := strings.TrimSpace(raw.SeoInfo.Description)
+ if description == "" {
+ description = "汇集近期最新发布的短剧、漫剧、AI换脸、AI魔改等内容,持续更新不同题材与类型的新作品,方便快速发现站内最近上线和最新更新的精彩内容。"
+ }
+ title := playletser.CategoryDetailPageTitle(pageName, q.PageNum, raw.SeoInfo)
+
+ data := gin.H{
+ "title": title,
+ "pageKey": "category_detail",
+ "pageName": pageName,
+ "pageTitle": pageName,
+ "categoryDescription": description,
+ "sectionTitle": "最新上架作品",
+ "pageNum": q.PageNum,
+ "pagePath": playletser.CategoryDetailCanonicalPath(q),
+ "seoInfo": raw.SeoInfo,
+ "list": raw.List,
+ "totalCount": raw.TotalCount,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildCategoryDetailPagination(q, raw.TotalCount),
+ }
+ if raw.TotalCount <= 0 || len(raw.List) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "categoryDetail.html", data)
+}
diff --git a/app/api/web/chapter_detail.go b/app/api/web/chapter_detail.go
new file mode 100644
index 0000000..a53068c
--- /dev/null
+++ b/app/api/web/chapter_detail.go
@@ -0,0 +1,50 @@
+package web
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+ "videoweb/app/service/authser"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ChapterDetail 代理上游 GET /api/app/playlet/chapter/detail(播放页 CSR 切集)。
+func ChapterDetail(c *gin.Context) {
+ chapterID, _ := strconv.ParseInt(c.Query("chapterId"), 10, 64)
+ playletID, _ := strconv.ParseInt(c.Query("playletId"), 10, 64)
+ playletSlug := strings.TrimSpace(c.Query("playletSlug"))
+ if chapterID <= 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 chapterId",
+ "data": nil,
+ })
+ return
+ }
+
+ data, err := playletser.FetchChapterDetail(c, chapterID, playletID, playletSlug)
+ if err != nil {
+ log.Error("chapter detail upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter detail failed",
+ "tip": "获取剧集失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if data != nil && useragent.TokenFromRequest(c.Request) != "" {
+ authser.SetRechargeCookie(c, data.Balance)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": "success",
+ "tip": "成功",
+ "data": data,
+ })
+}
diff --git a/app/api/web/chapter_like.go b/app/api/web/chapter_like.go
new file mode 100644
index 0000000..de71915
--- /dev/null
+++ b/app/api/web/chapter_like.go
@@ -0,0 +1,196 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type chapterLikeBody struct {
+ ChapterID int64 `json:"chapterId"`
+ IsLike *bool `json:"isLike"`
+}
+
+// ChapterLikeSet 代理上游 POST /api/app/playlet/chapter/like/set。
+func ChapterLikeSet(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再点赞",
+ "data": nil,
+ })
+ return
+ }
+
+ var body chapterLikeBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ChapterID <= 0 || body.IsLike == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 chapterId 或 isLike",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
+ ChapterID: body.ChapterID,
+ IsLike: *body.IsLike,
+ })
+ if err != nil {
+ log.Error("chapter like set upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter like failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
+ ChapterID: body.ChapterID,
+ IsLike: *body.IsLike,
+ })
+ if err != nil {
+ log.Error("chapter like set retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter like failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "操作失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": env.Data,
+ })
+}
+
+type chapterCollectBody struct {
+ ChapterID int64 `json:"chapterId"`
+ IsCollect *bool `json:"isCollect"`
+}
+
+// ChapterCollectSet 代理上游 POST /api/app/playlet/chapter/collect/set。
+func ChapterCollectSet(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再收藏",
+ "data": nil,
+ })
+ return
+ }
+
+ var body chapterCollectBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ChapterID <= 0 || body.IsCollect == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 chapterId 或 isCollect",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
+ ChapterID: body.ChapterID,
+ IsCollect: *body.IsCollect,
+ })
+ if err != nil {
+ log.Error("chapter collect set upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter collect failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
+ ChapterID: body.ChapterID,
+ IsCollect: *body.IsCollect,
+ })
+ if err != nil {
+ log.Error("chapter collect set retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter collect failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "操作失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/chapter_unlock.go b/app/api/web/chapter_unlock.go
new file mode 100644
index 0000000..afaae08
--- /dev/null
+++ b/app/api/web/chapter_unlock.go
@@ -0,0 +1,106 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/authser"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type chapterUnlockBody struct {
+ ChapterID int64 `json:"chapterId"`
+ PlayletID int64 `json:"playletId"`
+}
+
+// ChapterUnlock 代理上游 POST /api/app/playlet/chapter/unlock。
+func ChapterUnlock(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再解锁",
+ "data": nil,
+ })
+ return
+ }
+
+ var body chapterUnlockBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ChapterID <= 0 || body.PlayletID <= 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 chapterId 或 playletId",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
+ ChapterID: body.ChapterID,
+ PlayletID: body.PlayletID,
+ })
+ if err != nil {
+ log.Error("chapter unlock upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter unlock failed",
+ "tip": "解锁失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
+ ChapterID: body.ChapterID,
+ PlayletID: body.PlayletID,
+ })
+ if err != nil {
+ log.Error("chapter unlock retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter unlock failed",
+ "tip": "解锁失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "解锁失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ authser.SetRechargeCookie(c, env.Data.Balance)
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "解锁成功"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/chapter_watch.go b/app/api/web/chapter_watch.go
new file mode 100644
index 0000000..e8b57b7
--- /dev/null
+++ b/app/api/web/chapter_watch.go
@@ -0,0 +1,103 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type chapterWatchReportBody struct {
+ ChapterID int64 `json:"chapterId"`
+ PlayletID int64 `json:"playletId"`
+}
+
+// ChapterWatchReport 代理上游 POST /api/app/playlet/chapter/watch/report。
+func ChapterWatchReport(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录",
+ "data": nil,
+ })
+ return
+ }
+
+ var body chapterWatchReportBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ChapterID <= 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 chapterId",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
+ ChapterID: body.ChapterID,
+ PlayletID: body.PlayletID,
+ })
+ if err != nil {
+ log.Error("chapter watch report upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter watch report failed",
+ "tip": "上报失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
+ ChapterID: body.ChapterID,
+ PlayletID: body.PlayletID,
+ })
+ if err != nil {
+ log.Error("chapter watch report retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "chapter watch report failed",
+ "tip": "上报失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "上报失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/collect.go b/app/api/web/collect.go
new file mode 100644
index 0000000..d8387ac
--- /dev/null
+++ b/app/api/web/collect.go
@@ -0,0 +1,84 @@
+package web
+
+import (
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Collect 我的收藏 SSR:
+// 短剧 GET /api/app/playlet/collect/list;剧集 GET /api/app/playlet/chapter/collect/list。
+func Collect(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ RedirectToLogin(c)
+ return
+ }
+
+ q := playletser.ParseCollectListQuery(routeOrQuery(c, "page", "page"), routeOrQuery(c, "type", "type"))
+ if redirectCanonical(c, playletser.CollectCanonicalPath(q)) {
+ return
+ }
+ data := gin.H{
+ "title": "我的收藏",
+ "description": "查看你在黄果短剧收藏的短剧与剧集,随时继续追更喜欢的内容。",
+ "navActive": "userCenter",
+ "collectType": q.Type,
+ "pageNum": q.PageNum,
+ "pageSize": q.PageSize,
+ "totalCount": int64(0),
+ "pagination": playletser.BuildCollectPagination(q, 0),
+ }
+ setNoIndex(data, false)
+
+ if q.Type == playletser.CollectTypeChapter {
+ raw, code, err := playletser.FetchChapterCollectList(c, q)
+ if err != nil {
+ aborted, retry := AbortSSRIfSessionExpired(c, code)
+ if aborted {
+ return
+ }
+ if retry {
+ raw, _, err = playletser.FetchChapterCollectList(c, q)
+ }
+ if err != nil {
+ log.Error("fetch playlet chapter collect list failed", log.E(err))
+ raw = &playletser.PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+ }
+ if raw == nil {
+ raw = &playletser.PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+ data["chapterList"] = raw.List
+ data["pageNum"] = raw.PageNum
+ data["pageSize"] = raw.PageSize
+ data["totalCount"] = raw.TotalCount
+ data["pagination"] = playletser.BuildCollectPagination(q, raw.TotalCount)
+ } else {
+ raw, code, err := playletser.FetchCollectList(c, q)
+ if err != nil {
+ aborted, retry := AbortSSRIfSessionExpired(c, code)
+ if aborted {
+ return
+ }
+ if retry {
+ raw, _, err = playletser.FetchCollectList(c, q)
+ }
+ if err != nil {
+ log.Error("fetch playlet collect list failed", log.E(err))
+ raw = &playletser.PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+ }
+ if raw == nil {
+ raw = &playletser.PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+ data["list"] = raw.List
+ data["pageNum"] = raw.PageNum
+ data["pageSize"] = raw.PageSize
+ data["totalCount"] = raw.TotalCount
+ data["pagination"] = playletser.BuildCollectPagination(q, raw.TotalCount)
+ }
+
+ HTMLOK(c, "collect.html", data)
+}
diff --git a/app/api/web/comment.go b/app/api/web/comment.go
new file mode 100644
index 0000000..6d9c17f
--- /dev/null
+++ b/app/api/web/comment.go
@@ -0,0 +1,282 @@
+package web
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+ "videoweb/app/service/commentser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// CommentList 代理上游 GET /api/app/comment/list(加载更多等)。
+func CommentList(c *gin.Context) {
+ objectID, _ := strconv.ParseInt(c.Query("objectId"), 10, 64)
+ objectType, _ := strconv.Atoi(c.Query("objectType"))
+ pageNum, _ := strconv.Atoi(c.Query("pageNum"))
+ pageSize, _ := strconv.Atoi(c.Query("pageSize"))
+ sortOrder, _ := strconv.Atoi(c.Query("sortOrder"))
+
+ q := commentser.ListQuery{
+ ObjectID: objectID,
+ ObjectType: objectType,
+ PageNum: pageNum,
+ PageSize: pageSize,
+ SortOrder: sortOrder,
+ }
+ if q.ObjectID <= 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 objectId",
+ "data": nil,
+ })
+ return
+ }
+
+ data, err := commentser.FetchList(c, q)
+ if err != nil {
+ log.Error("comment list upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "comment list failed",
+ "tip": "获取评论失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": "success",
+ "tip": "成功",
+ "data": data,
+ })
+}
+
+type commentSendBody struct {
+ ObjectID int64 `json:"objectId"`
+ ObjectType int `json:"objectType"`
+ Text string `json:"text"`
+ ParentsID int64 `json:"parentsId"`
+ ReplyCommentID int64 `json:"replyCommentId"`
+}
+
+// CommentSend 代理上游 POST /api/app/comment/send。
+// parentsId=0 一级评论;parentsId>0 二级评论;replyCommentId 为实际被回复评论。
+func CommentSend(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再发表评论",
+ "data": nil,
+ })
+ return
+ }
+
+ var body commentSendBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.Text = strings.TrimSpace(body.Text)
+ if body.ObjectID <= 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 objectId",
+ "data": nil,
+ })
+ return
+ }
+ if body.ObjectType != commentser.ObjectTypeChapter {
+ body.ObjectType = commentser.ObjectTypePlaylet
+ }
+ if body.Text == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请输入评论内容",
+ "data": nil,
+ })
+ return
+ }
+ if utf8.RuneCountInString(body.Text) > 200 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "评论最多 200 字",
+ "data": nil,
+ })
+ return
+ }
+ if body.ParentsID < 0 {
+ body.ParentsID = 0
+ }
+ if body.ReplyCommentID < 0 {
+ body.ReplyCommentID = 0
+ }
+
+ env, err := commentser.Send(c, commentser.CommentSendReq{
+ ObjectID: body.ObjectID,
+ ObjectType: body.ObjectType,
+ Text: body.Text,
+ ParentsID: body.ParentsID,
+ ReplyCommentID: body.ReplyCommentID,
+ })
+ if err != nil {
+ log.Error("comment send upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "comment send failed",
+ "tip": "发表失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = commentser.Send(c, commentser.CommentSendReq{
+ ObjectID: body.ObjectID,
+ ObjectType: body.ObjectType,
+ Text: body.Text,
+ ParentsID: body.ParentsID,
+ ReplyCommentID: body.ReplyCommentID,
+ })
+ if err != nil {
+ log.Error("comment send retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "comment send failed",
+ "tip": "发表失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "发表失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "发表成功"),
+ "data": env.Data,
+ })
+}
+
+type commentLikeBody struct {
+ CommentID int64 `json:"commentId"`
+ IsLike *bool `json:"isLike"`
+}
+
+// CommentLikeSet 代理上游 POST /api/app/comment/like/set。
+func CommentLikeSet(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再点赞",
+ "data": nil,
+ })
+ return
+ }
+
+ var body commentLikeBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.CommentID <= 0 || body.IsLike == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 commentId 或 isLike",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := commentser.LikeSet(c, commentser.CommentLikeSetReq{
+ CommentID: body.CommentID,
+ IsLike: *body.IsLike,
+ })
+ if err != nil {
+ log.Error("comment like set upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "comment like failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = commentser.LikeSet(c, commentser.CommentLikeSetReq{
+ CommentID: body.CommentID,
+ IsLike: *body.IsLike,
+ })
+ if err != nil {
+ log.Error("comment like set retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "comment like failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "操作失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/deposit.go b/app/api/web/deposit.go
new file mode 100644
index 0000000..0bcb66a
--- /dev/null
+++ b/app/api/web/deposit.go
@@ -0,0 +1,244 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+ "videoweb/app/service/authser"
+ "videoweb/app/service/rechargeser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type buyPayBody struct {
+ PackageID int64 `json:"packageId"`
+ PayMode string `json:"payMode"`
+}
+
+// attachWalletFromGetInfo 登录态拉取 getInfo,刷新 cookie,并写入模板金币余额。
+// 返回 true 表示注册账号会话失效,调用方应跳转登录。
+func attachWalletFromGetInfo(c *gin.Context, data gin.H) bool {
+ if data == nil || useragent.TokenFromRequest(c.Request) == "" {
+ return false
+ }
+ env, user, err := authser.GetInfo(c)
+ if err != nil {
+ log.Error("wallet getInfo failed", log.E(err))
+ return false
+ }
+ if env != nil && authser.IsSessionExpiredCode(env.Code) {
+ retry, needLogin := authser.OnSessionExpired(c, env.Code)
+ if needLogin {
+ return true
+ }
+ if retry {
+ env, user, err = authser.GetInfo(c)
+ if err != nil {
+ log.Error("wallet getInfo retry failed", log.E(err))
+ return false
+ }
+ }
+ }
+ if env == nil || env.Code != 200 || user == nil {
+ return false
+ }
+ authser.SetAuthCookies(c, user)
+ data["userId"] = authser.FormatUserID(user.UserID)
+ data["recharge"] = user.Recharge
+ data["rechargeText"] = authser.FormatRecharge(user.Recharge)
+ data["vipType"] = user.VipType
+ data["showCardId"] = user.ShowCardID
+ data["showCardName"] = strings.TrimSpace(user.ShowCardName)
+ active := authser.IsActiveVipUser(user.VipType, user.CurrentVipExpire)
+ data["isVip"] = active
+ if active {
+ data["vipLabel"] = authser.VipDisplayLabel(user.ShowCardName, user.VipType)
+ data["vipExpireText"] = authser.FormatVipExpireText(user.CurrentVipExpire)
+ } else {
+ delete(data, "vipLabel")
+ delete(data, "vipExpireText")
+ }
+ return false
+}
+
+// Deposit 金币与会员充值页 SSR:套餐列表 + 金币流水明细。
+// Query tab=coin 时默认选中金币充值套餐。
+func Deposit(c *gin.Context) {
+ // 先补游客会话,保证可拉流水。
+ authser.EnsureGuestOnEnter(c)
+
+ preferTab := routeOrQuery(c, "tab", "tab")
+ if hasQuery(c, "tab") {
+ target := "/deposit"
+ if preferTab != "" {
+ target += "/" + preferTab
+ }
+ target += "/"
+ if redirectCanonical(c, target) {
+ return
+ }
+ }
+ raw := rechargeser.FetchPackageListOrEmpty(c)
+ vm := rechargeser.BuildDepositPageVM(raw, preferTab)
+ if useragent.TokenFromRequest(c.Request) != "" {
+ list, code, err := rechargeser.FetchCoinTransList(c, 1, 20)
+ if err != nil {
+ retry, needLogin := authser.OnSessionExpired(c, code)
+ if needLogin {
+ RedirectToLogin(c)
+ return
+ }
+ if retry {
+ list, _, err = rechargeser.FetchCoinTransList(c, 1, 20)
+ }
+ if err != nil {
+ log.Error("fetch coin trans list failed", log.E(err))
+ list = nil
+ }
+ }
+ vm.AttachTxList(list)
+ }
+ data := gin.H{
+ "title": "金币与会员",
+ "vipList": vm.VipList,
+ "coinList": vm.CoinList,
+ "payMethods": vm.PayMethods,
+ "txList": vm.TxList,
+ "preferTab": preferTab,
+ }
+ setNoIndex(data, false)
+ if attachWalletFromGetInfo(c, data) {
+ RedirectToLogin(c)
+ return
+ }
+ HTMLOK(c, "deposit.html", data)
+}
+
+// CoinBuy 代理上游 POST /api/app/coin/buy。
+func CoinBuy(c *gin.Context) {
+ handleBuyPay(c, "coin")
+}
+
+// VipPay 代理上游 POST /api/app/vip/pay。
+func VipPay(c *gin.Context) {
+ handleBuyPay(c, "vip")
+}
+
+func handleBuyPay(c *gin.Context, kind string) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再支付",
+ "data": nil,
+ })
+ return
+ }
+
+ var body buyPayBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.PayMode = rechargeser.NormalizePayMode(body.PayMode)
+ if body.PackageID <= 0 || body.PayMode == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请选择套餐和支付方式",
+ "data": nil,
+ })
+ return
+ }
+
+ req := rechargeser.BuyPayReq{
+ PackageID: body.PackageID,
+ PayMode: body.PayMode,
+ }
+
+ var (
+ env *rechargeser.APIEnvelope[rechargeser.PayOrderResp]
+ err error
+ )
+ switch kind {
+ case "coin":
+ env, err = rechargeser.BuyCoin(c, req)
+ default:
+ env, err = rechargeser.VipPay(c, req)
+ }
+ if err != nil {
+ log.Error("buy/pay upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "pay failed",
+ "tip": "发起支付失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ switch kind {
+ case "coin":
+ env, err = rechargeser.BuyCoin(c, req)
+ default:
+ env, err = rechargeser.VipPay(c, req)
+ }
+ if err != nil {
+ log.Error("buy/pay retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "pay failed",
+ "tip": "发起支付失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "发起支付失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ payURL := strings.TrimSpace(env.Data.PayURL)
+ if payURL == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "empty payUrl",
+ "tip": "未获取到支付链接,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "请完成支付"),
+ "data": gin.H{
+ "isOpenNewBrowser": env.Data.IsOpenNewBrowser,
+ "payUrl": payURL,
+ "payUrlMode": env.Data.PayURLMode,
+ "tradeNo": env.Data.TradeNo,
+ },
+ })
+}
diff --git a/app/api/web/detail.go b/app/api/web/detail.go
new file mode 100644
index 0000000..10c5daf
--- /dev/null
+++ b/app/api/web/detail.go
@@ -0,0 +1,32 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Detail 详情页 SSR:GET /api/app/playlet/detail;seoInfo 直接注入模板。
+func Detail(c *gin.Context) {
+ key := strings.TrimSpace(c.Param("slug"))
+ if key == "" {
+ NotFound(c, "未找到该短剧")
+ return
+ }
+ raw := playletser.FetchDetailOrNilByKey(c, key)
+ if raw == nil {
+ NotFound(c, "未找到该短剧")
+ return
+ }
+ // 有 slug 使用 slug,否则保留 ID 路由。
+ if redirectCanonical(c, playletser.DetailPath(raw.Slug, raw.ID)) {
+ return
+ }
+ HTMLOK(c, "detail.html", gin.H{
+ "title": raw.Title,
+ "detail": raw,
+ "seoInfo": raw.SeoInfo,
+ "episodeRanges": playletser.BuildEpisodeRanges(raw.PlayletChapterInfo, raw.TotalEpisode, 30),
+ })
+}
diff --git a/app/api/web/dialog.go b/app/api/web/dialog.go
new file mode 100644
index 0000000..1c341c9
--- /dev/null
+++ b/app/api/web/dialog.go
@@ -0,0 +1,34 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/configser"
+
+ "github.com/gin-gonic/gin"
+)
+
+var dialogTemplates = map[string]string{
+ "download": "downloadAppDialog",
+ "login": "loginDialog",
+ "register": "registerDialog",
+ "search": "searchDialog",
+}
+
+// DialogFragment renders only the requested dialog body. Styles and behavior
+// are loaded by dialogLoader.js after the first user interaction.
+func DialogFragment(c *gin.Context) {
+ name := c.Param("name")
+ templateName, ok := dialogTemplates[name]
+ if !ok {
+ c.Status(http.StatusNotFound)
+ return
+ }
+
+ data := gin.H{}
+ if name == "download" {
+ data["appConfig"] = configser.FetchAppConfigVMOrEmpty(c)
+ }
+
+ c.Header("Cache-Control", "private, max-age=300")
+ c.HTML(http.StatusOK, templateName, data)
+}
diff --git a/app/api/web/feedback.go b/app/api/web/feedback.go
new file mode 100644
index 0000000..b62dd41
--- /dev/null
+++ b/app/api/web/feedback.go
@@ -0,0 +1,209 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+ "unicode/utf8"
+ "videoweb/app/service/feedbackser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type feedbackBody struct {
+ Type int `json:"type"`
+ Content string `json:"content"`
+ Contact string `json:"contact"`
+ Screenshots []string `json:"screenshots"`
+}
+
+type contactUsBody struct {
+ Type int `json:"type"`
+ Title string `json:"title"`
+ Content string `json:"content"`
+ Email string `json:"email"`
+ Attachments []string `json:"attachments"`
+}
+
+// AddFeedback 代理上游 POST /api/app/feedback/add。
+func AddFeedback(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再提交反馈",
+ "data": nil,
+ })
+ return
+ }
+
+ var body feedbackBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+
+ body.Content = strings.TrimSpace(body.Content)
+ body.Contact = strings.TrimSpace(body.Contact)
+ if body.Type < 1 || body.Type > 4 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请选择问题类型",
+ "data": nil,
+ })
+ return
+ }
+ if body.Content == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "请填写问题描述",
+ "data": nil,
+ })
+ return
+ }
+ if utf8.RuneCountInString(body.Content) > 500 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "问题描述最多 500 字",
+ "data": nil,
+ })
+ return
+ }
+
+ shots := make([]string, 0, len(body.Screenshots))
+ for _, s := range body.Screenshots {
+ s = strings.TrimSpace(s)
+ if s != "" {
+ shots = append(shots, s)
+ }
+ }
+ if len(shots) > 10 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "最多上传 10 张图片",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := feedbackser.Add(c, feedbackser.FeedbackReq{
+ Type: body.Type,
+ Content: body.Content,
+ Contact: body.Contact,
+ Screenshots: shots,
+ })
+ if err != nil {
+ log.Error("feedback add upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "feedback failed",
+ "tip": "提交失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = feedbackser.Add(c, feedbackser.FeedbackReq{
+ Type: body.Type,
+ Content: body.Content,
+ Contact: body.Contact,
+ Screenshots: shots,
+ })
+ if err != nil {
+ log.Error("feedback add retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "feedback failed",
+ "tip": "提交失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "提交失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "提交成功"),
+ "data": env.Data,
+ })
+}
+
+// AddContactUs 代理上游 POST /api/app/contactus/add。
+func AddContactUs(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录后再提交", "data": nil})
+ return
+ }
+ var body contactUsBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"code": 1001, "msg": "参数错误", "tip": "参数错误", "data": nil})
+ return
+ }
+ body.Title = strings.TrimSpace(body.Title)
+ body.Content = strings.TrimSpace(body.Content)
+ body.Email = strings.TrimSpace(body.Email)
+ if body.Type < 1 || body.Type > 5 || body.Title == "" || body.Content == "" || body.Email == "" {
+ c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "请完整填写问题类型、标题、内容和邮箱", "data": nil})
+ return
+ }
+ attachments := make([]string, 0, len(body.Attachments))
+ for _, attachment := range body.Attachments {
+ if attachment = strings.TrimSpace(attachment); attachment != "" {
+ attachments = append(attachments, attachment)
+ }
+ }
+ req := feedbackser.ContactUsAddReq{
+ Type: body.Type, Title: body.Title, Content: body.Content, Email: body.Email, Attachments: attachments,
+ }
+ env, err := feedbackser.AddContactUs(c, req)
+ if err == nil && env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = feedbackser.AddContactUs(c, req)
+ }
+ if err != nil {
+ log.Error("contact us upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "contact us failed", "tip": "提交失败,请稍后重试", "data": nil})
+ return
+ }
+ if env == nil {
+ c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "contact us failed", "tip": "提交失败", "data": nil})
+ return
+ }
+ defaultTip := "提交失败"
+ if env.Code == 200 {
+ defaultTip = "提交成功"
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": env.Code,
+ "msg": env.Msg,
+ "tip": firstNonEmpty(env.Tip, env.Msg, defaultTip),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/history.go b/app/api/web/history.go
new file mode 100644
index 0000000..9cf7c94
--- /dev/null
+++ b/app/api/web/history.go
@@ -0,0 +1,117 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// History 观看历史 SSR:GET /api/app/playlet/history/list。
+func History(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ RedirectToLogin(c)
+ return
+ }
+
+ q := playletser.ParseHistoryListQuery(routeOrQuery(c, "page", "page"))
+ if redirectCanonical(c, playletser.HistoryCanonicalPath(q)) {
+ return
+ }
+ raw, code, err := playletser.FetchHistoryList(c, q)
+ if err != nil {
+ aborted, retry := AbortSSRIfSessionExpired(c, code)
+ if aborted {
+ return
+ }
+ if retry {
+ raw, _, err = playletser.FetchHistoryList(c, q)
+ }
+ if err != nil {
+ log.Error("fetch playlet history list failed", log.E(err))
+ raw = &playletser.PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+ }
+ if raw == nil {
+ raw = &playletser.PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+
+ data := gin.H{
+ "title": "观看历史",
+ "description": "查看黄果短剧观看历史,快速回到上次看到的剧集继续观看。",
+ "navActive": "userCenter",
+ "list": raw.List,
+ "pageNum": raw.PageNum,
+ "pageSize": raw.PageSize,
+ "totalCount": raw.TotalCount,
+ "pagination": playletser.BuildHistoryPagination(q, raw.TotalCount),
+ "emptyTitle": "暂无观看历史",
+ "emptyDesc": "开始播放短剧后,记录会出现在这里",
+ }
+ setNoIndex(data, false)
+ HTMLOK(c, "history.html", data)
+}
+
+// HistoryClear 代理上游 POST /api/app/playlet/history/clear。
+func HistoryClear(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.HistoryClear(c)
+ if err != nil {
+ log.Error("playlet history clear upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "history clear failed",
+ "tip": "清空失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.HistoryClear(c)
+ if err != nil {
+ log.Error("playlet history clear retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "history clear failed",
+ "tip": "清空失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "清空失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "已清空观看历史"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/home_road.go b/app/api/web/home_road.go
new file mode 100644
index 0000000..61a9f76
--- /dev/null
+++ b/app/api/web/home_road.go
@@ -0,0 +1,28 @@
+package web
+
+import (
+ "videoweb/app/service/configser"
+ "videoweb/app/service/seoser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HomeRoad 渲染黄果短剧最新地址与防失联入口页。
+func HomeRoad(c *gin.Context) {
+ cfg := configser.FetchAppConfigVMOrEmpty(c)
+ HTMLOK(c, "homeRoad.html", gin.H{
+ "title": "回家的路 - 黄果短剧官方最新地址发布页",
+ "pageKey": "home_road",
+ "pageName": "回家的路",
+ "pagePath": "/home-road/",
+ "navActive": "homeRoad",
+ "appConfig": cfg,
+ "homeRoad": configser.BuildHomeRoadPageVM(cfg),
+ "faqItems": []seoser.FAQItem{
+ {Question: "回家的路页面有什么作用?", Answer: "集中展示最新地址、永久入口、备用入口和保存方式,方便访问异常时快速返回黄果短剧。"},
+ {Question: "建议保存哪个入口?", Answer: "优先收藏当前推荐入口,并额外保存永久入口或备用入口。"},
+ {Question: "入口打不开怎么办?", Answer: "先切换网络,再尝试永久入口或备用入口;仍无法访问时返回本页查看最新地址。"},
+ {Question: "如何确认入口是否安全?", Answer: "只从本页、站内导航或黄果社区提示进入,不要从陌生弹窗、未知短链跳转。"},
+ },
+ })
+}
diff --git a/app/api/web/hot_like.go b/app/api/web/hot_like.go
new file mode 100644
index 0000000..29a1434
--- /dev/null
+++ b/app/api/web/hot_like.go
@@ -0,0 +1,30 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HotLike 代理上游 GET /api/app/playlet/hot-like。
+func HotLike(c *gin.Context) {
+ data, err := playletser.FetchHotLike(c)
+ if err != nil {
+ log.Error("playlet hot-like upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "playlet hot-like failed",
+ "tip": "获取热搜和猜你喜欢失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": "success",
+ "tip": "成功",
+ "data": data,
+ })
+}
diff --git a/app/api/web/index.go b/app/api/web/index.go
new file mode 100644
index 0000000..3c6122c
--- /dev/null
+++ b/app/api/web/index.go
@@ -0,0 +1,39 @@
+package web
+
+import (
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Index 首页 SSR:拉取 /api/app/playlet/home,直接用接口字段渲染。
+func Index(c *gin.Context) {
+ renderIndex(c, "")
+}
+
+func AuthIndex(mode string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ renderIndex(c, mode)
+ }
+}
+
+func renderIndex(c *gin.Context, authMode string) {
+ authRedirect := c.Param("redirect")
+ raw := playletser.FetchHomeOrEmpty(c)
+ data := gin.H{
+ "title": "黄果短剧",
+ "navActive": "home",
+ "homeResponse": raw,
+ "seoInfo": raw.SeoInfo,
+ "announcements": raw.Announcements,
+ "homeCategoryData": raw.HomeCategoryData,
+ "homeRecommendThemes": raw.HomeRecommendThemes,
+ "homeBannerData": playletser.ActiveHomeBanners(raw.HomeBannerData),
+ "authMode": authMode,
+ "authRedirect": authRedirect,
+ }
+ if authMode != "" {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "index.html", data)
+}
diff --git a/app/api/web/list.go b/app/api/web/list.go
new file mode 100644
index 0000000..0cbc828
--- /dev/null
+++ b/app/api/web/list.go
@@ -0,0 +1,61 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// List 短剧列表页 SSR:GET /api/app/playlet/operate/list,operateType 固定为 1。
+func List(c *gin.Context) {
+ slug := strings.TrimSpace(c.Param("slug"))
+ if slug == "ai-series" {
+ legacy := playletser.ParseListQuery(
+ routeOrQuery(c, "page", "page"),
+ routeOrQuery(c, "sortOrder", "sortOrder"),
+ c.Query("type"),
+ )
+ redirectCanonical(c, playletser.SeriesPathFromLegacyList(legacy.SortOrder, legacy.PageNum))
+ return
+ }
+ filter := routeOrQuery(c, "sortOrder", "filterType")
+ if filter == "" {
+ filter = strings.TrimSpace(c.Query("sortOrder"))
+ }
+ q, ok := playletser.ParseShortPlayQuery(filter, routeOrQuery(c, "page", "page"))
+ if !ok {
+ NotFound(c, "页面不存在")
+ return
+ }
+ if redirectCanonical(c, playletser.ShortPlayCanonicalPath(q)) {
+ return
+ }
+ raw := playletser.FetchOperateListOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+ sectionTitle := playletser.ShortPlaySectionTitle(q.FilterType)
+ data := gin.H{
+ "title": "短剧",
+ "description": "浏览黄果短剧全部短剧列表,按分类、标签筛选海量高清短剧,发现更多精彩剧集。",
+ "pageKey": "list",
+ "pageName": "AI成人短剧",
+ "pagePath": playletser.ShortPlayCanonicalPath(q),
+ "navActive": "list",
+ "seoInfo": playletser.SeoInfo{},
+ "seoSelections": []playletser.SEOSelection{{Dimension: "筛选", Value: strings.TrimSuffix(sectionTitle, "的AI成人短剧")}},
+ "filters": playletser.BuildShortPlayFilterTabs(q),
+ "sectionTitle": sectionTitle,
+ "list": raw.List,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildShortPlayPagination(q, raw.TotalCount),
+ }
+ // /list/ai-short-play/original/ 入口页始终可索引;其余空结果仍 noindex。
+ isOriginalEntry := q.FilterType == 3 && q.PageNum <= 1
+ if !isOriginalEntry && (raw.TotalCount <= 0 || len(raw.List) == 0) {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "list.html", data)
+}
diff --git a/app/api/web/more.go b/app/api/web/more.go
new file mode 100644
index 0000000..5dd7eb8
--- /dev/null
+++ b/app/api/web/more.go
@@ -0,0 +1,83 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// More 专题详情页 SSR:专题由路由 slug 标识;历史 /more 地址会跳转到 /topic canonical。
+func More(c *gin.Context) {
+ q := playletser.ParseRecommendQuery(routeOrQuery(c, "page", "page"))
+ if slug := strings.TrimSpace(c.Param("slug")); slug != "" {
+ if id := playletser.ParseDetailID(slug); id > 0 {
+ q.ThemeID = id
+ } else {
+ q.ThemeSlug = slug
+ }
+ }
+ if path := c.Request.URL.Path; path == "/more" || strings.HasPrefix(path, "/more/") {
+ redirectCanonical(c, playletser.RecommendPagePath(q))
+ return
+ }
+ themes := playletser.FetchThemesOrEmpty(c, 2)
+ themeName := ""
+ for _, theme := range themes {
+ matchesID := q.ThemeID > 0 && theme.ID == q.ThemeID
+ matchesSlug := strings.TrimSpace(q.ThemeSlug) != "" && strings.EqualFold(strings.TrimSpace(theme.Slug), strings.TrimSpace(q.ThemeSlug))
+ if !matchesID && !matchesSlug {
+ continue
+ }
+ themeName = strings.TrimSpace(theme.Name)
+ if q.ThemeID <= 0 {
+ q.ThemeID = theme.ID
+ }
+ if strings.TrimSpace(theme.Slug) != "" {
+ q.ThemeSlug = strings.TrimSpace(theme.Slug)
+ }
+ break
+ }
+ if redirectCanonical(c, playletser.RecommendPagePath(q)) {
+ return
+ }
+ raw := playletser.FetchRecommendListOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+ hasIdentifier := q.ThemeID > 0 || strings.TrimSpace(q.ThemeSlug) != ""
+ if emptyScopedPage(hasIdentifier, raw.TotalCount, len(raw.List)) {
+ NotFound(c, "专题不存在")
+ return
+ }
+ title := playletser.RecommendThemeTitle(themeName, raw.SeoInfo.Title, q.ThemeID)
+ if queryTitle := strings.TrimSpace(c.Query("title")); queryTitle != "" {
+ title = queryTitle
+ }
+ heading := title
+ if !strings.HasSuffix(heading, "介绍") {
+ heading += "介绍"
+ }
+ description := strings.TrimSpace(raw.SeoInfo.Description)
+ if description == "" {
+ description = "精品高分 AI 成人短剧专题,精选高人气、高评分 AI 原创短剧,每日持续更新。涵盖多种热门剧情与角色设定,为你带来高质量、高清流畅的观影体验。"
+ }
+ data := gin.H{
+ "title": title,
+ "pageKey": "topic",
+ "pageName": title,
+ "pageTitle": title,
+ "topicHeading": heading,
+ "topicDescription": description,
+ "pagePath": playletser.RecommendCanonicalPath(q.ThemeID, q.ThemeSlug),
+ "seoInfo": raw.SeoInfo,
+ "list": raw.List,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildRecommendPagination(q, raw.TotalCount),
+ }
+ if raw.TotalCount <= 0 || len(raw.List) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "more.html", data)
+}
diff --git a/app/api/web/nav.go b/app/api/web/nav.go
new file mode 100644
index 0000000..4a0f251
--- /dev/null
+++ b/app/api/web/nav.go
@@ -0,0 +1,15 @@
+package web
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// Nav 官方导航页 SSR:直接使用 config/info 注入的 appConfig。
+func Nav(c *gin.Context) {
+ HTMLOK(c, "nav.html", gin.H{
+ "title": "黄果短剧 · 官方导航",
+ "isNavPage": true,
+ "pageKey": "nav",
+ "pageName": "官方导航",
+ })
+}
diff --git a/app/api/web/notfound.go b/app/api/web/notfound.go
new file mode 100644
index 0000000..b9eabdb
--- /dev/null
+++ b/app/api/web/notfound.go
@@ -0,0 +1,27 @@
+package web
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+// NotFound 统一 404 页(HTTP 404)。
+func NotFound(c *gin.Context, title string) {
+ if title == "" {
+ title = "页面未找到"
+ }
+ data := gin.H{
+ "title": title,
+ "path": c.Request.URL.RequestURI(),
+ }
+ setNoIndex(data, true)
+ HTML(c, http.StatusNotFound, "404.html", data)
+}
+
+// emptyScopedPage 判断带明确资源标识的列表详情是否不存在。
+// 聚合页允许暂时为空;标签、分类、专题等详情页成功返回空结果时应响应 404,
+// 避免用空模板和 HTTP 200 形成搜索引擎软 404。
+func emptyScopedPage(hasIdentifier bool, totalCount int64, itemCount int) bool {
+ return hasIdentifier && totalCount <= 0 && itemCount == 0
+}
diff --git a/app/api/web/notfound_test.go b/app/api/web/notfound_test.go
new file mode 100644
index 0000000..190abc7
--- /dev/null
+++ b/app/api/web/notfound_test.go
@@ -0,0 +1,26 @@
+package web
+
+import "testing"
+
+func TestEmptyScopedPage(t *testing.T) {
+ tests := []struct {
+ name string
+ hasIdentifier bool
+ totalCount int64
+ itemCount int
+ want bool
+ }{
+ {name: "missing detail", hasIdentifier: true, want: true},
+ {name: "empty aggregate page", hasIdentifier: false, want: false},
+ {name: "has total", hasIdentifier: true, totalCount: 1, want: false},
+ {name: "has item", hasIdentifier: true, itemCount: 1, want: false},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := emptyScopedPage(tt.hasIdentifier, tt.totalCount, tt.itemCount); got != tt.want {
+ t.Fatalf("emptyScopedPage(%v, %d, %d) = %v, want %v", tt.hasIdentifier, tt.totalCount, tt.itemCount, got, tt.want)
+ }
+ })
+ }
+}
diff --git a/app/api/web/play.go b/app/api/web/play.go
new file mode 100644
index 0000000..2035b26
--- /dev/null
+++ b/app/api/web/play.go
@@ -0,0 +1,76 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/authser"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Play 播放页 SSR:GET /api/app/playlet/chapter/detail;seoInfo 直接注入模板。
+func Play(c *gin.Context) {
+ playletKey := strings.TrimSpace(c.Param("slug"))
+ requestedChapterID := strings.TrimSpace(c.Param("chapterId"))
+ if playletKey == "" {
+ NotFound(c, "未找到可播放内容")
+ return
+ }
+ if requestedChapterID != "" && playletser.ParseDetailID(requestedChapterID) <= 0 {
+ NotFound(c, "未找到该剧集")
+ return
+ }
+ playlet := playletser.FetchDetailOrNilByKey(c, playletKey)
+ if playlet == nil {
+ NotFound(c, "未找到可播放内容")
+ return
+ }
+ q := playletser.ParsePlayQuery(playletKey, c.Param("chapterId"))
+ q.PlayletID = playlet.ID
+ q.PlayletSlug = playlet.Slug
+
+ q.ChapterID = playletser.ResolvePlayChapterID(playlet, q.ChapterID)
+ if q.ChapterID <= 0 {
+ NotFound(c, "未找到可播放内容")
+ return
+ }
+ target := playletser.PlayPath(q.PlayletSlug, q.PlayletID)
+ if q.ChapterID > 0 {
+ target = playletser.ChapterPlayPath(q.PlayletSlug, q.PlayletID, q.ChapterID)
+ }
+ if redirectCanonical(c, target) {
+ return
+ }
+
+ chapter := playletser.FetchChapterDetailOrNil(c, q.ChapterID, q.PlayletID, q.PlayletSlug)
+ if chapter == nil {
+ NotFound(c, "未找到该剧集")
+ return
+ }
+
+ var episodeRanges []playletser.EpisodeRange
+ var chapters []playletser.PlayletChapterData
+ playletID := playlet.ID
+ playletSlug := playlet.Slug
+ chapters = playlet.PlayletChapterInfo
+ episodeRanges = playletser.BuildEpisodeRanges(chapters, playlet.TotalEpisode, 30)
+
+ if chapter != nil && useragent.TokenFromRequest(c.Request) != "" {
+ authser.SetRechargeCookie(c, chapter.Balance)
+ }
+
+ seoInfo := playletser.ResolvePlayPageSEO(chapter, playlet)
+
+ data := gin.H{
+ "title": playletser.ResolvePlayPageTitle(chapter, playlet),
+ "chapter": chapter,
+ "playlet": playlet,
+ "playletID": playletID,
+ "playletSlug": playletSlug,
+ "seoInfo": seoInfo,
+ "chapters": chapters,
+ "episodeRanges": episodeRanges,
+ }
+ HTMLOK(c, "play.html", data)
+}
diff --git a/app/api/web/playlet_api.go b/app/api/web/playlet_api.go
new file mode 100644
index 0000000..76d8a62
--- /dev/null
+++ b/app/api/web/playlet_api.go
@@ -0,0 +1,54 @@
+package web
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ChapterList 代理上游短剧选集列表接口。
+func ChapterList(c *gin.Context) {
+ playletID, _ := strconv.ParseInt(c.Query("playletId"), 10, 64)
+ playletSlug := strings.TrimSpace(c.Query("playletSlug"))
+ if playletSlug == "" && playletID <= 0 {
+ c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "缺少 playletSlug 或 playletId", "data": nil})
+ return
+ }
+ data, err := playletser.FetchChapterList(c, playletID, playletSlug)
+ if err != nil {
+ log.Error("chapter list upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "chapter list failed", "tip": "获取选集失败,请稍后重试", "data": nil})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success", "tip": "成功", "data": data})
+}
+
+// OperateList 代理上游运营位短剧列表接口。
+func OperateList(c *gin.Context) {
+ q := playletser.OperateListQuery{
+ OperateType: parsePositiveInt(c.Query("operateType")),
+ FilterType: parsePositiveInt(c.Query("filterType")),
+ PageNum: parsePositiveInt(c.Query("pageNum")),
+ PageSize: parsePositiveInt(c.Query("pageSize")),
+ }
+ q.Normalize()
+ data, err := playletser.FetchOperateList(c, q)
+ if err != nil {
+ log.Error("operate list upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "operate list failed", "tip": "获取运营短剧失败,请稍后重试", "data": nil})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success", "tip": "成功", "data": data})
+}
+
+func parsePositiveInt(raw string) int {
+ n, err := strconv.Atoi(strings.TrimSpace(raw))
+ if err != nil || n <= 0 {
+ return 0
+ }
+ return n
+}
diff --git a/app/api/web/playlet_forward_heat.go b/app/api/web/playlet_forward_heat.go
new file mode 100644
index 0000000..6e05d4b
--- /dev/null
+++ b/app/api/web/playlet_forward_heat.go
@@ -0,0 +1,72 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+type playletForwardHeatBody struct {
+ ID int64 `json:"id"`
+}
+
+// PlayletForwardHeat 代理上游 POST /api/app/playlet/forward/heat。
+func PlayletForwardHeat(c *gin.Context) {
+ var body playletForwardHeatBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ID <= 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 id",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.ForwardHeat(c, playletser.PlayletForwardHeatReq{ID: body.ID})
+ if err != nil {
+ log.Error("playlet forward heat upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "playlet forward heat failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env == nil || env.Code != 200 {
+ tip := "操作失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": env.Msg,
+ "tip": firstNonEmpty(env.Tip, env.Msg, "success"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/playlet_like.go b/app/api/web/playlet_like.go
new file mode 100644
index 0000000..6887ff0
--- /dev/null
+++ b/app/api/web/playlet_like.go
@@ -0,0 +1,196 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+type playletLikeBody struct {
+ ID int64 `json:"id"`
+ IsLike *bool `json:"isLike"`
+}
+
+// PlayletLikeSet 代理上游 POST /api/app/playlet/like/set。
+func PlayletLikeSet(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再点赞",
+ "data": nil,
+ })
+ return
+ }
+
+ var body playletLikeBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ID <= 0 || body.IsLike == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 id 或 isLike",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.LikeSet(c, playletser.PlayletLikeSetReq{
+ ID: body.ID,
+ IsLike: *body.IsLike,
+ })
+ if err != nil {
+ log.Error("playlet like set upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "playlet like failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.LikeSet(c, playletser.PlayletLikeSetReq{
+ ID: body.ID,
+ IsLike: *body.IsLike,
+ })
+ if err != nil {
+ log.Error("playlet like set retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "playlet like failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "操作失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": env.Data,
+ })
+}
+
+type playletCollectBody struct {
+ ID int64 `json:"id"`
+ IsCollect *bool `json:"isCollect"`
+}
+
+// PlayletCollectSet 代理上游 POST /api/app/playlet/collect/set。
+func PlayletCollectSet(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 401,
+ "msg": "unauthorized",
+ "tip": "请先登录后再收藏",
+ "data": nil,
+ })
+ return
+ }
+
+ var body playletCollectBody
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "参数错误",
+ "data": nil,
+ })
+ return
+ }
+ if body.ID <= 0 || body.IsCollect == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "code": 1001,
+ "msg": "参数错误",
+ "tip": "缺少 id 或 isCollect",
+ "data": nil,
+ })
+ return
+ }
+
+ env, err := playletser.CollectSet(c, playletser.PlayletCollectSetReq{
+ ID: body.ID,
+ IsCollect: *body.IsCollect,
+ })
+ if err != nil {
+ log.Error("playlet collect set upstream failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "playlet collect failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ if env != nil && TryJSONSessionRecover(c, env.Code) {
+ env, err = playletser.CollectSet(c, playletser.PlayletCollectSetReq{
+ ID: body.ID,
+ IsCollect: *body.IsCollect,
+ })
+ if err != nil {
+ log.Error("playlet collect set retry failed", log.E(err))
+ c.JSON(http.StatusOK, gin.H{
+ "code": 500,
+ "msg": "playlet collect failed",
+ "tip": "操作失败,请稍后重试",
+ "data": nil,
+ })
+ return
+ }
+ }
+ if env == nil || env.Code != 200 {
+ tip := "操作失败"
+ msg := ""
+ code := 500
+ if env != nil {
+ code = env.Code
+ msg = env.Msg
+ tip = firstNonEmpty(env.Tip, env.Msg, tip)
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "msg": msg,
+ "tip": tip,
+ "data": nil,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": firstNonEmpty(env.Msg, "success"),
+ "tip": firstNonEmpty(env.Tip, "成功"),
+ "data": env.Data,
+ })
+}
diff --git a/app/api/web/rank.go b/app/api/web/rank.go
new file mode 100644
index 0000000..f5046df
--- /dev/null
+++ b/app/api/web/rank.go
@@ -0,0 +1,82 @@
+package web
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Rank 排行榜页 SSR:GET /api/app/playlet/rank,列表用接口字段渲染。
+func Rank(c *gin.Context) {
+ sortOrder := strings.TrimSpace(c.Param("sortOrder"))
+ if sortOrder != "" {
+ value, err := strconv.Atoi(sortOrder)
+ if err != nil || value < 1 || value > 3 {
+ NotFound(c, "页面不存在")
+ return
+ }
+ }
+ if sortOrder == "" {
+ sortOrder = strings.TrimSpace(c.Query("sortOrder"))
+ }
+ page := strings.TrimSpace(c.Param("page"))
+ if page != "" {
+ value, err := strconv.Atoi(page)
+ if err != nil || value <= 0 {
+ NotFound(c, "页面不存在")
+ return
+ }
+ }
+ if page == "" {
+ page = strings.TrimSpace(c.Query("page"))
+ }
+ q := playletser.ParseRankQuery(page, sortOrder)
+ if c.Query("sortOrder") != "" || c.Query("page") != "" {
+ target := "/rank"
+ if q.SortOrder != 1 {
+ target += "/" + strconv.Itoa(q.SortOrder)
+ }
+ if q.PageNum > 1 {
+ target += "/page/" + strconv.Itoa(q.PageNum)
+ }
+ target += "/"
+ c.Redirect(http.StatusMovedPermanently, target)
+ return
+ }
+ if redirectCanonical(c, playletser.RankCanonicalPath(q)) {
+ return
+ }
+ raw := playletser.FetchRankOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+ tabs := playletser.BuildRankTabs(q)
+ seoSelections := make([]playletser.SEOSelection, 0, 1)
+ for _, tab := range tabs {
+ if tab.Active {
+ seoSelections = append(seoSelections, playletser.SEOSelection{Dimension: "排行榜类型", Value: tab.Label})
+ break
+ }
+ }
+ data := gin.H{
+ "title": "排行榜",
+ "description": "黄果短剧排行榜实时更新热播短剧,查看热门榜、飙升榜,不错过高口碑必看剧集。",
+ "navActive": "rank",
+ "seoInfo": raw.SeoInfo,
+ "seoSelections": seoSelections,
+ "tabs": tabs,
+ "list": raw.List,
+ "rankOffset": playletser.RankOffset(q),
+ "sortOrder": q.SortOrder,
+ "isBinge": playletser.IsRankBinge(q.SortOrder),
+ "pagination": playletser.BuildRankPagination(q, raw.TotalCount),
+ }
+ if raw.TotalCount <= 0 || len(raw.List) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "rank.html", data)
+}
diff --git a/app/api/web/render.go b/app/api/web/render.go
new file mode 100644
index 0000000..42d8624
--- /dev/null
+++ b/app/api/web/render.go
@@ -0,0 +1,314 @@
+package web
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+ "videoweb/app/service/authser"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/playletser"
+ "videoweb/app/service/seoser"
+ "videoweb/app/service/translationser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HTML 渲染页面并注入 footer / 导航 / 公共配置 / 登录态(以 cookie 为准)。
+func HTML(c *gin.Context, code int, name string, data gin.H) {
+ if data == nil {
+ data = gin.H{}
+ }
+ if _, ok := data["footer"]; !ok {
+ footer := configser.FetchFooterOrEmpty(c)
+ data["footer"] = footer
+ }
+ headerNav, ok := data["headerNav"].([]translationser.HeaderNavItemVM)
+ if !ok {
+ headerNav = translationser.FetchHeaderNavOrDefault(c)
+ data["headerNav"] = headerNav
+ }
+ activeKey, _ := data["navActive"].(string)
+ resolvedNavActive := translationser.ResolveNavigationActiveKey(activeKey, c.Request.URL.Path)
+ headerNav = translationser.MarkHeaderNavActive(headerNav, c.Request.URL.Path)
+ data["headerNav"] = headerNav
+ data["resolvedNavActive"] = resolvedNavActive
+ data["pageConsoleData"] = buildPageConsoleData(c.Request.URL.RequestURI(), code, name, data)
+ seoInfo := pageSEOFromData(data["seoInfo"])
+ seoChanged := false
+ if navSEO, found := translationser.FindHeaderNavSEO(headerNav, activeKey, c.Request.URL.Path); found {
+ seoInfo = playletser.MergeSeoInfo(playletser.SeoInfo{
+ Title: navSEO.Title,
+ Description: navSEO.Description,
+ Keywords: navSEO.Keyworld,
+ }, seoInfo)
+ seoChanged = true
+ }
+ if selections, ok := data["seoSelections"].([]playletser.SEOSelection); ok && len(selections) > 0 {
+ seoInfo = playletser.DifferentiateSeoInfo(seoInfo, selections)
+ seoChanged = true
+ }
+ if pageNum := paginationPage(data["pagination"]); pageNum > 1 {
+ pageTitle, _ := data["title"].(string)
+ pageTitle = playletser.PaginatedTitle(pageTitle, pageNum)
+ if pageTitle != "" {
+ data["title"] = pageTitle
+ }
+ seoInfo = playletser.PaginateSeoInfo(seoInfo, paginationDescription(data, pageTitle), pageNum)
+ seoChanged = true
+ }
+ if seoChanged {
+ data["seoInfo"] = seoInfo
+ if seoInfo.Title != "" {
+ data["title"] = seoInfo.Title
+ }
+ }
+ if _, ok := data["appConfig"]; !ok {
+ data["appConfig"] = configser.FetchAppConfigVMOrEmpty(c)
+ }
+ cfg, ok := data["appConfig"].(configser.AppConfigVM)
+ if !ok {
+ cfg = configser.FetchAppConfigVMOrEmpty(c)
+ }
+ cfg = configser.ApplyTrackerFallback(cfg)
+ data["appConfig"] = cfg
+ siteURL := seoser.ResolveSiteURL(c, cfg)
+ data["foreverUrlAddr"] = siteURL
+ data["siteURL"] = siteURL
+ attachPaginationLinks(data, siteURL)
+ canonicalURL := seoser.CanonicalWithoutQuery(seoser.PageCanonicalURL(siteURL, cfg.ForeverURLAddr, c))
+ canonicalURL = seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data))
+ data["canonicalURL"] = canonicalURL
+ data["seoVerify"] = seoser.VerifyFromConfig()
+ attachTrackerPage(c, code, data)
+ noindex := pageNoIndex(data)
+ if noindex {
+ data["noindex"] = true
+ }
+ if code >= http.StatusOK && code < http.StatusMultipleChoices && !noindex && seoser.IsIndexablePath(c.Request.URL.Path) {
+ data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg)
+ }
+
+ // 无登录 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。
+ authser.EnsureGuestOnEnter(c)
+
+ loggedIn, userName, avatar, email := authser.AuthFromCookies(c)
+ registered := authser.IsRegisteredFromCookies(c)
+ data["isLoggedIn"] = loggedIn
+ data["isRegistered"] = registered
+ if _, ok := data["isVip"]; !ok {
+ data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c)
+ }
+ isVip, _ := data["isVip"].(bool)
+ data["isVip"] = isVip
+ if loggedIn {
+ if userName != "" {
+ data["userName"] = userName
+ }
+ if avatar != "" {
+ data["avatar"] = avatar
+ }
+ if email != "" {
+ data["email"] = email
+ }
+ if _, ok := data["userId"]; !ok {
+ if uid := authser.UserIDFromCookies(c); uid > 0 {
+ data["userId"] = authser.FormatUserID(uid)
+ }
+ }
+ // 允许页面 handler 先注入最新钱包数据(如 getInfo),避免被 cookie 覆盖。
+ if _, ok := data["recharge"]; !ok {
+ recharge := authser.RechargeFromCookies(c)
+ data["recharge"] = recharge
+ data["rechargeText"] = authser.FormatRecharge(recharge)
+ } else if _, ok := data["rechargeText"]; !ok {
+ data["rechargeText"] = authser.FormatRecharge(toInt64(data["recharge"]))
+ }
+ if _, ok := data["vipType"]; !ok {
+ data["vipType"] = authser.VipTypeFromCookies(c)
+ }
+ vipType := toInt(data["vipType"])
+ data["vipType"] = vipType
+ if _, ok := data["showCardName"]; !ok {
+ data["showCardName"] = authser.ShowCardNameFromCookies(c)
+ }
+ showCardName, _ := data["showCardName"].(string)
+ data["showCardName"] = strings.TrimSpace(showCardName)
+ if _, ok := data["showCardId"]; !ok {
+ data["showCardId"] = authser.ShowCardIDFromCookies(c)
+ }
+ if isVip {
+ if _, ok := data["vipLabel"]; !ok {
+ data["vipLabel"] = authser.VipDisplayLabel(showCardName, vipType)
+ }
+ if _, ok := data["vipExpireText"]; !ok {
+ data["vipExpireText"] = authser.VipExpireTextFromCookies(c)
+ }
+ } else {
+ delete(data, "vipLabel")
+ delete(data, "vipExpireText")
+ }
+ } else {
+ delete(data, "userName")
+ delete(data, "avatar")
+ delete(data, "email")
+ delete(data, "userId")
+ delete(data, "recharge")
+ delete(data, "rechargeText")
+ delete(data, "vipType")
+ delete(data, "showCardId")
+ delete(data, "showCardName")
+ delete(data, "vipLabel")
+ delete(data, "vipExpireText")
+ }
+
+ c.HTML(code, name, data)
+}
+
+// buildPageConsoleData 构建浏览器控制台使用的页面返回数据快照。
+// 快照在公共配置和 cookie 登录态注入前生成,避免把会话相关公共字段写入日志。
+func buildPageConsoleData(path string, code int, templateName string, data gin.H) gin.H {
+ pageData := make(gin.H, len(data))
+ for key, value := range data {
+ if pageConsoleExcludedKey(key) {
+ continue
+ }
+ pageData[key] = value
+ }
+ payload := gin.H{
+ "path": path,
+ "status": code,
+ "template": templateName,
+ "data": pageData,
+ }
+ if headerNav, ok := data["headerNav"]; ok {
+ payload["headerNav"] = headerNav
+ }
+ if footer, ok := data["footer"]; ok {
+ payload["footer"] = footer
+ }
+ return payload
+}
+
+func pageConsoleExcludedKey(key string) bool {
+ switch key {
+ case "pageConsoleData", "appConfig", "footer", "headerNav", "jsonLD", "seoVerify",
+ "isLoggedIn", "isRegistered", "userName", "avatar", "email", "userId":
+ return true
+ default:
+ return false
+ }
+}
+
+func pageSEOFromData(value any) playletser.SeoInfo {
+ switch seo := value.(type) {
+ case playletser.SeoInfo:
+ return seo
+ case *playletser.SeoInfo:
+ if seo != nil {
+ return *seo
+ }
+ }
+ return playletser.SeoInfo{}
+}
+
+func paginationPage(value any) int {
+ switch pagination := value.(type) {
+ case playletser.PaginationVM:
+ return pagination.Page
+ case *playletser.PaginationVM:
+ if pagination != nil {
+ return pagination.Page
+ }
+ }
+ return 0
+}
+
+func paginationDescription(data gin.H, pageTitle string) string {
+ for _, key := range []string{"description", "categoryDescription", "tagDescription", "topicDescription"} {
+ if value, _ := data[key].(string); strings.TrimSpace(value) != "" {
+ return value
+ }
+ }
+ pageTitle = strings.TrimSpace(pageTitle)
+ if pageTitle == "" {
+ return "浏览本页内容与站内相关推荐。"
+ }
+ return "浏览" + pageTitle + "的本页内容与站内相关推荐。"
+}
+
+func attachPaginationLinks(data gin.H, siteURL string) {
+ var pagination playletser.PaginationVM
+ switch value := data["pagination"].(type) {
+ case playletser.PaginationVM:
+ pagination = value
+ case *playletser.PaginationVM:
+ if value == nil {
+ return
+ }
+ pagination = *value
+ default:
+ return
+ }
+ if !pagination.Visible {
+ return
+ }
+ if pagination.HasPrev && strings.TrimSpace(pagination.PrevURL) != "" {
+ data["paginationPrevURL"] = seoser.AbsURL(siteURL, pagination.PrevURL)
+ }
+ if pagination.HasNext && strings.TrimSpace(pagination.NextURL) != "" {
+ data["paginationNextURL"] = seoser.AbsURL(siteURL, pagination.NextURL)
+ }
+}
+
+// HTMLOK 200 快捷方法。
+func HTMLOK(c *gin.Context, name string, data gin.H) {
+ HTML(c, http.StatusOK, name, data)
+}
+
+// setNoIndex 统一设置页面级索引策略,并让公共渲染层同步关闭 JSON-LD。
+func setNoIndex(data gin.H, follow bool) {
+ if data == nil {
+ return
+ }
+ data["noindex"] = true
+ if follow {
+ data["robots"] = "noindex,follow"
+ return
+ }
+ data["robots"] = "noindex,nofollow"
+}
+
+func pageNoIndex(data gin.H) bool {
+ if noindex, _ := data["noindex"].(bool); noindex {
+ return true
+ }
+ robots, _ := data["robots"].(string)
+ for _, directive := range strings.Split(strings.ToLower(robots), ",") {
+ if strings.TrimSpace(directive) == "noindex" {
+ return true
+ }
+ }
+ return false
+}
+
+func toInt64(v any) int64 {
+ switch n := v.(type) {
+ case int64:
+ return n
+ case int:
+ return int64(n)
+ case int32:
+ return int64(n)
+ case float64:
+ return int64(n)
+ case string:
+ i, _ := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
+ return i
+ default:
+ return 0
+ }
+}
+
+func toInt(v any) int {
+ return int(toInt64(v))
+}
diff --git a/app/api/web/render_test.go b/app/api/web/render_test.go
new file mode 100644
index 0000000..ca8b90f
--- /dev/null
+++ b/app/api/web/render_test.go
@@ -0,0 +1,66 @@
+package web
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+)
+
+func TestBuildPageConsoleDataFiltersCommonSessionFields(t *testing.T) {
+ payload := buildPageConsoleData("/detail/example", http.StatusOK, "detail.html", gin.H{
+ "title": "示例短剧",
+ "detail": gin.H{"id": 12, "slug": "example"},
+ "appConfig": "public config",
+ "headerNav": "navigation",
+ "footer": "footer data",
+ "isLoggedIn": true,
+ "email": "viewer@example.com",
+ "pageConsoleData": "old payload",
+ })
+
+ if payload["path"] != "/detail/example" || payload["status"] != http.StatusOK || payload["template"] != "detail.html" {
+ t.Fatalf("unexpected page console metadata: %#v", payload)
+ }
+ pageData, ok := payload["data"].(gin.H)
+ if !ok {
+ t.Fatalf("payload data type = %T, want gin.H", payload["data"])
+ }
+ if _, ok := pageData["detail"]; !ok {
+ t.Fatalf("page response data was removed: %#v", pageData)
+ }
+ if payload["headerNav"] != "navigation" || payload["footer"] != "footer data" {
+ t.Fatalf("common navigation/footer data missing: %#v", payload)
+ }
+ for _, key := range []string{"appConfig", "headerNav", "isLoggedIn", "email", "pageConsoleData"} {
+ if _, ok := pageData[key]; ok {
+ t.Fatalf("excluded key %q remains in console data: %#v", key, pageData)
+ }
+ }
+}
+
+func TestSetNoIndexAndPageNoIndex(t *testing.T) {
+ data := gin.H{}
+ setNoIndex(data, false)
+ if data["robots"] != "noindex,nofollow" || !pageNoIndex(data) {
+ t.Fatalf("noindex policy was not applied: %#v", data)
+ }
+
+ if pageNoIndex(gin.H{"robots": "index,follow"}) {
+ t.Fatal("indexable robots value was treated as noindex")
+ }
+}
+
+func TestRichtextIndexPolicy(t *testing.T) {
+ publicData := gin.H{}
+ applyRichtextIndexPolicy(publicData, "关于我们")
+ if pageNoIndex(publicData) {
+ t.Fatal("about page should remain indexable")
+ }
+
+ legalData := gin.H{}
+ applyRichtextIndexPolicy(legalData, "隐私政策")
+ if !pageNoIndex(legalData) || legalData["robots"] != "noindex,follow" {
+ t.Fatalf("privacy page policy = %#v, want noindex,follow", legalData)
+ }
+}
diff --git a/app/api/web/richtext.go b/app/api/web/richtext.go
new file mode 100644
index 0000000..e4294bd
--- /dev/null
+++ b/app/api/web/richtext.go
@@ -0,0 +1,88 @@
+package web
+
+import (
+ "net/url"
+ "strings"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/seoser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Richtext 富文本页 SSR:GET /api/app/config/richtext/list?titles=
+func Richtext(c *gin.Context) {
+ title := routeOrQuery(c, "title", "title")
+ target := "/richtext"
+ if title != "" {
+ target += "/" + url.PathEscape(title)
+ }
+ target += "/"
+ if redirectCanonical(c, target) {
+ return
+ }
+ renderRichtext(c, title)
+}
+
+func RichtextPage(title string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ page := configser.FetchRichTextByTitle(c, title)
+ if !page.Found {
+ fallbackTemplates := map[string]string{
+ "关于我们": "aboutUs.html",
+ "用户协议": "userAgreement.html",
+ "隐私政策": "privacyPolicy.html",
+ "帮助中心": "helpCenter.html",
+ }
+ if fallbackTemplate := fallbackTemplates[title]; fallbackTemplate != "" {
+ data := gin.H{"title": title}
+ applyRichtextIndexPolicy(data, title)
+ if title == "帮助中心" {
+ data["faqItems"] = []seoser.FAQItem{
+ {Question: "无法登录账号怎么办?", Answer: "请确认邮箱与密码是否正确;若忘记密码,可在登录页面使用“找回密码”。"},
+ {Question: "观看记录没有同步?", Answer: "请确认已登录同一账号并保持网络连接,记录通常会自动同步。"},
+ {Question: "视频无法播放或加载缓慢?", Answer: "建议切换网络、刷新页面或更新浏览器;问题持续时可提交反馈。"},
+ {Question: "如何管理收藏内容?", Answer: "进入个人中心的“追剧与收藏”页面,可查看或取消收藏。"},
+ }
+ }
+ HTMLOK(c, fallbackTemplate, data)
+ return
+ }
+ }
+ renderRichtextData(c, title, page)
+ }
+}
+
+func renderRichtext(c *gin.Context, title string) {
+ title = strings.TrimSpace(title)
+ page := configser.FetchRichTextByTitle(c, title)
+ renderRichtextData(c, title, page)
+}
+
+func renderRichtextData(c *gin.Context, title string, page configser.RichTextPageVM) {
+ if !page.Found {
+ NotFound(c, "未找到该内容")
+ return
+ }
+
+ displayTitle := page.Title
+ if displayTitle == "" {
+ displayTitle = "内容详情"
+ }
+
+ data := gin.H{
+ "title": displayTitle,
+ "richtext": page,
+ "richtextKey": title,
+ }
+ applyRichtextIndexPolicy(data, title)
+ HTMLOK(c, "richtext.html", data)
+}
+
+func applyRichtextIndexPolicy(data gin.H, title string) {
+ switch strings.TrimSpace(title) {
+ case "关于我们", "帮助中心":
+ return
+ default:
+ setNoIndex(data, true)
+ }
+}
diff --git a/app/api/web/route_url.go b/app/api/web/route_url.go
new file mode 100644
index 0000000..1086e52
--- /dev/null
+++ b/app/api/web/route_url.go
@@ -0,0 +1,43 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+func routeOrQuery(c *gin.Context, routeName, queryName string) string {
+ if c == nil {
+ return ""
+ }
+ if value := strings.TrimSpace(c.Param(routeName)); value != "" {
+ return value
+ }
+ return strings.TrimSpace(c.Query(queryName))
+}
+
+func hasQuery(c *gin.Context, names ...string) bool {
+ if c == nil || c.Request == nil || c.Request.URL == nil {
+ return false
+ }
+ values := c.Request.URL.Query()
+ for _, name := range names {
+ if _, ok := values[name]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+func redirectCanonical(c *gin.Context, target string) bool {
+ target = strings.TrimSpace(target)
+ if c == nil || target == "" || c.Request == nil || c.Request.URL == nil {
+ return false
+ }
+ if c.Request.URL.RequestURI() == target {
+ return false
+ }
+ c.Redirect(http.StatusMovedPermanently, target)
+ return true
+}
diff --git a/app/api/web/search.go b/app/api/web/search.go
new file mode 100644
index 0000000..137ccb4
--- /dev/null
+++ b/app/api/web/search.go
@@ -0,0 +1,45 @@
+package web
+
+import (
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Search 搜索页 SSR:GET /api/app/playlet/search,列表直接用接口字段渲染。
+func Search(c *gin.Context) {
+ q := playletser.ParseSearchQuery(routeOrQuery(c, "keyword", "q"), routeOrQuery(c, "page", "page"))
+ if redirectCanonical(c, playletser.SearchCanonicalPath(q)) {
+ return
+ }
+ var raw *playletser.PlayletVideoResp
+ if q.Keyword != "" {
+ raw = playletser.FetchSearchOrEmpty(c, q)
+ } else {
+ raw = &playletser.PlayletVideoResp{PageNum: q.PageNum, PageSize: q.PageSize}
+ }
+ emptyTitle := "请输入关键词搜索"
+ emptyDesc := "在上方输入剧名、题材或标签开始搜索"
+ if q.Keyword != "" {
+ emptyTitle = "未找到相关短剧"
+ emptyDesc = "换个关键词试试,或返回首页看看热门内容"
+ }
+ pageTitle := "搜索短剧"
+ description := "在黄果短剧搜索短剧名称、标签与关键词,快速找到想看的剧集。"
+ if q.Keyword != "" {
+ pageTitle = "搜索:" + q.Keyword
+ description = "在黄果短剧搜索「" + q.Keyword + "」相关短剧结果,快速找到想看的剧集。"
+ }
+ data := gin.H{
+ "title": pageTitle,
+ "description": description,
+ "query": q.Keyword,
+ "list": raw.List,
+ "totalCount": raw.TotalCount,
+ "pagination": playletser.BuildSearchPagination(q, raw.TotalCount),
+ "emptyTitle": emptyTitle,
+ "emptyDesc": emptyDesc,
+ }
+ setNoIndex(data, true)
+ HTMLOK(c, "search.html", data)
+}
diff --git a/app/api/web/seo.go b/app/api/web/seo.go
new file mode 100644
index 0000000..acf934c
--- /dev/null
+++ b/app/api/web/seo.go
@@ -0,0 +1,38 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/seoser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// RobotsTXT 动态 robots.txt(含 Sitemap 声明)。
+func RobotsTXT(c *gin.Context) {
+ cfg := configser.FetchAppConfigVMOrEmpty(c)
+ foreverURL := seoser.ResolveSiteURL(c, cfg)
+ c.Header("Content-Type", "text/plain; charset=utf-8")
+ c.Header("Cache-Control", "public, max-age=3600")
+ c.String(http.StatusOK, seoser.BuildRobotsTXT(foreverURL))
+}
+
+// SEOVerifyFile 提供百度 HTML 验证文件和 IndexNow key.txt。
+func SEOVerifyFile(c *gin.Context) {
+ codes := seoser.VerifyFromConfig()
+ name := strings.TrimPrefix(c.Request.URL.Path, "/")
+
+ switch {
+ case codes.BaiduHTML != "" && name == codes.BaiduHTML:
+ c.Header("Cache-Control", "public, max-age=86400")
+ c.String(http.StatusOK, codes.Baidu)
+ return
+ case codes.IndexNowKey != "" && name == codes.IndexNowKey+".txt":
+ c.Header("Cache-Control", "public, max-age=86400")
+ c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(codes.IndexNowKey))
+ return
+ }
+
+ c.Status(http.StatusNotFound)
+}
diff --git a/app/api/web/series.go b/app/api/web/series.go
new file mode 100644
index 0000000..5fb0958
--- /dev/null
+++ b/app/api/web/series.go
@@ -0,0 +1,46 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Series AI 成人剧集 SSR:Swagger operate/list 固定 operateType=2,筛选映射 filterType=1/2/3。
+func Series(c *gin.Context) {
+ q, ok := playletser.ParseSeriesQuery(c.Param("filter"), routeOrQuery(c, "page", "page"))
+ if !ok {
+ NotFound(c, "页面不存在")
+ return
+ }
+ if redirectCanonical(c, playletser.SeriesCanonicalPath(q)) {
+ return
+ }
+ raw := playletser.FetchOperateListOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+
+ sectionTitle := playletser.SeriesSectionTitle(q.FilterType)
+ data := gin.H{
+ "title": "AI成人剧集",
+ "description": "黄果短剧精选AI成人剧集,持续更新热门漫剧、独家原创剧集,支持免费在线观看。",
+ "pageKey": "series",
+ "pageName": "AI成人剧集",
+ "pagePath": playletser.SeriesCanonicalPath(q),
+ "navActive": "aiSeries",
+ "filters": playletser.BuildSeriesFilterTabs(q),
+ "sectionTitle": sectionTitle,
+ "seoSelections": []playletser.SEOSelection{{Dimension: "筛选", Value: strings.TrimSuffix(sectionTitle, "的AI成人剧集")}},
+ "list": raw.List,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildSeriesPagination(q, raw.TotalCount),
+ }
+ // 剧集入口页(/series/、/series/popular/、/series/original/)始终可索引;空结果仅对分页打 noindex。
+ if q.PageNum > 1 && (raw.TotalCount <= 0 || len(raw.List) == 0) {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "series.html", data)
+}
diff --git a/app/api/web/session.go b/app/api/web/session.go
new file mode 100644
index 0000000..ab243a5
--- /dev/null
+++ b/app/api/web/session.go
@@ -0,0 +1,32 @@
+package web
+
+import (
+ "videoweb/app/service/authser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// AbortSSRIfSessionExpired 处理 SSR 场景下的会话失效码。
+// aborted=true:已跳转登录,调用方应立刻 return;
+// retry=true:游客已重新登录,调用方应重试上游请求。
+func AbortSSRIfSessionExpired(c *gin.Context, code int) (aborted, retry bool) {
+ if !authser.IsSessionExpiredCode(code) {
+ return false, false
+ }
+ retry, needLogin := authser.OnSessionExpired(c, code)
+ if needLogin {
+ RedirectToLogin(c)
+ return true, false
+ }
+ return false, retry
+}
+
+// TryJSONSessionRecover JSON 接口遇到会话失效时:游客重登后返回 true(应重试);
+// 注册账号清 cookie 后返回 false,调用方继续把 code=1000 交给前端弹登录。
+func TryJSONSessionRecover(c *gin.Context, code int) bool {
+ if !authser.IsSessionExpiredCode(code) {
+ return false
+ }
+ retry, _ := authser.OnSessionExpired(c, code)
+ return retry
+}
diff --git a/app/api/web/setting.go b/app/api/web/setting.go
new file mode 100644
index 0000000..0391f4d
--- /dev/null
+++ b/app/api/web/setting.go
@@ -0,0 +1,119 @@
+package web
+
+import (
+ "net/http"
+ "videoweb/app/service/authser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Setting 账号设置页:SSR 拉取 /api/app/auth/getInfo。
+func Setting(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ RedirectToLogin(c)
+ return
+ }
+
+ env, user, err := authser.GetInfo(c)
+ if err != nil {
+ log.Error("setting getInfo failed", log.E(err))
+ authser.ClearAuthCookies(c)
+ RedirectToLogin(c)
+ return
+ }
+ if env != nil && authser.IsSessionExpiredCode(env.Code) {
+ retry, needLogin := authser.OnSessionExpired(c, env.Code)
+ if needLogin || !retry {
+ RedirectToLogin(c)
+ return
+ }
+ env, user, err = authser.GetInfo(c)
+ if err != nil || env == nil || env.Code != 200 || user == nil {
+ authser.ClearAuthCookies(c)
+ RedirectToLogin(c)
+ return
+ }
+ } else if env == nil || env.Code != 200 || user == nil {
+ authser.ClearAuthCookies(c)
+ RedirectToLogin(c)
+ return
+ }
+
+ authser.SetAuthCookies(c, user)
+ vm := authser.BuildSettingVM(user)
+ data := gin.H{
+ "title": "账号设置",
+ "setting": vm,
+ }
+ setNoIndex(data, false)
+ HTMLOK(c, "setting.html", data)
+}
+
+// EditProfile 编辑资料页:SSR 拉取 getInfo,头像/昵称等回填。
+func EditProfile(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ RedirectToLogin(c)
+ return
+ }
+
+ env, user, err := authser.GetInfo(c)
+ if err != nil {
+ log.Error("edit-profile getInfo failed", log.E(err))
+ authser.ClearAuthCookies(c)
+ RedirectToLogin(c)
+ return
+ }
+ if env != nil && authser.IsSessionExpiredCode(env.Code) {
+ retry, needLogin := authser.OnSessionExpired(c, env.Code)
+ if needLogin || !retry {
+ RedirectToLogin(c)
+ return
+ }
+ env, user, err = authser.GetInfo(c)
+ if err != nil || env == nil || env.Code != 200 || user == nil {
+ authser.ClearAuthCookies(c)
+ RedirectToLogin(c)
+ return
+ }
+ } else if env == nil || env.Code != 200 || user == nil {
+ authser.ClearAuthCookies(c)
+ RedirectToLogin(c)
+ return
+ }
+
+ authser.SetAuthCookies(c, user)
+ vm := authser.BuildSettingVM(user)
+ data := gin.H{
+ "title": "编辑个人资料",
+ "profile": vm,
+ }
+ setNoIndex(data, false)
+ HTMLOK(c, "editProfile.html", data)
+}
+
+// EditPassword 修改密码页:需登录。
+func EditPassword(c *gin.Context) {
+ if useragent.TokenFromRequest(c.Request) == "" {
+ RedirectToLogin(c)
+ return
+ }
+ data := gin.H{
+ "title": "修改登录密码",
+ }
+ setNoIndex(data, false)
+ HTMLOK(c, "editPassword.html", data)
+}
+
+// Logout 清除登录 cookie,并标记跳过紧接着的一次自动游客登录。
+func Logout(c *gin.Context) {
+ authser.ClearAuthCookies(c)
+ authser.MarkGuestSkipAuto(c)
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "msg": "success",
+ "tip": "已退出登录",
+ "data": nil,
+ })
+}
diff --git a/app/api/web/tag_list.go b/app/api/web/tag_list.go
new file mode 100644
index 0000000..29b2946
--- /dev/null
+++ b/app/api/web/tag_list.go
@@ -0,0 +1,75 @@
+package web
+
+import (
+ "strings"
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+func TagList(c *gin.Context) {
+ q := playletser.ParseTagListQuery(routeOrQuery(c, "page", "page"))
+ if slug := strings.TrimSpace(c.Param("slug")); slug != "" {
+ if id := playletser.ParseDetailID(slug); id > 0 {
+ q.TagID = id
+ } else {
+ q.TagSlug = slug
+ }
+ }
+ if path := c.Request.URL.Path; path == "/tag-list" || strings.HasPrefix(path, "/tag-list/") {
+ redirectCanonical(c, playletser.TagListCanonicalPath(q))
+ return
+ }
+ raw := playletser.FetchTagListOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+ hasIdentifier := q.TagID > 0 || strings.TrimSpace(q.TagSlug) != ""
+ if emptyScopedPage(hasIdentifier, raw.TotalCount, len(raw.List)) {
+ NotFound(c, "标签不存在")
+ return
+ }
+ if q.TagID > 0 {
+ q.TagSlug = playletser.FindTagSlug(raw.List, q.TagID)
+ }
+ if redirectCanonical(c, playletser.TagListCanonicalPath(q)) {
+ return
+ }
+
+ pageName := playletser.FindTagNameBySlug(raw.List, q.TagSlug)
+ if pageName == "" {
+ pageName = playletser.FindTagName(raw.List, q.TagID)
+ }
+ if pageName == "" {
+ if name := strings.TrimSpace(raw.SeoInfo.Title); name != "" {
+ pageName = name
+ } else {
+ pageName = "标签"
+ }
+ }
+ description := strings.TrimSpace(raw.SeoInfo.Description)
+ if description == "" {
+ description = "精选" + pageName + "题材相关AI成人短剧与AI成人漫剧,持续更新" + pageName + "题材作品,方便快速浏览相关短剧。"
+ }
+
+ title := playletser.TagListPageTitle(pageName, q.PageNum, raw.SeoInfo)
+ data := gin.H{
+ "title": title,
+ "pageKey": "tag_list",
+ "pageName": pageName,
+ "pageTitle": pageName,
+ "tagDescription": description,
+ "pageNum": q.PageNum,
+ "pagePath": playletser.TagListCanonicalPath(q),
+ "seoInfo": raw.SeoInfo,
+ "list": raw.List,
+ "totalCount": raw.TotalCount,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildTagListPagination(q, raw.TotalCount),
+ }
+ if raw.TotalCount <= 0 || len(raw.List) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "tagList.html", data)
+}
diff --git a/app/api/web/theme_index.go b/app/api/web/theme_index.go
new file mode 100644
index 0000000..73bb3fe
--- /dev/null
+++ b/app/api/web/theme_index.go
@@ -0,0 +1,39 @@
+package web
+
+import (
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// ThemeIndex 专题列表页:专题数据来自 GET /api/app/theme/list?themeType=2。
+func ThemeIndex(c *gin.Context) {
+ q := playletser.ParseThemeIndexQuery(c.Param("page"))
+ if redirectCanonical(c, playletser.ThemeIndexCanonicalPath(q)) {
+ return
+ }
+ themes := playletser.FetchThemesOrEmpty(c, 2)
+ list, totalCount := playletser.BuildThemeIndexPage(themes, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, totalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+
+ data := gin.H{
+ "title": "AI成人短剧专题推荐" + playletser.PageTitleSuffix(q.PageNum),
+ "description": "专题集中展示黄果短剧各类精品专题合集,涵盖AI成人短剧、AI色情短剧、擦边短剧、AI换脸、AI魔改等热门主题。内容丰富多样,每日持续更新。",
+ "pageKey": "theme_index",
+ "pageName": "专题",
+ "navActive": "topic",
+ "pageNum": q.PageNum,
+ "pagePath": playletser.ThemeIndexCanonicalPath(q),
+ "list": list,
+ "totalCount": totalCount,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildThemeIndexPagination(q, totalCount),
+ }
+ if totalCount <= 0 || len(list) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "themeIndex.html", data)
+}
diff --git a/app/api/web/today_update.go b/app/api/web/today_update.go
new file mode 100644
index 0000000..fe6c34f
--- /dev/null
+++ b/app/api/web/today_update.go
@@ -0,0 +1,33 @@
+package web
+
+import (
+ "videoweb/app/service/playletser"
+
+ "github.com/gin-gonic/gin"
+)
+
+// TodayUpdate 今日更新页 SSR:GET /api/app/playlet/today-update,列表用接口字段渲染。
+func TodayUpdate(c *gin.Context) {
+ q := playletser.ParseTodayUpdateQuery(routeOrQuery(c, "page", "page"))
+ if redirectCanonical(c, playletser.TodayUpdateCanonicalPath(q)) {
+ return
+ }
+ raw := playletser.FetchTodayUpdateOrEmpty(c, q)
+ if playletser.PageOutOfRange(q.PageNum, q.PageSize, raw.TotalCount) {
+ NotFound(c, "页面不存在")
+ return
+ }
+ data := gin.H{
+ "title": "今日更新",
+ "description": "黄果短剧今日更新:第一时间追更最新上线短剧与连载剧集,不错过每日精彩更新。",
+ "navActive": "todayUpdate",
+ "seoInfo": raw.SeoInfo,
+ "list": raw.List,
+ "itemListOffset": (q.PageNum - 1) * q.PageSize,
+ "pagination": playletser.BuildTodayUpdatePagination(q, raw.TotalCount),
+ }
+ if raw.TotalCount <= 0 || len(raw.List) == 0 {
+ setNoIndex(data, true)
+ }
+ HTMLOK(c, "todayUpdate.html", data)
+}
diff --git a/app/api/web/tracker_page.go b/app/api/web/tracker_page.go
new file mode 100644
index 0000000..58d65a3
--- /dev/null
+++ b/app/api/web/tracker_page.go
@@ -0,0 +1,127 @@
+package web
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// attachTrackerPage 按响应状态和路径注入 pageKey / pageName(handler 可显式覆盖)。
+// 所有 HTML 页面都必须得到明确页面信息,避免埋点落入 unknown / 未知页面。
+func attachTrackerPage(c *gin.Context, code int, data gin.H) {
+ if data == nil {
+ return
+ }
+ key, name := trackerPageFromRequest(c.Request.URL.Path, code)
+ if code >= http.StatusBadRequest {
+ data["pageKey"] = key
+ data["pageName"] = name
+ return
+ }
+ existingKey, _ := data["pageKey"].(string)
+ existingName, _ := data["pageName"].(string)
+ existingKey = strings.TrimSpace(existingKey)
+ existingName = strings.TrimSpace(existingName)
+
+ if existingKey == "" || existingKey == "unknown" {
+ data["pageKey"] = key
+ }
+ if existingName == "" || existingName == "未知页面" {
+ data["pageName"] = name
+ }
+}
+
+func trackerPageFromRequest(path string, code int) (pageKey, pageName string) {
+ if code >= http.StatusInternalServerError {
+ return "server_error", "服务异常页面"
+ }
+ if code >= http.StatusBadRequest {
+ return "not_found", "404页面"
+ }
+ return trackerPageFromPath(path)
+}
+
+func trackerPageFromPath(path string) (pageKey, pageName string) {
+ p := strings.TrimSpace(path)
+ if p == "" || p == "/" {
+ return "home", "首页"
+ }
+ // 去掉尾部斜杠,便于精确匹配
+ p = strings.TrimSuffix(p, "/")
+ switch {
+ case trackerPathHasPrefix(p, "/list"):
+ return "list", "短剧列表"
+ case trackerPathHasPrefix(p, "/series"):
+ return "series", "AI成人剧集"
+ case trackerPathHasPrefix(p, "/tag"), trackerPathHasPrefix(p, "/tag-list"):
+ return "tag_list", "标签详情"
+ case trackerPathHasPrefix(p, "/rank"):
+ return "rank", "排行榜"
+ case trackerPathHasPrefix(p, "/today-update"):
+ return "today_update", "今日更新"
+ case trackerPathHasPrefix(p, "/category"):
+ return "category_detail", "分类详情"
+ case trackerTopicIndexPath(p):
+ return "theme_index", "专题列表"
+ case trackerPathHasPrefix(p, "/topic"), trackerPathHasPrefix(p, "/more"):
+ return "topic", "专题详情"
+ case trackerPathHasPrefix(p, "/download-app"):
+ return "download_app", "下载APP"
+ case trackerPathHasPrefix(p, "/application"):
+ return "application", "应用推荐"
+ case trackerPathHasPrefix(p, "/home-road"):
+ return "home_road", "回家的路"
+ case trackerPathHasPrefix(p, "/detail"):
+ return "detail", "短剧详情"
+ case trackerPathHasPrefix(p, "/play"):
+ return "play", "播放页"
+ case trackerPathHasPrefix(p, "/login"):
+ return "login", "登录"
+ case trackerPathHasPrefix(p, "/register"):
+ return "register", "注册"
+ case trackerPathHasPrefix(p, "/user-center"):
+ return "user_center", "个人中心"
+ case trackerPathHasPrefix(p, "/deposit"):
+ return "deposit", "充值中心"
+ case trackerPathHasPrefix(p, "/collect"):
+ return "collect", "我的收藏"
+ case trackerPathHasPrefix(p, "/history"):
+ return "history", "观看历史"
+ case trackerPathHasPrefix(p, "/setting"):
+ return "setting", "设置"
+ case trackerPathHasPrefix(p, "/edit-profile"):
+ return "edit_profile", "编辑资料"
+ case trackerPathHasPrefix(p, "/edit-password"):
+ return "edit_password", "修改密码"
+ case trackerPathHasPrefix(p, "/search"):
+ return "search", "搜索结果页"
+ case trackerPathHasPrefix(p, "/about-us"):
+ return "about_us", "关于我们"
+ case trackerPathHasPrefix(p, "/user-agreement"):
+ return "user_agreement", "用户协议"
+ case trackerPathHasPrefix(p, "/privacy-policy"):
+ return "privacy_policy", "隐私政策"
+ case trackerPathHasPrefix(p, "/help-center"):
+ return "help_center", "帮助中心"
+ case trackerPathHasPrefix(p, "/richtext"):
+ return "rich_text", "富文本页"
+ case trackerPathHasPrefix(p, "/feedback"):
+ return "feedback", "问题反馈"
+ case trackerPathHasPrefix(p, "/nav"), trackerPathHasPrefix(p, "/dh"):
+ return "nav", "官方导航"
+ default:
+ return "internal_other", "站内其他页面"
+ }
+}
+
+func trackerPathHasPrefix(path, base string) bool {
+ return path == base || strings.HasPrefix(path, base+"/")
+}
+
+func trackerTopicIndexPath(path string) bool {
+ if path == "/topic" || path == "/more" {
+ return true
+ }
+ return strings.HasPrefix(path, "/topic/page/") || strings.HasPrefix(path, "/more/page/")
+}
diff --git a/app/api/web/tracker_page_test.go b/app/api/web/tracker_page_test.go
new file mode 100644
index 0000000..f91d392
--- /dev/null
+++ b/app/api/web/tracker_page_test.go
@@ -0,0 +1,107 @@
+package web
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+)
+
+func TestTrackerPageFromPath(t *testing.T) {
+ tests := []struct {
+ path string
+ key string
+ name string
+ }{
+ {"/", "home", "首页"},
+ {"/list/ai-short-play/2/page/3/", "list", "短剧列表"},
+ {"/series/hot/page/2/", "series", "AI成人剧集"},
+ {"/tag/family/page/2/", "tag_list", "标签详情"},
+ {"/tag-list/family/", "tag_list", "标签详情"},
+ {"/rank/2/page/3/", "rank", "排行榜"},
+ {"/today-update/page/2/", "today_update", "今日更新"},
+ {"/category/drama/page/2/", "category_detail", "分类详情"},
+ {"/topic/page/2/", "theme_index", "专题列表"},
+ {"/topic/sweet-love/page/2/", "topic", "专题详情"},
+ {"/more/legacy-topic/", "topic", "专题详情"},
+ {"/detail/my-playlet/", "detail", "短剧详情"},
+ {"/play/my-playlet/42/", "play", "播放页"},
+ {"/login/return/detail/foo/", "login", "登录"},
+ {"/register/", "register", "注册"},
+ {"/collect/chapter/page/2/", "collect", "我的收藏"},
+ {"/search/测试/page/2/", "search", "搜索结果页"},
+ {"/about-us/", "about_us", "关于我们"},
+ {"/user-agreement/", "user_agreement", "用户协议"},
+ {"/privacy-policy/", "privacy_policy", "隐私政策"},
+ {"/help-center/", "help_center", "帮助中心"},
+ {"/richtext/自定义内容/", "rich_text", "富文本页"},
+ {"/dh/", "nav", "官方导航"},
+ {"/unregistered-path/", "internal_other", "站内其他页面"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.path, func(t *testing.T) {
+ key, name := trackerPageFromPath(tt.path)
+ if key != tt.key || name != tt.name {
+ t.Fatalf("trackerPageFromPath(%q) = (%q, %q), want (%q, %q)", tt.path, key, name, tt.key, tt.name)
+ }
+ })
+ }
+}
+
+func TestTrackerPageFromRequestUsesResponseStatus(t *testing.T) {
+ tests := []struct {
+ status int
+ key string
+ name string
+ }{
+ {http.StatusNotFound, "not_found", "404页面"},
+ {http.StatusInternalServerError, "server_error", "服务异常页面"},
+ }
+ for _, tt := range tests {
+ key, name := trackerPageFromRequest("/missing/", tt.status)
+ if key != tt.key || name != tt.name {
+ t.Fatalf("trackerPageFromRequest(status=%d) = (%q, %q), want (%q, %q)", tt.status, key, name, tt.key, tt.name)
+ }
+ }
+}
+
+func TestAttachTrackerPagePreservesExplicitPage(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ c, _ := gin.CreateTestContext(httptest.NewRecorder())
+ c.Request = httptest.NewRequest(http.MethodGet, "/topic/custom/", nil)
+ data := gin.H{"pageKey": "topic", "pageName": "自定义专题"}
+
+ attachTrackerPage(c, http.StatusOK, data)
+
+ if data["pageKey"] != "topic" || data["pageName"] != "自定义专题" {
+ t.Fatalf("explicit tracker page was overwritten: %#v", data)
+ }
+}
+
+func TestAttachTrackerPageReplacesUnknownValues(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ c, _ := gin.CreateTestContext(httptest.NewRecorder())
+ c.Request = httptest.NewRequest(http.MethodGet, "/play/demo/1/", nil)
+ data := gin.H{"pageKey": "unknown", "pageName": "未知页面"}
+
+ attachTrackerPage(c, http.StatusOK, data)
+
+ if data["pageKey"] != "play" || data["pageName"] != "播放页" {
+ t.Fatalf("unknown tracker page was not replaced: %#v", data)
+ }
+}
+
+func TestAttachTrackerPageMarksErrorResponse(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ c, _ := gin.CreateTestContext(httptest.NewRecorder())
+ c.Request = httptest.NewRequest(http.MethodGet, "/topic/deleted/", nil)
+ data := gin.H{"pageKey": "topic", "pageName": "已删除专题"}
+
+ attachTrackerPage(c, http.StatusNotFound, data)
+
+ if data["pageKey"] != "not_found" || data["pageName"] != "404页面" {
+ t.Fatalf("error response retained business page mapping: %#v", data)
+ }
+}
diff --git a/app/api/web/user_center.go b/app/api/web/user_center.go
new file mode 100644
index 0000000..033fe6d
--- /dev/null
+++ b/app/api/web/user_center.go
@@ -0,0 +1,58 @@
+package web
+
+import (
+ "strconv"
+ "videoweb/app/service/playletser"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// UserCenter 个人中心:登录态刷新 getInfo 金币余额,并拉取收藏数量。
+func UserCenter(c *gin.Context) {
+ data := gin.H{
+ "title": "个人中心",
+ "navActive": "userCenter",
+ }
+ setNoIndex(data, false)
+ if attachWalletFromGetInfo(c, data) {
+ RedirectToLogin(c)
+ return
+ }
+ if useragent.TokenFromRequest(c.Request) != "" {
+ raw, code, err := playletser.FetchCollectList(c, playletser.CollectListQuery{
+ PageNum: 1,
+ PageSize: 1,
+ })
+ if err != nil {
+ aborted, retry := AbortSSRIfSessionExpired(c, code)
+ if aborted {
+ return
+ }
+ if retry {
+ raw, _, err = playletser.FetchCollectList(c, playletser.CollectListQuery{
+ PageNum: 1,
+ PageSize: 1,
+ })
+ }
+ if err != nil {
+ log.Error("user-center collect count failed", log.E(err))
+ raw = nil
+ }
+ }
+ count := int64(0)
+ if raw != nil {
+ count = raw.TotalCount
+ }
+ if count < 0 {
+ count = 0
+ }
+ data["collectCount"] = count
+ data["collectCountText"] = strconv.FormatInt(count, 10) + "部"
+ } else {
+ data["collectCount"] = int64(0)
+ data["collectCountText"] = "0部"
+ }
+ HTMLOK(c, "userCenter.html", data)
+}
diff --git a/app/appg/appg.go b/app/appg/appg.go
new file mode 100644
index 0000000..e6de45f
--- /dev/null
+++ b/app/appg/appg.go
@@ -0,0 +1,33 @@
+package appg
+
+import "github.com/patrickmn/go-cache"
+
+const EsIndex = "sz"
+
+var (
+ Conf GlobalCfgEx
+ Cache *cache.Cache
+)
+
+type GlobalCfgEx struct {
+ ServerAddr string `yaml:"serverAddr"`
+ ListenAddr string `yaml:"listenAddr"`
+ DHListenAddr string `yaml:"dhListenAddr"`
+ RunMod string `yaml:"runMod"`
+ TsAuthKey string `yaml:"tsAuthKey"`
+ SEO SEOConf `yaml:"seo"`
+ Tracker TrackerConf `yaml:"tracker"`
+}
+
+// SEOConf 站长平台验证与 SEO 相关配置。
+type SEOConf struct {
+ BaiduSiteVerification string `yaml:"baiduSiteVerification"`
+ BaiduHTMLFile string `yaml:"baiduHtmlFile"` // 如 baidu_verify_codeva-xxx.html
+ IndexNowKey string `yaml:"indexNowKey"` // Bing IndexNow 密钥,提供 /{key}.txt
+}
+
+// TrackerConf 数据上报 SDK 本地兜底(上游 reportSdkConfig 优先)。
+type TrackerConf struct {
+ AppID string `yaml:"appId"`
+ EncryptedConfig string `yaml:"encryptedConfig"`
+}
diff --git a/app/conf/app.yaml b/app/conf/app.yaml
new file mode 100644
index 0000000..cb3b621
--- /dev/null
+++ b/app/conf/app.yaml
@@ -0,0 +1,11 @@
+serverAddr: "http://172.104.181.99:27413"
+listenAddr: "25119"
+dhListenAddr: "26812"
+runMod: "dev"
+tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
+
+# 数据上报 SDK(web-sdk)。encryptedConfig 优先用上游 config/info.reportSdkConfig;
+# 若上游暂未下发,可在此填写密文兜底。
+tracker:
+ appId: "JHA-1043"
+ encryptedConfig: ""
diff --git a/app/conf/m3u8.key b/app/conf/m3u8.key
new file mode 100644
index 0000000..6cce2a7
--- /dev/null
+++ b/app/conf/m3u8.key
@@ -0,0 +1 @@
+—�ˆȒšÁC�„
\ No newline at end of file
diff --git a/app/deploy_prod.sh b/app/deploy_prod.sh
new file mode 100755
index 0000000..23d9e90
--- /dev/null
+++ b/app/deploy_prod.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+FILE_NAME="web_cgdj_h5"
+REMOTE_FILE_PATH="/home/server2/web_cgdj/h5/"
+TAG_NAME="web_cgdj_h5.tar.gz"
+
+set -e
+set -o
+function send_msg() {
+ msg="@jscdsndj @xiuyuan_Lee @steven0yu %0A服务:SERVER2 站点-cgdj-H5 %0A更新内容:$content%0A创建时间: $(date +%F_%X)%0A操作人:$(git config user.name)"
+ curl -X POST "https://api.telegram.org/bot6357085965:AAEmXfjZnZwnDTicL7Wjbd7cXvYv_G98w8A/sendMessage" -d "chat_id=$1&text=$msg" >/dev/null 2>&1
+}
+
+read -p "输入yes, 开始上传: " word
+if [[ ! "yes" =~ $word ]]; then
+ echo "终止"
+else
+ read -e -p "本次更新内容(输入q退出编辑): " -r content
+ echo "开始"
+ rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
+ GOOS=linux GOARCH=amd64 go build -o $FILE_NAME
+ md5sum $FILE_NAME | cut -d ' ' -f1 >yinsemd5.txt
+ # 排除 app 定时生成的 SEO 静态文件,避免覆盖服务器上已有 sitemap/rss/llms
+ tar zvcf $TAG_NAME ./$FILE_NAME templates/ static/ yinsemd5.txt \
+ --exclude='templates/page/sitemaps/sitemap.xml' \
+ --exclude='templates/page/sitemaps/sitemap-*.xml' \
+ --exclude='templates/page/rss.xml' \
+ --exclude='templates/page/llms.txt' \
+ --exclude='templates/page/llms-full.txt'
+ md5sum $TAG_NAME | cut -d ' ' -f1 >yinsemd5.txt
+ scp -i ~/.ssh/id_rsa -P 22889 ./yinsemd5.txt server2@116.212.120.2:$REMOTE_FILE_PATH
+ scp -i ~/.ssh/id_rsa -P 22889 ./$TAG_NAME server2@116.212.120.2:$REMOTE_FILE_PATH
+ send_msg -4791758991 #服务器小组
+fi
+rm -rf ./$FILE_NAME ./$TAG_NAME ./yinsemd5.txt
\ No newline at end of file
diff --git a/app/innercache/.gitkeep b/app/innercache/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/main.go b/app/main.go
new file mode 100644
index 0000000..553f3d9
--- /dev/null
+++ b/app/main.go
@@ -0,0 +1,55 @@
+//go:generate swag init --parseDependency --parseDepth 4
+//go:generate mtool replicate-module -m contest
+package main
+
+import (
+ "os"
+ "videoweb/app/server"
+)
+
+// @title 视频站APP接口
+// @version 1.0
+// @description 这是视频站APP接口服务.
+// @termsOfService
+
+// @contact.name
+// @contact.url
+// @contact.email
+
+// @license.name Apache 2.0
+// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
+
+// @host https://视频站-api.lcncctn.com
+// @BasePath /
+// @Schemes http https
+
+// @securityDefinitions.basic BasicAuth
+
+// @securityDefinitions.apikey ApiKeyAuth
+// @in header
+// @name Authorization
+
+// @securitydefinitions.oauth2.application OAuth2Application
+// @tokenUrl https://example.com/oauth/token
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.implicit OAuth2Implicit
+// @authorizationUrl https://example.com/oauth/authorize
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.password OAuth2Password
+// @tokenUrl https://example.com/oauth/token
+// @scope.read Grants read access
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @securitydefinitions.oauth2.accessCode OAuth2AccessCode
+// @tokenUrl https://example.com/oauth/token
+// @authorizationUrl https://example.com/oauth/authorize
+// @scope.admin Grants read and write access to administrative information
+
+func main() {
+ server.Run(os.Args)
+}
diff --git a/app/middleware/.gitkeep b/app/middleware/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/router/category_template_parse_test.go b/app/router/category_template_parse_test.go
new file mode 100644
index 0000000..9259362
--- /dev/null
+++ b/app/router/category_template_parse_test.go
@@ -0,0 +1,25 @@
+package router
+
+import (
+ "os"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+)
+
+func TestCategoryDetailTemplateParses(t *testing.T) {
+ workingDir, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chdir(".."); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = os.Chdir(workingDir) })
+
+ engine := gin.New()
+ setupEngine(engine)
+ if engine.HTMLRender == nil {
+ t.Fatal("HTML renderer was not initialized")
+ }
+}
diff --git a/app/router/client_headers.go b/app/router/client_headers.go
new file mode 100644
index 0000000..986f476
--- /dev/null
+++ b/app/router/client_headers.go
@@ -0,0 +1,16 @@
+package router
+
+import (
+ "videoweb/pub/common/useragent"
+
+ "github.com/gin-gonic/gin"
+)
+
+// applyClientForwardHeaders 向上游透传客户端 IP / UA(与 API 上游一致):
+// X-Real-IP、User-Agent、X-User-Agent。
+func applyClientForwardHeaders(header map[string]string, c *gin.Context) {
+ if header == nil {
+ return
+ }
+ useragent.ApplyToHeader(header, c, useragent.DefaultVer)
+}
diff --git a/app/router/gin.go b/app/router/gin.go
new file mode 100644
index 0000000..42e54f8
--- /dev/null
+++ b/app/router/gin.go
@@ -0,0 +1,321 @@
+package router
+
+import (
+ "encoding/json"
+ "html/template"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "videoweb/app/api/web"
+ "videoweb/app/appg"
+ "videoweb/app/service/configser"
+ "videoweb/app/service/playletser"
+ "videoweb/app/service/seoser"
+
+ "github.com/gin-gonic/gin"
+)
+
+func Init() *gin.Engine {
+ setGinMode()
+ engine := gin.Default()
+ engine.HandleMethodNotAllowed = true
+ setupEngine(engine)
+ engine.NoRoute(renderNotFound)
+ engine.NoMethod(renderNotFound)
+
+ registerM3U8Routes(engine)
+ registerRoutes(engine)
+ return engine
+}
+
+// DHInit 导航站独立端口:仅暴露官方导航页及静态资源。
+func DHInit() *gin.Engine {
+ setGinMode()
+ engine := gin.Default()
+ engine.HandleMethodNotAllowed = true
+ setupEngine(engine)
+ engine.StaticFile("/favicon.ico", "./static/favicon.ico")
+ engine.StaticFile("/favicon.svg", "./static/favicon.svg")
+ engine.StaticFile("/favicon.png", "./static/favicon.png")
+ engine.GET("/ui/dialog/:name", web.DialogFragment)
+ engine.GET("/", web.Nav)
+ engine.GET("/nav/", web.Nav)
+ engine.GET("/dh/", web.Nav)
+ engine.NoRoute(renderNotFound)
+ engine.NoMethod(renderNotFound)
+ return engine
+}
+
+func setGinMode() {
+ switch strings.ToLower(strings.TrimSpace(appg.Conf.RunMod)) {
+ case "test":
+ gin.SetMode(gin.TestMode)
+ case "prod":
+ gin.SetMode(gin.ReleaseMode)
+ default:
+ gin.SetMode(gin.DebugMode)
+ }
+}
+
+func setupEngine(engine *gin.Engine) {
+ tplFuncs := playletser.TemplateFuncs()
+ tplFuncs["InJSONScript"] = inJSONScript
+ tplFuncs["jsonStr"] = jsonStr
+ tplFuncs["InTrackerAppId"] = inTrackerAppId
+ engine.SetFuncMap(tplFuncs)
+ engine.LoadHTMLFiles(listHTMLTemplates("templates")...)
+ engine.Use(staticCacheHeaders(), crawlerHeaders())
+ registerBrowserDiscoveryRoutes(engine)
+ engine.Static("/static", "./static")
+}
+
+// registerBrowserDiscoveryRoutes 处理 Chrome DevTools 的本地工作区探测请求。
+// 该请求并非站点页面,返回 204 可避免进入业务 404 页面和产生误导日志。
+func registerBrowserDiscoveryRoutes(engine *gin.Engine) {
+ engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) {
+ c.Header("Cache-Control", "public, max-age=86400")
+ c.Header("X-Robots-Tag", "noindex, nofollow")
+ c.Status(http.StatusNoContent)
+ })
+}
+
+// staticCacheHeaders 为静态资源设置浏览器缓存,减少重复下载。
+func staticCacheHeaders() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if strings.HasPrefix(c.Request.URL.Path, "/static/") {
+ c.Header("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400")
+ }
+ c.Next()
+ }
+}
+
+// crawlerHeaders 防止接口、片段和健康检查等非 HTML 内容进入搜索索引。
+func crawlerHeaders() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ path := c.Request.URL.Path
+ if strings.HasPrefix(path, "/api/") ||
+ strings.HasPrefix(path, "/ui/") ||
+ strings.HasPrefix(path, "/h5/") ||
+ path == "/healthz" || path == "/enkey" || path == "/newenkey" {
+ c.Header("X-Robots-Tag", "noindex, nofollow")
+ }
+ c.Next()
+ }
+}
+
+// listHTMLTemplates 收集 templates 下页面模板,跳过遗留广告文件。
+func listHTMLTemplates(root string) []string {
+ var files []string
+ _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
+ if err != nil || d.IsDir() {
+ return err
+ }
+ name := strings.ToLower(d.Name())
+ if !strings.HasSuffix(name, ".html") {
+ return nil
+ }
+ if name == "banners.html" {
+ return nil
+ }
+ files = append(files, path)
+ return nil
+ })
+ if len(files) == 0 {
+ panic("no html templates found under " + root)
+ }
+ return files
+}
+
+// inJSONScript 将任意数据序列化为可嵌入
+{{end}}
diff --git a/app/templates/base/contentExtras.html b/app/templates/base/contentExtras.html
new file mode 100644
index 0000000..6934fc5
--- /dev/null
+++ b/app/templates/base/contentExtras.html
@@ -0,0 +1,87 @@
+{{define "contentPromo"}}
+{{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "detail_top" "SlotName" "详情顶部广告" "Label" "推广内容" "PCOnly" "1"}}
+{{end}}
+
+{{define "contentOfficialInfo"}}
+{{$cfg := .appConfig}}
+{{if or $cfg.NewURLAddr $cfg.OfficialAppAddr $cfg.NewURLAddr1 $cfg.NewURLAddr2 $cfg.ForeverURLAddr $cfg.TGGroup $cfg.QQ $cfg.GmailAddr}}
+
+ {{if $cfg.NewURLAddr}}
+ 官方入口:{{$cfg.NewURLAddr}}
+ {{end}}
+ {{if $cfg.NewURLAddr1}}
+ 最新地址发布页:{{$cfg.NewURLAddr1}} 👈务必牢记,防止迷路
+ {{end}}
+ {{if $cfg.NewURLAddr2}}
+ 备用访问地址:{{$cfg.NewURLAddr2}}
+ {{end}}
+ {{if $cfg.ForeverURLAddr}}
+ 海外永久地址:{{$cfg.ForeverURLAddr}}
+ {{end}}
+ {{if $cfg.OfficialAppAddr}}
+ 官方APP下载:{{$cfg.OfficialAppAddr}}
+ {{end}}
+ {{if $cfg.TGGroup}}
+ 点击加入黄果短剧官方Telegram群
+ {{end}}
+ {{if $cfg.QQ}}
+ 黄果短剧官方QQ群:{{$cfg.QQ}}
+ {{end}}
+ {{if $cfg.GmailAddr}}
+ 发送邮件到 {{$cfg.GmailAddr}} 获取最新地址
+ {{end}}
+
+{{end}}
+{{end}}
+
+{{define "contentAds"}}
+{{if .appConfig.DetailBannerList}}
+
+ {{range $ad := .appConfig.DetailBannerList}}
+
+
+
+ {{end}}
+
+{{end}}
+{{end}}
+
+{{define "contentHotSidebar"}}
+
+{{end}}
diff --git a/app/templates/base/dialogAd.html b/app/templates/base/dialogAd.html
new file mode 100644
index 0000000..a588279
--- /dev/null
+++ b/app/templates/base/dialogAd.html
@@ -0,0 +1,43 @@
+{{define "dialogAd"}}
+{{if .appConfig.DialogList}}
+
+
+ {{range $index, $dialog := .appConfig.DialogList}}
+
+ {{if $dialog.Link}}
+
+
+
+ {{else}}
+
+ {{end}}
+
+
+
+
+ {{end}}
+
+
+{{end}}
+{{end}}
diff --git a/app/templates/base/downloadAppDialog.html b/app/templates/base/downloadAppDialog.html
new file mode 100644
index 0000000..48e305b
--- /dev/null
+++ b/app/templates/base/downloadAppDialog.html
@@ -0,0 +1,25 @@
+{{define "downloadAppDialog"}}
+
+
+
+
+
+
+
+
扫码下载「黄果短剧 App」
+
海量短剧,随时畅看
+
+
+
下载app 免费看
+
+
+
+
+
+
+{{end}}
diff --git a/app/templates/base/downloadAppFloat.html b/app/templates/base/downloadAppFloat.html
new file mode 100644
index 0000000..6793abf
--- /dev/null
+++ b/app/templates/base/downloadAppFloat.html
@@ -0,0 +1,40 @@
+{{define "downloadAppFloat"}} {{if .appConfig.AppAddr}}
+
+
+
+ 下载 App
+
+
+{{end}} {{end}}
diff --git a/app/templates/base/eightPop.html b/app/templates/base/eightPop.html
new file mode 100644
index 0000000..8962e94
--- /dev/null
+++ b/app/templates/base/eightPop.html
@@ -0,0 +1,22 @@
+{{define "eightPop"}}
+
+
+
+
+
+
+
这是个成人网站
+
本网站包含有年龄限制的内容,包括裸体和露骨色情素材的内容。 点击继续即表示您确认您已年满 18 岁,或在您访问本网站时所在的司法管辖区已是成年人。
+
+
+ 我已年满18岁-进入
+ 我未满18岁-退出
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/app/templates/base/empty.html b/app/templates/base/empty.html
new file mode 100644
index 0000000..1351729
--- /dev/null
+++ b/app/templates/base/empty.html
@@ -0,0 +1,24 @@
+{{define "empty"}}
+
+
+
+
+
{{if .emptyTitle}}{{.emptyTitle}}{{else}}这里还没有内容{{end}}
+
{{if .emptyDesc}}{{.emptyDesc}}{{else}}暂时没有找到相关短剧,换个分类或返回首页看看吧{{end}}
+
+
+
+{{end}}
diff --git a/app/templates/base/footer.html b/app/templates/base/footer.html
new file mode 100644
index 0000000..3d8ea9c
--- /dev/null
+++ b/app/templates/base/footer.html
@@ -0,0 +1,168 @@
+{{define "footer"}}
+
+
+
+
+
+
+ 主页
+
+
+
+ 热榜
+
+
+
+ 片库
+
+
+
+ 最新
+
+
+
+ 我的
+
+
+
+
+
+{{template "scrollToTop" .}}
+{{template "downloadAppFloat" .}}
+
+{{template "pageConsole" .}}
+{{end}}
+
+{{define "footerNavItems"}}
+{{range .}}
+{{if .Name}}
+{{$footerHref := footerLinkURL .Name .URLValue}}
+{{if $footerHref}}
+{{if footerLinkIsExternal $footerHref}}
+{{.Name}}
+{{else}}
+{{.Name}}
+{{end}}
+{{end}}
+{{end}}
+{{end}}
+{{end}}
diff --git a/app/templates/base/header.html b/app/templates/base/header.html
new file mode 100644
index 0000000..ebf8c2b
--- /dev/null
+++ b/app/templates/base/header.html
@@ -0,0 +1,237 @@
+{{define "header"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{template "toast" .}} {{template "eightPop" .}} {{template "dialogAd" .}} {{end}}
diff --git a/app/templates/base/homeAdRow.html b/app/templates/base/homeAdRow.html
new file mode 100644
index 0000000..28367a3
--- /dev/null
+++ b/app/templates/base/homeAdRow.html
@@ -0,0 +1,35 @@
+{{define "homeAdRow"}}
+{{$ads := .Ads}}
+{{if $ads}}
+
+
+{{end}}
+{{end}}
diff --git a/app/templates/base/loginDialog.html b/app/templates/base/loginDialog.html
new file mode 100644
index 0000000..067cc33
--- /dev/null
+++ b/app/templates/base/loginDialog.html
@@ -0,0 +1,104 @@
+{{define "loginDialog"}}
+
+
+
+
+
+ 黄果短剧
+ 黄果短剧
+
+
登录后同步收藏、历史与会员权益
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/app/templates/base/pageConsole.html b/app/templates/base/pageConsole.html
new file mode 100644
index 0000000..7d3710d
--- /dev/null
+++ b/app/templates/base/pageConsole.html
@@ -0,0 +1 @@
+{{define "pageConsole"}}{{end}}
diff --git a/app/templates/base/pagination.html b/app/templates/base/pagination.html
new file mode 100644
index 0000000..23f357c
--- /dev/null
+++ b/app/templates/base/pagination.html
@@ -0,0 +1,67 @@
+{{define "pagination"}}
+
+{{if .pagination}}
+{{if .pagination.Visible}}
+
+
+{{end}}
+{{else}}
+
+
+{{end}}
+{{end}}
diff --git a/app/templates/base/posterCardTag.html b/app/templates/base/posterCardTag.html
new file mode 100644
index 0000000..93b323e
--- /dev/null
+++ b/app/templates/base/posterCardTag.html
@@ -0,0 +1,6 @@
+{{define "posterCardTag"}}
+{{$tags := cardTags .}}
+{{if $tags}}{{range $tags}}
+ {{if .URL}}{{.Name}} {{else}}{{.Name}} {{end}}
+{{end}} {{end}}
+{{end}}
diff --git a/app/templates/base/posterCornerBadges.html b/app/templates/base/posterCornerBadges.html
new file mode 100644
index 0000000..71c279b
--- /dev/null
+++ b/app/templates/base/posterCornerBadges.html
@@ -0,0 +1,13 @@
+{{define "posterCornerBadges"}}
+{{if or .IsFeatured .IsRecommend .IsNew}}
+
+{{if .IsFeatured}}
+
+{{else if .IsRecommend}}
+
+{{else if .IsNew}}
+
+{{end}}
+
+{{end}}
+{{end}}
diff --git a/app/templates/base/posterLikeBadge.html b/app/templates/base/posterLikeBadge.html
new file mode 100644
index 0000000..a5b5bf6
--- /dev/null
+++ b/app/templates/base/posterLikeBadge.html
@@ -0,0 +1,6 @@
+{{define "posterLikeBadge"}}
+
+
+
{{formatLikeCount .CountLike}}
+
+{{end}}
diff --git a/app/templates/base/rankingCardTag.html b/app/templates/base/rankingCardTag.html
new file mode 100644
index 0000000..1c684b3
--- /dev/null
+++ b/app/templates/base/rankingCardTag.html
@@ -0,0 +1,6 @@
+{{define "rankingCardTag"}}
+{{$tags := cardTags .}}
+{{if $tags}}{{range $tags}}
+ {{if .URL}}{{.Name}} {{else}}{{.Name}} {{end}}
+{{end}} {{end}}
+{{end}}
diff --git a/app/templates/base/registerDialog.html b/app/templates/base/registerDialog.html
new file mode 100644
index 0000000..47d4dbf
--- /dev/null
+++ b/app/templates/base/registerDialog.html
@@ -0,0 +1,80 @@
+{{define "registerDialog"}}
+
+
+
+
+
创建账号
+
+ 注册后同步收藏、历史与会员权益
+ 注册成功即可同步收藏与追剧记录
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/app/templates/base/scrollToTop.html b/app/templates/base/scrollToTop.html
new file mode 100644
index 0000000..ca961fb
--- /dev/null
+++ b/app/templates/base/scrollToTop.html
@@ -0,0 +1,7 @@
+{{define "scrollToTop"}}
+
+
+
+
+
+{{end}}
diff --git a/app/templates/base/searchDialog.html b/app/templates/base/searchDialog.html
new file mode 100644
index 0000000..72fb264
--- /dev/null
+++ b/app/templates/base/searchDialog.html
@@ -0,0 +1,21 @@
+{{define "searchDialog"}}
+
+{{end}}
diff --git a/app/templates/base/seoCommon.html b/app/templates/base/seoCommon.html
new file mode 100644
index 0000000..930c77b
--- /dev/null
+++ b/app/templates/base/seoCommon.html
@@ -0,0 +1,77 @@
+{{define "seoCommon"}} {{/* 全站 SEO 公共头:验证码 / OG / 移动适配 / 性能提示 /
+百度推送 */}}
+
+
+
+
+
+
+
+
+
+
+
+{{if .seoVerify.Baidu}} {{end}}
+
+
+
+
+
+{{if .canonicalURL}}
+
+
+{{end}}
+{{if .paginationPrevURL}} {{end}}
+{{if .paginationNextURL}} {{end}}
+
+
+
+
+
+
+{{/* 首屏壳层:合并 base+header+footer+eightPop,减少弱网 RTT(改源文件后跑
+scripts/build-shell-css.sh) */}}
+
+
+{{template "trackerInit" .}}
+
+{{end}}
diff --git a/app/templates/base/squarePromo.html b/app/templates/base/squarePromo.html
new file mode 100644
index 0000000..6c7dd77
--- /dev/null
+++ b/app/templates/base/squarePromo.html
@@ -0,0 +1,53 @@
+{{define "squarePromo"}}
+{{$ads := .Ads}}
+{{if $ads}}
+
+
+
+{{end}}
+{{end}}
diff --git a/app/templates/base/toast.html b/app/templates/base/toast.html
new file mode 100644
index 0000000..93a44d4
--- /dev/null
+++ b/app/templates/base/toast.html
@@ -0,0 +1,5 @@
+{{define "toast"}}
+
+
+
+{{end}}
diff --git a/app/templates/base/trackerInit.html b/app/templates/base/trackerInit.html
new file mode 100644
index 0000000..9295430
--- /dev/null
+++ b/app/templates/base/trackerInit.html
@@ -0,0 +1,167 @@
+{{define "trackerInit"}}
+{{/* PC:defer 正常加载;H5:延后注入 SDK,降低首屏带宽与主线程争抢 */}}
+
+{{/* PC 保留原 defer;H5 不在此拉取,由下方脚本空闲时注入 */}}
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/app/templates/base/vipDialog.html b/app/templates/base/vipDialog.html
new file mode 100644
index 0000000..6f1c241
--- /dev/null
+++ b/app/templates/base/vipDialog.html
@@ -0,0 +1,24 @@
+{{define "vipDialog"}}
+
+
+
+
+
+
+
+
+
本集为 VIP 专享
+
当前账号尚未开通会员,开通后即可继续观看。
+
全集免费看 · 无广告打扰 · 专属内容
+
开通 VIP 立即观看
+
暂不观看
+
+
+
+{{end}}
diff --git a/app/templates/page/404.html b/app/templates/page/404.html
new file mode 100644
index 0000000..8d3d7bc
--- /dev/null
+++ b/app/templates/page/404.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .title}}{{.title}}{{else}}页面未找到{{end}} - 黄果短剧
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
404
+
+ 页面走丢了
+
+
+ {{if .title}}{{.title}}{{else}}抱歉,没有找到这个页面{{end}}
+
+
页面可能已被删除、链接已失效,或暂时无法访问
+
+
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/aboutUs.html b/app/templates/page/aboutUs.html
new file mode 100644
index 0000000..ea70767
--- /dev/null
+++ b/app/templates/page/aboutUs.html
@@ -0,0 +1,141 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 关于我们 - 黄果短剧
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+
+ 首页
+ /
+ 关于我们
+
+
+
关于我们
+
了解黄果短剧的品牌、产品与服务
+
+
+
+
+
+
+
+ 关于我们
+
+
+
+
+
+
+ 品牌介绍
+
+ 黄果短剧是面向短剧爱好者打造的在线内容平台,聚合丰富题材与优质作品,为用户提供清晰、便捷且沉浸的观看体验。
+
+
+ 我们关注每一次真实的情绪共鸣,也重视创作者与观众之间的连接,让好故事更快被看见。
+
+
+
+
+ 产品愿景
+
+ 以简洁可靠的产品体验连接内容、创作者与观众,持续优化搜索、追剧、收藏、观看记录和多端同步等核心能力。
+
+
+ 品牌主张:每一帧都能解你的色
+
+
+
+
+ 联系我们
+
+ 商务合作:business@qimengshorts.com
+
+
+ 用户反馈:support@qimengshorts.com
+
+ 服务时间:工作日 10:00–18:00
+
+ 最后更新:2026年7月23日
+
+
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/application.html b/app/templates/page/application.html
new file mode 100644
index 0000000..92049af
--- /dev/null
+++ b/app/templates/page/application.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}福利应用 - 黄果短剧{{end}}
+
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+ 首页
+ /
+ 福利应用
+
+ 福利应用
+ {{if .categories}}
+
+ {{range $i, $cat := .categories}}
+
+ {{$cat.Title}}
+
+ {{end}}
+
+
+ {{range $i, $cat := .categories}}
+
+
+
{{$cat.Title}}
+ {{if $cat.Intro}}
+
+ {{$cat.Intro}}
+
+ {{end}}
+
+
+
+ 显示全部
+
+
+
+ {{end}} {{else}}
+
+ {{end}}
+
+ {{template "footer" .}}
+
+
+
+
diff --git a/app/templates/page/categoryDetail.html b/app/templates/page/categoryDetail.html
new file mode 100644
index 0000000..8068e70
--- /dev/null
+++ b/app/templates/page/categoryDetail.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else}}{{.title}} - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}{{end}}
+
+
+
+
+
+{{template "header" .}}
+
+
+
+ 首页 / {{.pageName}}
+
+
+
{{.pageName}}
+
{{.categoryDescription}}
+
+
{{.pageName}}作品
+
+ {{template "pagination" .}}
+
+
+{{template "footer" .}}
+
+
diff --git a/app/templates/page/collect.html b/app/templates/page/collect.html
new file mode 100644
index 0000000..fd7bd78
--- /dev/null
+++ b/app/templates/page/collect.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}我的收藏 - 黄果短剧{{end}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{template "header" .}}
+
+
+
+ 个人中心
+ /
+ 我的收藏 我的追剧
+
+
+
+
+
+
+
+
+ 我的收藏
+
+
+
+
+
+
+ {{if eq .collectType "chapter"}}
+
+ {{else}}
+
+ {{end}}
+
+
+
+
+{{template "footer" .}}
+
+
diff --git a/app/templates/page/deposit.html b/app/templates/page/deposit.html
new file mode 100644
index 0000000..17f989a
--- /dev/null
+++ b/app/templates/page/deposit.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .title}}{{.title}} - 黄果短剧{{else}}金币充值 - 黄果短剧{{end}}
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
+ 个人中心
+ /
+ 金币充值
+
+
+
+
WALLET
+
金币充值
+
当前余额 {{if .rechargeText}}{{.rechargeText}}{{else}}0{{end}} 金币
+
+
+
+ 钱包充值
+ 选支付 · 立即到账
+ 支持多种支付方式,支付成功后即时到账。
+
+
+
购买套餐
+
+ 金币充值 按集解锁,自由使用
+
+ {{range .coinList}}
+
+ ¥{{.PriceText}}
+ {{.Name}}
+ {{if .Hint}}{{.Hint}} {{end}}
+ ✓
+
+ {{else}}
暂无金币套餐
{{end}}
+
+
+
+
+ {{if .payMethods}}立即支付{{else}}暂无支付方式{{end}}
+
+
请选择金币套餐
+
+
+ 交易明细
+
+ {{range .txList}}
+
+
+
{{if .Title}}{{.Title}}{{else if .TranTypeName}}{{.TranTypeName}}{{else if .Remark}}{{.Remark}}{{else}}金币变动{{end}}
+ {{if .CreatedAt}}
{{formatShortDateTime .CreatedAt}}
{{end}}
+
+
+ {{if eq .ChangeAmount 0}}{{if eq .FlowDir 1}}+{{else}}-{{end}}{{absInt64 .Amount}}{{else}}{{if eq .FlowDir 1}}+{{else}}-{{end}}{{absInt64 .ChangeAmount}}{{end}}
+
+
+ {{else}}
暂无交易记录
{{end}}
+
+
+
+
+
+
+ 个人中心 / 金币充值
+
+
+
+ 金币余额 (金币)
+ {{if .rechargeText}}{{.rechargeText}}{{else}}000.00{{end}}
+
+
+
+ 请选择充值金额 首次充值赠送10%
+
+ {{range .coinList}}
+
+
+ {{.Name}}
+ {{if .Hint}}{{.Hint}}{{else}}赠送 999金币{{end}}
+ ¥ {{.PriceText}}
+ ✓
+
+ {{else}}
暂无金币套餐
{{end}}
+
+
+
+
+ 金币怎么用?
+
+
解锁单集内容 按剧集实际定价消耗金币
+
奖励金币优先使用 解锁时系统自动优先扣除任务/签到获得的奖励金币
+
金币畅看范围内不消耗金币 会员权益仍优先于金币,避免重复付费
+
+
+
+
+
+
{{if .payMethods}}支付 ¥0 充值金币{{else}}暂无支付方式{{end}}
+
如遇充值问题请咨询 在线客服
+
+
+
+
{{range .payMethods}}{{.TypeName}} {{end}}
+
{{range .payMethods}}{{.TypeName}} {{end}}
+
+
+
+
+
+
+
确认支付
+ 金币充值
+
+
+
+ 300 金币
+ 支付成功后即时到账
+
+
¥30
+
+ 选择支付方式
+
+ 去支付
+ 点击支付即表示已确认订单金额与支付方式
+
+
+
+
+
+
+ {{template "footer" .}}
+
+
+
diff --git a/app/templates/page/detail.html b/app/templates/page/detail.html
new file mode 100644
index 0000000..c118c76
--- /dev/null
+++ b/app/templates/page/detail.html
@@ -0,0 +1,247 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{$siteTitle := "黄果短剧"}}{{if and .detail .detail.SeoInfo.WebSiteName}}{{$siteTitle = .detail.SeoInfo.WebSiteName}}{{else if .seoInfo.WebSiteName}}{{$siteTitle = .seoInfo.WebSiteName}}{{else if .appConfig.SeoInfo.WebSiteName}}{{$siteTitle = .appConfig.SeoInfo.WebSiteName}}{{end}}
+ {{$pageTitle := "短剧详情"}}{{if .detail}}{{if .detail.SeoInfo.Title}}{{$pageTitle = .detail.SeoInfo.Title}}{{else}}{{$pageTitle = .detail.Title}}{{end}}{{else if .title}}{{$pageTitle = .title}}{{end}}
+ {{$pageTitle}} - {{$siteTitle}}
+
+
+
+
+
+ {{if .detail}}{{if .detail.Cover}}
+ {{end}}
+
+ {{end}}
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+
+{{template "header" .}}
+{{if .detail}}
+{{with .detail}}
+
+
+
+
+
+
+
+
+
+
+
+ {{.CountLike}}
+
+
+
+
+
+
+
+
+
+
+
+ 首页
+ /
+ 短剧
+ /
+ {{.Title}}
+
+
+
+
+
+
+
+ {{$detailTags := namedTags .TagIDs .TagNames .TagSlugs}}
+ {{if $detailTags}}
+
+ {{end}}
+
{{.Title}}
+
+
+ 集数:{{if .TotalEpisode}}{{.TotalEpisode}}集{{else}}更新中{{end}}
+ 上线时间 : {{playletDateText .OnlineAt .MediaCreatedAt .CreatedAt}}
+
+
+ {{if .TotalEpisode}}全{{.TotalEpisode}}集{{else}}短剧{{end}}
+ ·
+ {{browseText .CountBrowse}}
+
+
+
+
+
简介
+
简介: {{if .Desc}}{{.Desc}}{{else}}暂无简介{{end}}
+
+
+
立即播放
+
+
+
+
+
+ {{if eq .IsLike 1}}已点赞{{else}}点赞{{end}}
+
+
+
+
+
+
+ {{if eq .IsCollect 1}}已收藏{{else}}收藏{{end}}
+
+
+
+
+
+
+ 分享
+
+
+
+
+
+
+ 反馈
+
+
+
+
+
+
+ {{template "contentPromo" $}}
+
+
+
+
剧集
+
+
{{updateStatusText .UpdateStatus}}{{if .TotalEpisode}} 共{{.TotalEpisode}}集{{end}}
+
+
+
+ {{with $.episodeRanges}}
+
+ {{range $i, $r := .}}
+ {{$r.Label}}
+ {{end}}
+
+ {{end}}
+
+
+
+ {{template "contentOfficialInfo" $}}
+ {{template "contentAds" $}}
+
+
+
+
+
+
+
+
+
+
+ {{if eq .IsLike 1}}已点赞{{else}}点赞{{end}}
+
+
开始播放
+
+{{end}}
+{{else}}
+
+ {{$pageTitle}} - {{$siteTitle}}
+ {{template "empty" .}}
+
+{{end}}
+{{template "footer" .}}
+{{template "vipDialog" .}}
+{{template "coinDialog" .}}
+
+
+
diff --git a/app/templates/page/downloadApp.html b/app/templates/page/downloadApp.html
new file mode 100644
index 0000000..a5b449c
--- /dev/null
+++ b/app/templates/page/downloadApp.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else}}下载APP - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+
下载黄果短剧 App
+
扫码安装,继续追剧不丢进度
+
+
+ ×
+
+
+
+
追剧更稳定
+
+ App 内播放、切集和续看更稳定,适合长时间追剧。
+
+
+
+
记录自动同步
+
+ 观看历史、收藏和追剧记录与 Web 账号同步。
+
+
+
+
更新及时提醒
+
+ 关注的剧集更新后提醒,减少回访流失。
+
+
+
+
+
+
+ iOS
+
+ Android
+
+
+
+
+
复制下载链接
+
二维码与下载链接指向同一地址
+
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/editPassword.html b/app/templates/page/editPassword.html
new file mode 100644
index 0000000..74f45f8
--- /dev/null
+++ b/app/templates/page/editPassword.html
@@ -0,0 +1,170 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 修改登录密码 - 黄果短剧
+
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
+ 个人中心
+ /
+ 账号设置
+ /
+ 修改密码
+
+
+
修改登录密码
+
+ 建议使用 8–20 位字母、数字和符号组合
+
+
+
+
+
+
+
+
+
+
修改登录密码
+
+
+
+
+
+
+ {{template "footer" .}}
+
+
+
diff --git a/app/templates/page/editProfile.html b/app/templates/page/editProfile.html
new file mode 100644
index 0000000..62134d9
--- /dev/null
+++ b/app/templates/page/editProfile.html
@@ -0,0 +1,118 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 编辑个人资料 - 黄果短剧
+
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+ 编辑个人资料
+
+
+
+
+
+
+
+
编辑个人资料
+
+
+
+
+
+
+ {{template "footer" .}}
+
+
+
diff --git a/app/templates/page/feedback.html b/app/templates/page/feedback.html
new file mode 100644
index 0000000..b38a4fd
--- /dev/null
+++ b/app/templates/page/feedback.html
@@ -0,0 +1,169 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 问题反馈 - 黄果短剧
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
+
+
+ 问题反馈
+
+
+
+
+
+
+
+
+
+
+
+ 问题类型 *
+
+
+ 播放卡顿/缓冲
+ 无法播放
+ 画质模糊
+ 音画不同步
+ 加载失败
+ 闪退/崩溃
+ 登陆异常
+ 其他问题
+
+
+
+
+ 网络环境信息 *
+
+
所在地址
+
+
+ 所在省份
+ 北京市 天津市 河北省 山西省 内蒙古自治区
+ 辽宁省 吉林省 黑龙江省 上海市 江苏省
+ 浙江省 安徽省 福建省 江西省 山东省
+ 河南省 湖北省 湖南省 广东省 广西壮族自治区
+ 海南省 重庆市 四川省 贵州省 云南省
+ 西藏自治区 陕西省 甘肃省 青海省 宁夏回族自治区
+ 新疆维吾尔自治区 香港特别行政区 澳门特别行政区 台湾省 海外
+
+
+ 城市(选填)
+
+
+
运营商
+
+ 选择运营商
+ 中国移动
+ 中国联通
+ 中国电信
+ 中国广电
+ 其他
+
+
网络类型
+
+ WIFI
+ 5G
+ 4G
+ 其他
+
+
+
+
+
+ 问题发生频率 *
+
+ 每次都会
+ 经常出现
+ 偶尔出现
+ 仅现一次
+
+
+
+
+ 设备信息 *
+
+
设备类型
+
+ iPhone
+ Android
+ iPad
+ 其他
+
+
设备型号(选填)
+
+
APP版本号(选填)
+
+
+
+
+
+
+
+ 上传图片
+
+
+
+
+
+
+ 上传图片/视频
+ 支持jpg/png 最多3个文件
+
+
+
+
+
+
+
+ 提交反馈
+ 提交后我们将在1-3个工作日内处理 感谢您的反馈!
+
+
+
+
+ {{template "footer" .}}
+
+
+
diff --git a/app/templates/page/helpCenter.html b/app/templates/page/helpCenter.html
new file mode 100644
index 0000000..e57a116
--- /dev/null
+++ b/app/templates/page/helpCenter.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 帮助中心 - 黄果短剧
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+
+
+
+
+
+ 帮助中心
+
+
+
+
+
+ 常见问题
+
+ {{if .faqItems}}
+ {{range $index, $item := .faqItems}}
+
+
{{$item.Question}}
+
{{$item.Answer}}
+
+ {{if lt (add $index 1) (len $.faqItems)}}
+
+ {{end}}
+ {{end}}
+ {{else}}
+
+
无法登录账号怎么办?
+
请确认邮箱与密码是否正确;若忘记密码,可在登录页面使用“找回密码”。
+
+
+
+
观看记录没有同步?
+
请确认已登录同一账号并保持网络连接,记录通常会自动同步。
+
+
+
+
视频无法播放或加载缓慢?
+
建议切换网络、刷新页面或更新浏览器;问题持续时可提交反馈。
+
+
+
+
如何管理收藏内容?
+
进入个人中心的“追剧与收藏”页面,可查看或取消收藏。
+
+ {{end}}
+
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/history.html b/app/templates/page/history.html
new file mode 100644
index 0000000..1e2cb81
--- /dev/null
+++ b/app/templates/page/history.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}观看历史 - 黄果短剧{{end}}
+
+
+
+
+
+
+
+
+
+
+
+
+{{template "header" .}}
+
+
+
+ 个人中心
+ /
+ 观看历史 历史记录
+
+
+
+
+
+
+
+
+ 观看历史
+ 清空
+
+
+
+
+
+
+ {{template "empty" .}}
+
+
+
+
+
+
+
+
+
+
+
+
+
清空观看历史
+
确定清空全部观看历史吗?此操作不可恢复。
+
+ 取消
+ 清空
+
+
+
+
+{{template "footer" .}}
+
+
+
diff --git a/app/templates/page/homeRoad.html b/app/templates/page/homeRoad.html
new file mode 100644
index 0000000..12fa979
--- /dev/null
+++ b/app/templates/page/homeRoad.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else}}{{.title}}{{end}}
+
+
+
+
+
+ {{if .jsonLD}}{{end}}
+
+
+
+
+{{template "header" .}}
+
+
+ 首页 / 回家的路
+
+
+
+ 官方权威发布 · 永久有效入口
+
+ 黄果短剧 · 官方最新地址发布页
+ 实时同步最新可用线路与备用入口,帮助你在不同网络环境下快速返回。
+
+
+
+
+
+
+
国内可用地址
+
(直连无需代理)
+
+ 以下线路均经过可用性检查,优先选择高速线路。
+
+ {{range .homeRoad.Routes}}
+
+
{{.Label}} ● 极速
+ {{navDisplayURL .URL}}
+
+
+ {{end}}
+
+
+
+
+
+
+
永久访问地址
+
建议收藏
+
+ 同时保存永久入口和备用入口,主域名变更时仍可返回。
+
+ {{range .homeRoad.Permanent}}
+
+ {{.Label}} {{navDisplayURL .URL}}
+ 复制并保存
+
+ {{end}}
+
+
+
+
+
+
+
+
+
访问异常排查
+
安全提示
+
+ 遇到打不开、跳转慢或入口失效时,按顺序完成排查。
+
+
01 打不开页面先切换网络,再尝试永久入口或备用入口。
+
02 地址跳转异常确认地址拼写完整,不要从陌生弹窗进入。
+
03 入口失效返回本页查看最新入口,或从收藏页重新进入。
+
+
+
+
+
+
+
设备使用与兼容性建议
+
+ 建议使用主流浏览器访问,以获得更好的兼容性与加载速度;遇到缓慢或无法访问时可切换线路或刷新页面。
+
+
+ iPhone 用户
+ ✓ 推荐使用 Safari 浏览器
! 避免在微信 / QQ 等第三方 App 内打开
+
+
+ Android 用户
+ ✓ 推荐使用 Google Chrome 浏览器
! 自带浏览器可能存在兼容与域名拦截风险
+
+
+ 电脑 / 桌面端
+ ✓ 首选 Chrome / Edge 浏览器
! 无法加载时可清除 DNS 缓存或更换公共 DNS
+
+
+
+
+
+ FAQ
+
+ {{range .faqItems}}
+
{{.Question}} {{.Answer}}
+ {{end}}
+
+
+
+{{template "footer" .}}
+
+
+
diff --git a/app/templates/page/index.html b/app/templates/page/index.html
new file mode 100644
index 0000000..3e21036
--- /dev/null
+++ b/app/templates/page/index.html
@@ -0,0 +1,214 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .appConfig.SeoInfo.Title}}{{.appConfig.SeoInfo.Title}}{{else if .title}}{{.title}}{{else}}黄果短剧 - 热门短剧在线看{{end}}
+
+
+
+
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+ {{/* PC 需要 Swiper 样式;H5 延后按需注入,避免首屏下载与强制重排 */}}
+
+
+
+ {{if .homeBannerData}}{{with index .homeBannerData 0}}
+ {{$heroSrc := coverURL .Image ""}}
+ {{if $heroSrc}} {{end}}
+ {{end}}{{end}}
+
+
+{{template "header" .}}
+
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .appConfig.SeoInfo.Title}}{{.appConfig.SeoInfo.Title}}{{else if .title}}{{.title}}{{else}}黄果短剧 - 热门短剧在线看{{end}}
+ {{if .homeBannerData}}
+
+
+
+
+ {{range $i, $banner := .homeBannerData}}
+
+ {{end}}
+
+
+
+
+
+
+ {{range $i, $banner := .homeBannerData}}
+
+
+
+
+ {{end}}
+
+
+
+
+
+
+
+ {{end}}
+
+ {{if .announcements}}
+
+
+
+
+
+
+
+
+
+ {{range $i, $announcement := .announcements}}{{if $i}} · {{end}}{{if $announcement.Title}}{{$announcement.Title}} {{if $announcement.Content}}:{{end}}{{end}}{{$announcement.Content}}{{end}}
+ {{range $i, $announcement := .announcements}}{{if $i}} · {{end}}{{if $announcement.Title}}{{$announcement.Title}} {{if $announcement.Content}}:{{end}}{{end}}{{$announcement.Content}}{{end}}
+
+
+
+ {{end}}
+
+ {{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "home_detail_top" "SlotName" "首页方块广告" "Label" "广告"}}
+
+ {{range $catIndex, $category := .homeCategoryData}}
+ {{if contains $category.Name "热榜"}}
+
+ {{else}}
+
+ {{end}}
+
+ {{template "homeAdRow" dict "Ads" ($.appConfig.NineGridRow $catIndex) "SlotKey" "home_nine_grid" "SlotName" "首页横幅广告" "Label" "广告"}}
+ {{end}}
+
+ {{if .homeRecommendThemes}}
+
+ {{end}}
+
+{{template "footer" .}}
+
+
+
+
diff --git a/app/templates/page/list.html b/app/templates/page/list.html
new file mode 100644
index 0000000..38cdf9a
--- /dev/null
+++ b/app/templates/page/list.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}短剧列表 - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+
+{{template "header" .}}
+
+
+ {{$navTitle := "AI成人短剧"}}
+ {{range .headerNav}}{{if .IsActive}}{{$navTitle = .Name}}{{end}}{{end}}
+
+ 首页 / {{$navTitle}}
+
+
{{$navTitle}}在线观看
+
+ {{range .filters}}
+
+ {{.Label}}
+
+ {{end}}
+
+
{{.sectionTitle}}
+
+ {{template "pagination" .}}
+ {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "list_nine_grid" "SlotName" "片库横幅广告" "Label" "广告"}}
+ {{if .appConfig.BottomBanner}}
+
+ {{range $ad := .appConfig.BottomBanner}}
+
+
+
+ {{end}}
+
+ {{end}}
+
+
+{{template "footer" .}}
+
+
diff --git a/app/templates/page/more.html b/app/templates/page/more.html
new file mode 100644
index 0000000..f5454b2
--- /dev/null
+++ b/app/templates/page/more.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}今日推荐 - 黄果短剧{{end}}
+
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+ {{template "header" .}}
+
+
+
+ 首页
+ /
+ 专题
+ /
+ {{if .pageTitle}}{{.pageTitle}}{{else}}今日推荐{{end}}
+ {{if .topicHeading}}{{.topicHeading}}{{else if .pageTitle}}{{.pageTitle}}介绍{{else}}今日推荐介绍{{end}}
+
+
+
{{if .topicHeading}}{{.topicHeading}}{{else if .pageTitle}}{{.pageTitle}}介绍{{else}}今日推荐介绍{{end}}
+
{{if .topicDescription}}{{.topicDescription}}{{else}}精品高分 AI 成人短剧专题,精选高人气、高评分 AI 原创短剧,每日持续更新。涵盖多种热门剧情与角色设定,为你带来高质量、高清流畅的观影体验。{{end}}
+
+
专题作品
+
+ {{template "pagination" .}}
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/nav.html b/app/templates/page/nav.html
new file mode 100644
index 0000000..090718e
--- /dev/null
+++ b/app/templates/page/nav.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .appConfig.SeoNavInfo.DhTitle}}{{.appConfig.SeoNavInfo.DhTitle}}{{else if .appConfig.SeoNavInfo.DhSiteName}}{{.appConfig.SeoNavInfo.DhSiteName}}{{else if .title}}{{.title}}{{else}}黄果短剧 · 官方导航{{end}}
+
+
+
+
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+
+ {{if .appConfig.SeoNavInfo.DhSiteName}}{{.appConfig.SeoNavInfo.DhSiteName}}{{else if .appConfig.SeoNavInfo.DhTitle}}{{.appConfig.SeoNavInfo.DhTitle}}{{else if .title}}{{.title}}{{else}}黄果短剧 · 官方导航{{end}}
+
+
+
+
+ 全网最新最火短剧,黄果为你呈现
+ 永久免费 最新资源 无需下载
+
+
+
+
+ {{if .appConfig.AppAddr}}
+
+
+ 黄果短剧 · APP 下载
+
+ {{end}}
+
+ {{if .appConfig.TGGroup}}
+ 官方联系:{{.appConfig.TGGroup}}
+ {{end}}
+
+
+
+
+
+{{template "scrollToTop" .}}
+{{template "downloadAppFloat" .}}
+{{template "pageConsole" .}}
+
+
+{{template "dialogAd" .}}
+
+
+
diff --git a/app/templates/page/play.html b/app/templates/page/play.html
new file mode 100644
index 0000000..acea133
--- /dev/null
+++ b/app/templates/page/play.html
@@ -0,0 +1,497 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{$siteTitle := "黄果短剧"}}{{if and .playlet .playlet.SeoInfo.WebSiteName}}{{$siteTitle = .playlet.SeoInfo.WebSiteName}}{{else if and .chapter .chapter.SeoInfo.WebSiteName}}{{$siteTitle = .chapter.SeoInfo.WebSiteName}}{{else if .seoInfo.WebSiteName}}{{$siteTitle = .seoInfo.WebSiteName}}{{else if .appConfig.SeoInfo.WebSiteName}}{{$siteTitle = .appConfig.SeoInfo.WebSiteName}}{{end}}
+ {{$pageTitle := "在线播放"}}
+ {{if .chapter}}
+ {{if .chapter.SeoInfo.Title}}{{if .chapter.CurrentEpisode}}{{$pageTitle = printf "%s - 第%d集" .chapter.SeoInfo.Title .chapter.CurrentEpisode}}{{else}}{{$pageTitle = .chapter.SeoInfo.Title}}{{end}}
+ {{else if .playlet}}{{if .playlet.SeoInfo.Title}}{{if .chapter.CurrentEpisode}}{{$pageTitle = printf "%s - 第%d集" .playlet.SeoInfo.Title .chapter.CurrentEpisode}}{{else}}{{$pageTitle = .playlet.SeoInfo.Title}}{{end}}{{else if .title}}{{$pageTitle = .title}}{{end}}
+ {{else if .title}}{{$pageTitle = .title}}{{end}}
+ {{else if .playlet}}{{if .playlet.SeoInfo.Title}}{{$pageTitle = .playlet.SeoInfo.Title}}{{else if .title}}{{$pageTitle = .title}}{{end}}
+ {{else if .title}}{{$pageTitle = .title}}{{end}}
+ {{$pageTitle}} - {{$siteTitle}}
+
+
+
+
+
+ {{if .playlet}}{{if .playlet.Cover}}
+ {{end}}{{end}}
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+
+
+
+
+
+{{template "header" .}}
+{{$playlet := .playlet}}
+{{$chapter := .chapter}}
+{{$playletID := .playletID}}
+{{$playletSlug := .playletSlug}}
+{{$canWatch := false}}
+{{$needUnlock := false}}
+{{$needVip := false}}
+{{$unlockCost := 0}}
+{{if $chapter}}
+{{$canWatch = $chapter.AllowWatch}}
+{{$needUnlock = $chapter.NeedUnlock}}
+{{$needVip = $chapter.NeedVip}}
+{{$unlockCost = $chapter.UnlockCost}}
+{{end}}
+{{if or $chapter $playlet}}
+{{$isLike := 2}}
+{{$isCollect := 2}}
+{{$countLike := 0}}
+{{$countCollect := 0}}
+{{if $playlet}}
+{{$isLike = $playlet.IsLike}}
+{{$isCollect = $playlet.IsCollect}}
+{{$countLike = $playlet.CountLike}}
+{{$countCollect = $playlet.CountCollect}}
+{{end}}
+
+
+ 首页
+ /
+ 短剧
+ /
+ {{if $playletID}}
+ {{if $playlet}}{{$playlet.Title}}{{else}}短剧{{end}}
+ {{else}}
+ {{if $playlet}}{{$playlet.Title}}{{else}}短剧{{end}}
+ {{end}}
+ {{if $chapter}}
+ /
+ 第{{$chapter.CurrentEpisode}}集
+ {{end}}
+
+
+
+
+ {{if and $chapter $canWatch $chapter.VideoURL}}
+
+ {{end}}
+ {{if $playlet}}
+
+ {{else}}
+
+ {{end}}
+ {{if and $chapter $canWatch $chapter.VideoURL}}
+
+
+
+ {{end}}
+
+
+
+
+
+
+
+
+
{{if $chapter}}第{{$chapter.CurrentEpisode}}集{{else}}播放{{end}}
+
+
+
+ 1.0x
+
+
+
+
+
+
+
+
+
+
+ {{$countLike}}
+
+
+
+ {{if $chapter}}{{$chapter.CountComment}}{{else}}0{{end}}
+
+
+
+ {{if eq $isCollect 1}}已收藏{{else}}收藏{{end}}
+
+
+
+ 分享
+
+
+
+
+
+
+ 自动播放下一集
+ 倒计时3s
+
+
+
+
+
+ 选集{{if $playlet}}{{if $playlet.TotalEpisode}}·全{{$playlet.TotalEpisode}}集{{end}}·{{updateStatusText $playlet.UpdateStatus}}{{end}}
+
+
+
+
+
+
+
+
+ {{template "contentPromo" $}}
+
+
+
+
{{if $chapter}}第 {{$chapter.CurrentEpisode}} 集{{else}}—{{end}}
+
{{if $playlet}}{{$playlet.Title}}{{else}}短剧播放{{end}}
+ {{if $playlet}}
+ {{if $playlet.TagNames}}
+
+ {{end}}
+ {{end}}
+
+ {{if and $playlet $chapter}}{{$playlet.Title}} - 第{{printf "%02d" $chapter.CurrentEpisode}}集{{else if $playlet}}{{$playlet.Title}}{{else}}—{{end}}
+
+ {{if $playlet}}
+
+ 集数:{{$playlet.TotalEpisode}}集
+ 上线时间 : {{playletDateText $playlet.OnlineAt $playlet.MediaCreatedAt $playlet.CreatedAt}}
+
+
{{if $playlet.Desc}}{{$playlet.Desc}}{{else}}暂无简介{{end}}
+ {{end}}
+
+
+
+
+
+
+ {{if eq $isLike 1}}已点赞{{else}}点赞{{end}}
+
+
+
+
+
+
+ {{if eq $isCollect 1}}已收藏{{else}}收藏{{end}}
+
+
+
+
+
+
+ 分享
+
+
+
+
+
+
+ 反馈
+
+
+
下载APP
+
+
+
+ 剧集
+ {{with $.episodeRanges}}
+
+ {{range $i, $r := .}}
+ {{$r.Label}}
+ {{end}}
+
+ {{end}}
+
+
+
+ {{template "contentOfficialInfo" $}}
+ {{template "contentAds" $}}
+
+ {{if $chapter}}
+
+ {{end}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
倍速
+
+
+
+
+
+
+
+ {{if $playlet}}
+
+
+
+
+
+
{{$playlet.Title}}
+
+
+
+
+
{{updateStatusText $playlet.UpdateStatus}}{{if $playlet.TotalEpisode}} 共{{$playlet.TotalEpisode}}集{{end}}
+
+
+
+ {{if eq $isCollect 1}}已收藏{{else}}收藏{{end}}
+
+
+ {{end}}
+
+ {{range $i, $r := $.episodeRanges}}
+ {{$r.Label}}
+ {{end}}
+ {{if and $playlet $playlet.RelatedRecommendData}}
+ 更多好剧
+ {{end}}
+
+
+ {{if and $playlet $playlet.RelatedRecommendData}}
+
+ {{end}}
+
+
+{{if $chapter}}
+
+{{end}}
+{{else}}
+
+ 短剧播放 - 黄果短剧
+ {{template "empty" .}}
+
+{{end}}
+
+{{template "footer" .}}
+{{template "vipDialog" .}}
+{{template "coinDialog" .}}
+
+
+
+
diff --git a/app/templates/page/privacyPolicy.html b/app/templates/page/privacyPolicy.html
new file mode 100644
index 0000000..c7a2f0d
--- /dev/null
+++ b/app/templates/page/privacyPolicy.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 隐私政策 - 黄果短剧
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+
+ 首页
+ /
+ 隐私政策
+
+
+
隐私政策
+
我们重视您的个人信息与隐私安全
+
+
+
+
+
+
+
+ 隐私政策
+
+
+
+
+
+ 隐私政策
+ 黄果短剧(以下简称本公司)非常重视您的隐私权,并依据《个人资料保护法》(以下简称个资法)的规定,向您告知下列有关隐私权保护政策的更多内容。
+
+一、本公司使用您个人资料的目的
+(一)会员管理
+(二)提供会员服务,例如:发送电子邮件通知、调阅游戏历程
+
+二、个人资料收集及利用之期间、对象、地区及方式
+(一)期间:平台提供相关服务及营运期间。
+(二)对象:本公司。个人资料的收集及利用不适用于非本公司所有或控制的公司,也不适用于非本公司雇用或管理之人,本公司的关系企业或子公司将使用其原先的隐私权政策。
+(三)地区:营运之地区。
+(四)方式:本公司在您使用官网服务信箱或进行其他活动时,会收集您所提供的姓名、电子邮件地址、联络方式等。
+
+三、依据个资法第三条规定,您就本公司保有个人资料得行使之权利及方式
+(一)得行使之权利:查询、请求阅览;请求制给复制本;请求补充或更正;请求停止收集、处理或利用;请求删除。
+(二)行使权利之方式:以书面将申请行使上述权利的资料送至本公司所在地。
+
+四、您不同意提供个人资料所致权益之影响
+您可自由选择是否提供个人资料给本公司,但若特定资料栏位属于必填栏位,您若不提供该等资料,则可能造成无法使用本公司相关的产品及服务。建议您应提供完整的个人资料并随时更新。若因您未提供完整且真实的个人资料,造成您及第三人发生损害时,您应负担一切责任。
+
+五、资料分享与披露
+除非是为了向您提供您所要求的产品或服务、获得您的授权,或有以下情形,否则本公司不会将您的个人资料披露给第三人或用于收集目的以外的其他用途:
+(一)本公司因业务需要,有必要委托本公司相关单位或关系企业提供服务的情形,本公司将严格要求其遵守保密义务,并采取必要检查程序以确定其将确实遵守。
+(二)本公司对法院传票、行政命令或法律程序作出回应或行使法律权利,或对诉讼上的请求提出防御的情形。
+(三)为了调查和防止非法活动、涉嫌欺诈、对人身安全有潜在威胁的情况、对本公司服务条款的违反,或为了对上述情形采取应对措施,或法律另有规定,而有必要提供您的个人资料时。
+(四)如果本公司全部或部分业务被其他公司收购或合并,我们会将您的个人资料移转给该公司。如果发生这种情况,本公司会在您的个人资料被移转且将适用不同的隐私权政策前通知您。
+
+六、机密性与安全性
+我们会将有权存取您个人资料的员工,限定于我们合理相信是为了向您提供产品或服务或为了完成工作而需要取得这些资料的员工。我们将采取符合法规要求的实体、电子和程序防护措施,以保护您个人资料的安全,防止您的个人资料被窃取、篡改、毁损、灭失或泄漏。
+
+七、隐私权保护政策修订
+本公司有权不时修订本政策。当我们在个人资料及隐私权政策的处理上有重大变更时,会在我们的网站上张贴告示。请您密切注意以保障个人权益。当您于隐私权政策修改或变更后继续使用本公司所提供之产品及服务,即表示您已阅读、了解并同意接受隐私权政策修改或变更后的内容。如果您不同意本隐私权政策,请您立即停止使用本公司所提供之产品及服务。
+
+八、问题和建议
+如果您有任何问题或建议,请通过客户服务信箱与我们联系。
+
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/rank.html b/app/templates/page/rank.html
new file mode 100644
index 0000000..20989a1
--- /dev/null
+++ b/app/templates/page/rank.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}短剧排行榜 - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+
+{{template "header" .}}
+
+
+
+ 首页 / 排行榜
+
+
+
全站排行榜
+
聚合站内播放、评分和近期热度表现突出的短剧内容,快速发现当前值得看的作品。
+
+
+
+ {{template "pagination" .}}
+ {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "rank_nine_grid" "SlotName" "排行榜横幅广告" "Label" "广告"}}
+ {{if .appConfig.BottomBanner}}
+
+ {{range $ad := .appConfig.BottomBanner}}
+
+
+
+ {{end}}
+
+ {{end}}
+
+
+{{template "footer" .}}
+
+
diff --git a/app/templates/page/richtext.html b/app/templates/page/richtext.html
new file mode 100644
index 0000000..3468289
--- /dev/null
+++ b/app/templates/page/richtext.html
@@ -0,0 +1,132 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+
+ {{if .title}}{{.title}} - 黄果短剧{{else}}内容详情 - 黄果短剧{{end}}
+
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+
+ 首页
+ /
+ {{if
+ .richtext.Title}}{{.richtext.Title}}{{else}}内容详情{{end}}
+
+
+
+ {{if .richtext.Title}}{{.richtext.Title}}{{else}}内容详情{{end}}
+
+ {{if .richtext.Subtitle}}
+
{{.richtext.Subtitle}}
+ {{end}}
+
+
+
+
+
+
+
+
+ {{if .richtext.Title}}{{.richtext.Title}}{{else}}内容详情{{end}}
+
+
+
+
+
+ {{if .richtext.Found}}
+
+ {{.richtext.ContentHTML}}
+
+ {{if eq .richtextKey "关于我们"}}
+
+ {{end}}
+ {{else}} {{template "empty" .}} {{end}}
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/robots.txt b/app/templates/page/robots.txt
new file mode 100644
index 0000000..41fd749
--- /dev/null
+++ b/app/templates/page/robots.txt
@@ -0,0 +1,17 @@
+User-agent: *
+Allow: /
+
+# 非 HTML 接口与工具路由不抓取
+Disallow: /api/
+Disallow: /ui/
+Disallow: /healthz
+
+# 媒体代理与解密 key
+Disallow: /h5/
+Disallow: /enkey
+Disallow: /newenkey
+
+# 登录、搜索和账户 HTML 页面允许抓取,以便爬虫读取页面内 noindex。
+
+# 线上以动态 /robots.txt 为准(含绝对 Sitemap 地址与 Host)
+Sitemap: /sitemap.xml
diff --git a/app/templates/page/search.html b/app/templates/page/search.html
new file mode 100644
index 0000000..49910bf
--- /dev/null
+++ b/app/templates/page/search.html
@@ -0,0 +1,172 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} -
+ 黄果短剧{{else}}搜索短剧 - 黄果短剧{{end}}
+
+
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
+ 首页
+ /
+ {{if .query}}
+ 全站搜索
+ /
+ {{end}}
+ {{if .query}}{{.query}}{{else}}全站搜索{{end}}
+
+
+
+ {{if .query}}关于“{{.query}}”的搜索结果{{else}}全站搜索{{end}}
+
+
+ {{if not .query}}
+
+ {{end}}
+
+
+
+
+ 为您找到关于“{{if .query}}{{.query}}{{else}}关键词{{end}} ”的短剧
+
+
+ 为你整理与“{{if .query}}{{.query}}{{else}}关键词{{end}}”相关的视频内容,汇集站内相关作品、热门内容及最新更新。通过搜索结果可以快速浏览与“{{if .query}}{{.query}}{{else}}关键词{{end}}”相关的视频和作品,发现更多符合兴趣的内容。
+
+
+ 共 {{if .totalCount}}{{.totalCount}}{{else}}0{{end}} 个结果
+
+
+
+
+
+ {{template "pagination" .}}
+
+
+
+ {{template "footer" .}}
+
+
+
+
diff --git a/app/templates/page/series.html b/app/templates/page/series.html
new file mode 100644
index 0000000..6f1c3eb
--- /dev/null
+++ b/app/templates/page/series.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else}}AI成人剧集在线观看 - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+{{template "header" .}}
+
+
+
+ 首页 / AI成人剧集
+
+
AI成人剧集在线观看
+
+ {{range .filters}}
+
+ {{.Label}}
+
+ {{end}}
+
+
{{.sectionTitle}}
+
+ {{template "pagination" .}}
+ {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "series_nine_grid" "SlotName" "剧集横幅广告" "Label" "广告"}}
+ {{if .appConfig.BottomBanner}}
+
+ {{range $ad := .appConfig.BottomBanner}}
+
+
+
+ {{end}}
+
+ {{end}}
+
+
+{{template "footer" .}}
+
+
diff --git a/app/templates/page/setting.html b/app/templates/page/setting.html
new file mode 100644
index 0000000..9d16615
--- /dev/null
+++ b/app/templates/page/setting.html
@@ -0,0 +1,199 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 账号设置 - 黄果短剧
+
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
+ 个人中心
+ /
+ 账号设置
+
+
+
+
+
+
+
+
+ 账号设置
+
+
+
+
+
+ 个人资料
+
+ {{if .setting.Avatar}}
+
+ {{else}}
+
+ {{end}}
+
+
{{.setting.DisplayName}}
+
+
+
+ 昵称
+
+
+
+
个人简介
+
+ {{if
+ .setting.Introduction}}{{.setting.Introduction}}{{else}}未设置简介{{end}}
+
+
+ 保存修改
+
+
+
+
+
+
+ 账号安全
+
+ 账号安全
+
+
+
+
+
+ 退出登录
+
+
+
+
+ {{template "footer" .}}
+
+
+
diff --git a/app/templates/page/tagList.html b/app/templates/page/tagList.html
new file mode 100644
index 0000000..5b75db3
--- /dev/null
+++ b/app/templates/page/tagList.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}标签短剧 - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+{{template "header" .}}
+
+
+
+ 首页
+ /
+ {{if .pageName}}{{.pageName}}{{else if .pageTitle}}{{.pageTitle}}{{else}}标签{{end}}
+
+
+
“{{if .pageTitle}}{{.pageTitle}}{{else}}标签{{end}}”相关短剧
+
{{if .tagDescription}}{{.tagDescription}}{{else}}精选标签题材相关AI成人短剧与AI成人漫剧,持续更新标签题材作品,方便快速浏览相关短剧。{{end}}
+
+
+ {{template "pagination" .}}
+
+
+{{template "footer" .}}
+
+
diff --git a/app/templates/page/themeIndex.html b/app/templates/page/themeIndex.html
new file mode 100644
index 0000000..172beb7
--- /dev/null
+++ b/app/templates/page/themeIndex.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}AI成人短剧专题推荐 - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}{{end}}
+
+
+
+
+ {{template "header" .}}
+
+
+ 首页
+ /
+ 专题
+
+
+
+ AI成人短剧专题推荐
+ 专题集中展示黄果短剧各类精品专题合集,涵盖AI成人短剧、AI色情短剧、擦边短剧、AI换脸、AI魔改等热门主题。内容丰富多样,每日持续更新,方便你按主题快速找到想看的高清短剧,随时畅享极致观看体验。
+
+
+ 精选AI成人短剧专题
+
+
+
+ {{template "pagination" .}}
+
+ {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "topic_nine_grid" "SlotName" "专题横幅广告" "Label" "广告"}}
+
+ {{if .appConfig.BottomBanner}}
+
+ {{range $ad := .appConfig.BottomBanner}}
+
+
+
+ {{end}}
+
+ {{end}}
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/todayUpdate.html b/app/templates/page/todayUpdate.html
new file mode 100644
index 0000000..d4547a5
--- /dev/null
+++ b/app/templates/page/todayUpdate.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - 黄果短剧{{else}}今日更新 - 黄果短剧{{end}}
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+
+
+
+{{template "header" .}}
+
+
+
+
LATEST
+ {{$navTitle := "最近更新"}}
+ {{range .headerNav}}{{if eq .Key "todayUpdate"}}{{$navTitle = .Name}}{{end}}{{end}}
+
{{$navTitle}}
+
按剧集最近更新时间排序,快速查看各频道新内容
+
+
+ {{template "pagination" .}}
+
+
+{{template "footer" .}}
+
+
+
+
diff --git a/app/templates/page/userAgreement.html b/app/templates/page/userAgreement.html
new file mode 100644
index 0000000..c6c6271
--- /dev/null
+++ b/app/templates/page/userAgreement.html
@@ -0,0 +1,137 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 用户协议 - 黄果短剧
+
+
+
+
+
+ {{if .jsonLD}}
+
+ {{end}}
+
+
+
+ {{template "header" .}}
+
+
+
+ 首页
+ /
+ 用户协议
+
+
+
用户协议
+
使用黄果短剧服务前,请仔细阅读以下条款
+
+
+
+
+
+
+
+ 用户协议
+
+
+
+
+
+ 使用协议
+ 使用条款 欢迎您光临【黄果短剧】(以下简称本网站),本网站由「黄果短剧」(以下简称黄果短剧)经营。
+本网站重视每一位使用者所享有的服务,特此说明本网站的使用政策,以保障您的权益。
+请您仔细阅读本使用条款的内容:
+
+关于《使用条款》
+在您决定使用本网站所提供的服务(以下简称本服务)前,请仔细阅读本使用条款。您必须在完全同意以下条款的前提下,才能使用本服务。本使用条款与其他附加条件或其他特殊条款相矛盾时,以附加条件或特殊条款为准。本网站有权在任何时间修改或变更本使用条款的内容,您应经常查看以了解您当前的权利及义务。若您在本网站对本使用条款进行任何修改或变更后仍继续使用本服务,视为您已同意接受本使用条款的修改及变更。
+您确认您在访问本网站的司法管辖区内的年龄至少为18岁,或者确认您已经达到所在地法律要求能够查看成人内容的年龄,并且完全有能力履行及遵守本使用条款。如果您未满18岁,您应立即停止使用本服务。您并同意并表示您访问本服务的管辖区不禁止接收或查看色情内容。
+
+服务内容
+本服务的具体内容由本网站根据实际情况提供,并对其所提供之服务拥有最终解释权。
+本服务系通过互联网向您提供各项网络信息服务,您必须自行配备所需的一切设备,并承担所需费用。
+本网站对用户的联系将以电子邮件、网页的消息揭示及其他本网站判断为适宜的方式进行。通知以本网站发出之时视为用户已收到该通知。
+本网站对用户自身在各服务内的账号及密码,用户需自行承担管理责任,并知悉本网站仅提供各项网络信息服务。第三方不正当的使用等情况本网站概不负责。
+本网站及各服务的用户资格,或基于用户资格使用本服务的权利及义务等,不得通过任何方式使第三方以任何方式使用或进行其他处分。
+若您的行为违反本使用条款之禁止规定,本网站得不经事先通知限制或者解除您使用本网站提供服务的资格。因前项措施造成用户损害的情况本网站概不负责,本网站对您违反服务条款造成的一切损害、损失、费用(含诉讼费用及律师费用)均可提出赔偿要求。
+
+我们保留删除任何达到一定期间不活跃的账户的权利。已绑定账号而连续一年内无登录记录的账户视为放弃该账户之权利,我们将进行封存处理;未绑定账号(即访客)而连续半年内无登录记录的账户视为放弃该账户之权利。用户可申请账号复原,但我方不保证该账户的完整性或恢复的可能性。
+
+授权
+在本使用条款的约束下,本网站仅此授予您有限、仅供个人使用、非商业用途、非专属且不可转让的使用本服务之权利。
+
+使用者的行为 您在使用本网站服务过程中,必须遵循以下原则:
+(a) 遵守本网站适用之有关的法律和法规;
+(b) 遵守所有与网络服务有关的网络协议、规定和程序;
+(c) 不得为任何非法目的而使用网络服务系统;
+(d) 不得使用任何设备、软件或程序,干扰或试图干扰本网站、本网站之正常运作;
+(e) 不允许侵入、强行入侵、接通、使用或企图侵入、接通、使用本网站服务器及未经本网站对您发出许可的任何资料区;
+(f) 不得限制、制止其他使用者使用并享用本服务;
+(g) 如发现任何非法使用账号或任何出现安全漏洞的情况,应立即告知本网站;
+(h) 违反公序良俗或法令的行为;
+(i) 违反儿童色情、色情、猥亵物等相关法令的行为;
+(j) 将虚假情报在本服务登录的行为;
+(k) 违反本网站其他或个别条款的行为;
+(l) 对本网站或本服务有隐匿、欺诈、延迟给付或不为给付财产等行为;
+(m) 其他本网站判断为不适当的行为。
+
+知识产权
+本网站提供的服务内容(包括但不限于:文字、照片、图形、图像、图表、声音、动画、视频、音频、商标等),均由本网站或其他相关权利人依法拥有,并受商业形象、著作权法、商标法、公平交易法以及各类知识产权法之保护。非经本网站或其他相关权利人之事先书面同意,本网站及其内容的任何部分,不得修改、复制、重制、重印、公开播送、改作、散布、发行、公开发表及为任何非法目的使用。
+
+广告信息
+本网站上如有传播或刊载之广告,关于其内容所展现的文字、图片、动画、声音、影像或以其他方式对商品或服务的说明,均由广告主或广告代理商提供。您应自行判断该广告的正确性及可信度,本网站不对该等广告作任何担保。
+
+链接
+本网站或所有服务相关网页上所提供的链接,可能链接到其他个人、公司或组织之网站。提供该等链接之目的仅为便利使用者自行搜集或取得信息,本网站对于被链接网站所提供之产品、服务或信息不作担保。
+
+违约赔偿
+您同意保障和维护本网站及任何其他第三方的利益,如因违反有关法律、法规或本使用条款项下的任何条款而致使本网站或任何其他第三方造成损失,您同意承担所造成损害之赔偿责任。
+
+拒绝提供担保
+本网站对本服务不提供任何明示或暗示的担保,包含但不限于商业适售性、特定目的之适用性及未侵害他人权利。
+
+本网站不担保以下事项:
+(a) 本服务将符合您的需求;
+(b) 本服务不受干扰、即时提供、安全可靠或从不出错;
+(c) 经由本服务之使用而取得任何资料之合适性、有效性、正确性、完整性、安全性、可靠性、即时性及是否侵害他人权利。
+
+免责声明
+在任何情况下,本网站对于您或任何其他第三方因使用或不能使用该等服务所发生的任何直接、间接、附带、特别、惩罚性或间接性的损失或损害赔偿,不承担任何责任。
+
+本网站于下列情形之一时,得暂停或中断本服务之部分或全部服务,且对使用者任何直接或间接之损害,不承担任何责任:
+(a) 对于本网站相关系统设备进行迁移、更换或维护时;
+(b) 任何不可归责于本网站之事由所造成之服务停止或中断;
+(c) 天灾或其他不可抗力造成本网站之服务停止或中断;
+(d) 使用者有任何违反政府法令或本使用条款情事。
+
+服务变更、中断或终止
+鉴于网络服务的特殊性,您同意本网站有权随时变更、中断或终止部分或全部的网络服务。
+
+法律管辖
+本使用条款的订立、执行和解释及争议的解决均应适用于本网站所指定法律,并以本网站所指定法院作为第一审管辖法院。
+
+其他规定
+本使用条款构成双方对本服务及其他有关事宜的完整协议,并取代双方先前就本使用条款所为之任何形式的承诺或协议。如本使用条款中的任何条款无论因何种原因完全或部分无效或不具有执行力,本使用条款的其余条款仍应有效并且有约束力。本使用条款的标题仅为方便阅读所设,非对条款的定义、限制、解释或描述其范围或界限。
+
+联络方式
+若您对于本使用条款或本网站有任何问题或建议,欢迎来信至我们的服务信箱。本使用条款之订立、修改、更新及最终解释权均属本网站所有。
+
+
+
+ {{template "footer" .}}
+
+
diff --git a/app/templates/page/userCenter.html b/app/templates/page/userCenter.html
new file mode 100644
index 0000000..c00af1e
--- /dev/null
+++ b/app/templates/page/userCenter.html
@@ -0,0 +1,318 @@
+
+
+
+
+
+ {{template "seoCommon" .}}
+ 个人中心 - 黄果短剧
+
+
+
+
+
+
+
+
+
+ {{template "header" .}}
+
+
+
+
ACCOUNT
+
+
+
+ 个人中心
+
+ {{if and .isLoggedIn (not .isRegistered)}}
+
游客注册
+ {{end}}
+
+
+
+ {{if .isLoggedIn}}
+
欢迎回来,继续你的追剧旅程
+ {{else}}
+
管理观看数据、收藏和金币余额
+ {{end}}
+
+
+ {{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "user_center_detail_top" "SlotName" "个人中心方块广告" "Label" "推广应用"}}
+
+ {{if .isLoggedIn}}
+
+ {{else}}
+
+ {{end}}
+
+
+
+
+
+ {{if .isLoggedIn}}
+
+ {{if .avatar}}
+
+ {{else}}
+
+ {{end}}
+
+
+ {{if .userName}}{{.userName}}{{else}}用户{{end}}
+
+
+ {{if .userId}}ID {{.userId}}{{else}}ID --{{end}}
+
+
+ {{if not .isRegistered}}
+
游客注册
+ {{end}}
+
+
+
+
+
+
金币余额 (金币)
+
{{if .rechargeText}}{{.rechargeText}}{{else}}000.00{{end}}
+
+ 金币充值
+
+
+ {{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "user_center_detail_top" "SlotName" "个人中心方块广告" "Label" "推广应用"}}
+
+
+
+ {{else}}
+
+
+
+
登录
+
+
+
+
+
+ 金币充值
+
+
+ {{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "user_center_detail_top" "SlotName" "个人中心方块广告" "Label" "推广应用"}}
+
+
+
+ {{end}}
+
+
+ {{template "footer" .}}
+
+
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c303874
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,91 @@
+module videoweb
+
+go 1.24.0
+
+toolchain go1.24.3
+
+require (
+ github.com/aws/aws-sdk-go v1.34.28
+ github.com/ddliu/go-httpclient v0.6.9
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
+ github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3
+ github.com/elastic/go-elasticsearch/v7 v7.17.10
+ github.com/gin-gonic/gin v1.10.1
+ github.com/go-playground/validator/v10 v10.27.0
+ github.com/go-redis/redis v6.15.9+incompatible
+ github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
+ github.com/gorilla/sessions v1.4.0
+ github.com/grafov/m3u8 v0.11.1
+ github.com/karrick/godirwalk v1.10.3
+ github.com/mitchellh/mapstructure v1.4.1
+ github.com/nyaruka/phonenumbers v1.0.68
+ github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/robfig/cron/v3 v3.0.1
+ github.com/satori/go.uuid v1.2.0
+ github.com/shopspring/decimal v1.2.0
+ github.com/swaggo/files v1.0.1
+ github.com/swaggo/gin-swagger v1.6.0
+ github.com/xxjwxc/gowp v0.0.0-20200603141413-57c3ba7108be
+ go.mongodb.org/mongo-driver v1.17.3
+ go.uber.org/zap v1.16.0
+ golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
+ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
+ gopkg.in/yaml.v2 v2.4.0
+)
+
+require (
+ github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/PuerkitoBio/purell v1.1.1 // indirect
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/bytedance/sonic v1.14.0 // indirect
+ github.com/bytedance/sonic/loader v0.3.0 // indirect
+ github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
+ github.com/go-openapi/spec v0.20.4 // indirect
+ github.com/go-openapi/swag v0.19.15 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/golang/snappy v1.0.0 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mailru/easyjson v0.7.6 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/montanaflynn/stats v0.7.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/rogpeppe/go-internal v1.13.1 // indirect
+ github.com/swaggo/swag v1.16.1 // indirect
+ github.com/technoweenie/multipartstreamer v1.0.1 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/xxjwxc/public v0.0.0-20200603141144-4001846f9957 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+ go.uber.org/atomic v1.6.0 // indirect
+ go.uber.org/multierr v1.5.0 // indirect
+ golang.org/x/arch v0.20.0 // indirect
+ golang.org/x/crypto v0.44.0 // indirect
+ golang.org/x/net v0.47.0 // indirect
+ golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
+ golang.org/x/tools v0.38.0 // indirect
+ google.golang.org/protobuf v1.36.7 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/eapache/queue.v1 v1.1.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..293c6de
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,442 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/ant0ine/go-json-rest v3.3.2+incompatible/go.mod h1:q6aCt0GfU6LhpBsnZ/2U+mwe+0XB5WStbmwyoPfc+sk=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk=
+github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
+github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
+github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
+github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/ddliu/go-httpclient v0.6.9 h1:/3hsBVpcgCJwqm1dkVlnAJ9NWuYInbRc+i9FyUXX/LE=
+github.com/ddliu/go-httpclient v0.6.9/go.mod h1:zM9P0OxV4OGGz1pt/ibuj0ooX2SWH9a6MvXZLbT0JMc=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc=
+github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
+github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
+github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
+github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
+github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jander/golog v0.0.0-20150917071935-954a5be801fc/go.mod h1:uWhWXOR4dpfk9J8fegnMY7sP2GFXxe3PFI9Ps+TRXJs=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kardianos/service v1.0.0/go.mod h1:8CzDhVuCuugtsHyZoTvsOBuvonN/UDBvl0kH+BUxvbo=
+github.com/karrick/godirwalk v1.10.3 h1:lOpSw2vJP0y5eLBW906QwKsUK/fe/QDyoqM5rnnuPDY=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/muesli/cache2go v0.0.0-20200423001931-a100c5aac93f/go.mod h1:414R+qZrt4f9S2TO/s6YVQMNAXR2KdwqQ7pW+O4oYzU=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
+github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
+github.com/nyaruka/phonenumbers v1.0.68 h1:HM+zMsS0iOwREnRKieB+RmK3Sgthwf1Kftgi3GxIp7U=
+github.com/nyaruka/phonenumbers v1.0.68/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olivere/elastic v6.2.31+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
+github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
+github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
+github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
+github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
+github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
+github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xxjwxc/gowp v0.0.0-20200603130651-4d7368b0e285/go.mod h1:yJ/fY5BorWARfDDsxBU/MyQTHc5MVyNcqBQQYD6MN0k=
+github.com/xxjwxc/gowp v0.0.0-20200603141413-57c3ba7108be h1:v4Ws2Pd0HNxegMWiZTgaSBfeLtfyFP/eWc50o2CFFX8=
+github.com/xxjwxc/gowp v0.0.0-20200603141413-57c3ba7108be/go.mod h1:mstQ3GY2ZYdP9fCUJJmsq5hwhP0LKoR4ZVTq0XaxK0U=
+github.com/xxjwxc/public v0.0.0-20200603115833-341beff27850/go.mod h1:fp3M+FEQrCgWD1fZ/PLwZkCTglf086OEhC9LcydAUnc=
+github.com/xxjwxc/public v0.0.0-20200603141144-4001846f9957 h1:1JDmDFVQFpty205Kh9lxBAjjn9rG1eTYtdF/Q0dTh5w=
+github.com/xxjwxc/public v0.0.0-20200603141144-4001846f9957/go.mod h1:0BFWVHqt7nKW8MtIx7R7bOkoGQFFnKsaJeeVbkzY88E=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
+go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
+go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
+golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
+golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
+golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
+golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
+google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/eapache/queue.v1 v1.1.0 h1:EldqoJEGtXYiVCMRo2C9mePO2UUGnYn2+qLmlQSqPdc=
+gopkg.in/eapache/queue.v1 v1.1.0/go.mod h1:wNtmx1/O7kZSR9zNT1TTOJ7GLpm3Vn7srzlfylFbQwU=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/go-with/wxpay.v1 v1.3.0/go.mod h1:12lWy92n19pAUSSE3BrOiEZbWRkl+9tneOd/aU/LU6g=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
diff --git a/make.sh b/make.sh
new file mode 100755
index 0000000..9d2ef5a
--- /dev/null
+++ b/make.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+export GOPATH=$(cd $(dirname $0)/../;pwd)
+export PATH=${PATH}:${GOPATH}/bin
+
+curDir=$(cd $(dirname $0);pwd)
+cd ${curDir}
+
+which swag
+if [ $? -ne 0 ]; then
+ echo "未发现swag,拉取最新swag"
+ go get github.com/swaggo/swag/cmd/swag
+fi
+
+prepare=(
+ "github.com/swaggo/gin-swagger"
+ "github.com/swaggo/gin-swagger/swaggerFiles"
+)
+for goget in ${prepare[@]};do
+ go get ${goget}
+ if [ $? -ne 0 ]; then
+ echo "拉取${goget}失败,终止构建"
+ exit 1
+ fi
+done
+
+echo "package main;func main() {}" > main.go
+
+swag init --exclude web --output app/docs
+if [ $? -ne 0 ]; then
+ echo "执行swag init失败,终止构建"
+ rm main.go
+ exit 1
+fi
+
+swag init --exclude app --output web/docs
+if [ $? -ne 0 ]; then
+ echo "执行swag init失败,终止构建"
+ rm main.go
+ exit 1
+fi
+
+rm main.go
+
diff --git a/project.md b/project.md
new file mode 100644
index 0000000..055baf2
--- /dev/null
+++ b/project.md
@@ -0,0 +1,83 @@
+---
+项目名称: 91ly
+一级部门: 北斗
+二级部门: 瑞升
+
+DMP编码:
+
+项目类型: PC
+ # 可选值:
+# H5 / APP / PC / 服务 / 组件 / 工具 / AI / 中间件
+
+开发语言: GO
+ # 可选值:
+# GO / Java / PHP / Node
+
+项目负责人: DN1425 # 员工编号, 多个用逗号隔开,这里的负责是这套代码的负责人
+
+运维负责人: DN9460, DN9651, DN4792 # 负责部署、监控、K8S、发布、证书等
+
+产品负责人: YY1166 # 项目产品经理
+
+
+项目成员: DN7208 DN2185
+
+项目状态: 维护中
+ # 可选值:
+ # 开发中 / 维护中 / 已停止 / 已归档
+
+项目级别: 中
+ # 可选值:
+ # 高 / 中 / 低 , 北斗体系 s和a为 高 b为中 c d为低
+
+核心分支: main
+ # 即部署时候使用的分支,可选值:
+ # master / main / dev / release
+
+代码仓库:
+ - ssh://git@bd-git-sh.jsyyds.com:8082/ruisheng/jha-1021-91jhs/frontend/jha-1043-hgdj-web.git
+
+最后审计人:
+
+最后审计时间:
+
+告警群:
+
+备注:
+
+---
+
+# 1. 项目说明
+
+- hgdj_server
+- app 包含前端API及页面
+- web 包含管理后端API
+
+
+# 2. 基础设施与中间件
+
+- MongoDB 主从
+- Redis 单节点
+- Elasticsearch 搜索(v7)
+
+
+# 3. 运维部署
+
+- Shell脚本部署(scp + supervisorctl)
+- Telegram Bot 部署通知
+
+
+# 4. 风险说明
+
+- Redis 热Key风险
+
+
+# 5. 历史事故(可选)
+
+-
+
+
+# 6. 交接说明(可选)
+
+- appid 代码固定:1043
+-
diff --git a/pub/common/comm/comm.go b/pub/common/comm/comm.go
new file mode 100644
index 0000000..4f87259
--- /dev/null
+++ b/pub/common/comm/comm.go
@@ -0,0 +1,138 @@
+package comm
+
+import (
+ "encoding/json"
+ "sync"
+ "time"
+ "videoweb/pub/common/godef"
+ "videoweb/pub/common/lock"
+ "videoweb/pub/common/log"
+
+ "github.com/go-redis/redis"
+ "go.uber.org/zap"
+)
+
+type MessageType int64
+
+const (
+ MessageTypeError MessageType = iota // 心跳消息
+ MessageTypeAlive
+ MessageTypeMediaAdd // 添加一个媒体
+ MessageTypeMediaDel // 删除一个媒体
+ MessageTypePostAdd // 添加一个帖子
+ MessageTypePostDel // 删除一个帖子
+)
+
+type message struct {
+ Type MessageType
+ Payload []byte
+}
+
+type OnReceive func(data []byte)
+
+type Communicate struct {
+ sendChannel string // 发送消息的频道
+ recvChannel string // 接收消息的频道
+ client *redis.Client // Redis的客户端
+ lock sync.RWMutex
+ callbacks map[MessageType][]OnReceive
+ publish *lock.Queue[*message]
+}
+
+func NewCommunicate(client *redis.Client, sendChannel string, recvChannel string) *Communicate {
+ return &Communicate{
+ sendChannel: sendChannel,
+ recvChannel: recvChannel,
+ client: client,
+ callbacks: make(map[MessageType][]OnReceive),
+ publish: lock.NewQueue[*message](),
+ }
+}
+
+func (c *Communicate) RegisterReceiver(mType MessageType, receive OnReceive) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ c.callbacks[mType] = append(c.callbacks[mType], receive)
+}
+
+func (c *Communicate) findEvent(mType MessageType) []OnReceive {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ return c.callbacks[mType]
+}
+
+func (c *Communicate) dispatchMessage(mType MessageType, payload []byte) {
+ events := c.findEvent(mType)
+ for _, event := range events {
+ event(payload)
+ }
+}
+
+func (c *Communicate) handleRecv(data *redis.Message) {
+ if data.Channel == "error" {
+ log.Error("recv from channel %s failed", zap.Any("error", data.Payload))
+ return
+ }
+ if data.Channel == c.recvChannel {
+ msg := &message{}
+ err := json.Unmarshal([]byte(data.Payload), msg)
+ if err != nil {
+ log.Error("recv from channel %s failed", zap.Any("error", err))
+ }
+ c.dispatchMessage(msg.Type, msg.Payload)
+ }
+}
+
+func (c *Communicate) Send(mType MessageType, payload []byte) error {
+ msg := &message{
+ Type: mType,
+ Payload: payload,
+ }
+ c.publish.Enqueue(msg)
+ return nil
+}
+
+func (c *Communicate) Start() {
+ // 启动接收线程
+ godef.Go(func() {
+ sub := c.client.Subscribe(c.recvChannel)
+ defer sub.Close()
+ // 循环接收消息
+ for {
+ msg, err := sub.ReceiveMessage()
+ if err != nil {
+ msg = &redis.Message{
+ Channel: "error",
+ Payload: err.Error(),
+ }
+ c.handleRecv(msg)
+ time.Sleep(time.Second)
+ } else {
+ c.handleRecv(msg)
+ }
+ }
+ })
+ // 启动发送线程
+ godef.Go(func() {
+ for {
+ time.Sleep(time.Second)
+ for !c.publish.IsEmpty() {
+ msg, ok := c.publish.Dequeue()
+ if !ok {
+ break
+ }
+ data, err := json.Marshal(msg)
+ if err != nil {
+ log.Error("Communicate msg marshal failed", zap.Any("error", err))
+ break
+ }
+ err = c.client.Publish(c.sendChannel, data).Err()
+ if err != nil {
+ c.publish.Enqueue(msg) // 再写回队列
+ log.Error("Communicate send msg failed", zap.Any("error", err))
+ break
+ }
+ }
+ }
+ })
+}
diff --git a/pub/common/common.go b/pub/common/common.go
new file mode 100644
index 0000000..f95f6a0
--- /dev/null
+++ b/pub/common/common.go
@@ -0,0 +1,131 @@
+package common
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/useragent"
+
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+var (
+ cryptSecret string
+ env consts.RunMod
+ //routineCount int64
+)
+
+// InitGinSecret InitResponseParam
+func InitGinSecret(secret string, env_ consts.RunMod) {
+ cryptSecret = secret
+ env = env_
+}
+
+// ServerJSON 返回数据并处理多语言
+func ServerJSON(c *gin.Context, codeOrErr srvdef.ErrInfoer, data interface{}) {
+ code := codeOrErr.GetCode()
+ tip := codeOrErr.GetTip()
+ msg := codeOrErr.GetMsg()
+ var hash bool
+ if code != srvdef.Success { //发生错误,记录日志
+ var version, sysType, devType string
+ id := GetUserId(c)
+ ua, _ := GetUA(c)
+ if ua.Ver != "" {
+ version = ua.Ver
+ }
+ if ua.SysType != "" {
+ sysType = ua.SysType
+ }
+ if devType != "" {
+ devType = ua.DevType
+ }
+ log.Warn("Error:",
+ log.Any("UID", strconv.FormatInt(id, 10)),
+ log.Any("IP", c.ClientIP()),
+ log.Any("Version", version),
+ log.Any(consts.CtxSysType, sysType),
+ log.Any("DevType", devType),
+ log.Any("Router", c.Request.RequestURI),
+ log.Any("PID", os.Getpid()),
+ log.Any("Error", data),
+ log.Any("Code", code))
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "code": code,
+ "hash": hash,
+ "msg": msg,
+ "tip": tip,
+ "data": data,
+ "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
+ })
+}
+
+// GetUID 获取用户uid
+// oauth.Auth执行后uid有效
+func GetUID(ctx *gin.Context) (uid uint64, err error) {
+ val, exists := ctx.Get(consts.CtxUserID)
+ if !exists {
+ err = errors.New("userId not exist")
+ return
+ }
+ uid, ok := val.(uint64)
+ if !ok {
+ return 0, errors.New("userId type error")
+ }
+ return
+}
+
+func GetUserId(ctx *gin.Context) int64 {
+ return ctx.GetInt64(consts.CtxUserID)
+}
+
+// GetUA
+func GetUA(ctx *gin.Context) (u useragent.UA, err error) {
+ val, exists := ctx.Get(consts.CtxUA)
+ if !exists {
+ err = errors.New("user-agent not exists")
+ return
+ }
+ u, ok := val.(useragent.UA)
+ if !ok {
+ err = errors.New("user-agent type error")
+ return
+ }
+ return u, nil
+}
+
+// GetIP 获取真实IP
+func GetIP(ctx *gin.Context) string {
+ relIP, exists := ctx.Get(consts.CtxIP)
+ if !exists {
+ if relIP == "" {
+ relIP = ctx.ClientIP()
+ }
+ }
+ v, ok := relIP.(string)
+ if !ok {
+ return ""
+ }
+ log.Info(fmt.Sprintf("[IP-ROUTER] %s,PID %d", ctx.GetHeader("X-Forwarded-For"), os.Getpid()))
+ return v
+}
+
+func IsNilOrEmpty(in interface{}) bool {
+ if in == nil || in == "" {
+ return true
+ }
+ return false
+}
+
+func AlphabeticColl() []string {
+ return []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
+}
\ No newline at end of file
diff --git a/pub/common/convUtil.go b/pub/common/convUtil.go
new file mode 100644
index 0000000..0a99f82
--- /dev/null
+++ b/pub/common/convUtil.go
@@ -0,0 +1,135 @@
+package common
+
+import (
+ "encoding/json"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ID = primitive.ObjectID
+
+// ToJsonM 将struct转换为 Json Map
+func ToJsonM(obj interface{}) (map[string]interface{}, error) {
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+func ToBsonM(s interface{}) (bson.M, error) {
+ data, err := bson.Marshal(s)
+ if err != nil {
+ return nil, err
+ }
+ m := make(bson.M)
+ if err := bson.Unmarshal(data, &m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func MapToJsonString(m map[string]string) (string, error) {
+ if data, err := json.Marshal(m); err != nil {
+ return "", err
+ } else {
+ return string(data), nil
+ }
+}
+
+// MapToStruct s is struct ptr
+func MapToStruct(s interface{}, m map[string]interface{}) error {
+ bsonBytes, err := bson.Marshal(m)
+ if err != nil {
+ return err
+ }
+ return bson.Unmarshal(bsonBytes, s)
+}
+
+// JSONStruct2Map 将struct转换为Map
+func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+// Map2JSONStruct 将map转换为Json struct
+func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
+ j, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(j, v); err != nil {
+ return err
+ }
+ return err
+}
+
+func IDArray(ids []string) ([]ID, error) {
+ var idArray []ID
+ for _, id := range ids {
+ _id, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ return nil, err
+ }
+ idArray = append(idArray, _id)
+ }
+
+ return idArray, nil
+}
+
+func ObjectIDs2String(obj []primitive.ObjectID) []string {
+ arr := []string{}
+ if len(obj) == 0 {
+ return arr
+ }
+ for _, o := range obj {
+ arr = append(arr, o.Hex())
+ }
+ return arr
+}
+
+func String2ObjectID(obj []string) []primitive.ObjectID {
+ arr := []primitive.ObjectID{}
+ for _, o := range obj {
+ oid, err := primitive.ObjectIDFromHex(o)
+ if err != nil {
+ continue
+ }
+ arr = append(arr, oid)
+ }
+ return arr
+}
+
+// PercentOf - calculate what percent [number1] is of [number2].
+// ex. 300 is 12.5% of 2400
+func PercentOf(part int, total int) float64 {
+ return (float64(part) * float64(100)) / float64(total)
+}
+
+func MergeMap(src, dest map[string]interface{}) map[string]interface{} {
+ if len(src) == 0 {
+ return dest
+ }
+ if len(dest) == 0 {
+ return src
+ }
+ if len(src) == 0 && len(dest) == 0 {
+ return make(map[string]interface{})
+ }
+ for k, v := range dest {
+ src[k] = v
+ }
+ return src
+}
diff --git a/pub/common/cronmgr/crommgr.go b/pub/common/cronmgr/crommgr.go
new file mode 100644
index 0000000..2494c63
--- /dev/null
+++ b/pub/common/cronmgr/crommgr.go
@@ -0,0 +1,119 @@
+package cronmgr
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "videoweb/pub/common/log"
+
+ "github.com/robfig/cron/v3"
+)
+
+var c *cron.Cron
+
+// runIngs 正在运行中的任务
+var runIngs map[*context.Context]struct{}
+var r_lock sync.Mutex
+
+// RegisterCron 注册一个定时任务
+func RegisterCron(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
+ if len(spec) == 0 || cmd == nil {
+ return -1, errors.New("spec or cmd is nil")
+ }
+ return registerCron(spec, func() {
+ defer func() {
+ if err := recover(); err != nil {
+ if errfunc != nil {
+ errfunc(err)
+ }
+ log.Errorf("cron task error.%v", err)
+ }
+ }()
+ cmd()
+ })
+}
+
+// RegisterCronAsyncOne 注册一个协程执行的定时任务 限制了cmd同时只有一个协程在执行
+func RegisterCronAsyncOne(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
+ if len(spec) == 0 || cmd == nil {
+ return -1, errors.New("spec or cmd is nil")
+ }
+ ctx := context.Background()
+ return registerCron(spec, func() {
+ go func(c *context.Context) {
+ defer func() {
+ if err := recover(); err != nil {
+ if errfunc != nil {
+ errfunc(err)
+ }
+ log.Errorf("cron task error.%v", err)
+ }
+ }()
+ if taskRunIng(c) {
+ cmd()
+ taskRunEnd(c)
+ } else {
+ log.Warnf("Limits cmd to only one coroutine executing at the same time")
+ }
+ }(&ctx)
+ })
+}
+
+// RegisterCronAsync 注册一个协程执行的定时任务
+func RegisterCronAsync(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
+ if len(spec) == 0 || cmd == nil {
+ return -1, errors.New("spec or cmd is nil")
+ }
+ return registerCron(spec, func() {
+ go func() {
+ defer func() {
+ if err := recover(); err != nil {
+ if errfunc != nil {
+ errfunc(err)
+ }
+ log.Errorf("cron task error.%v", err)
+ }
+ }()
+ cmd()
+ }()
+ })
+}
+func registerCron(spec string, cmd func()) (cron.EntryID, error) {
+ if c == nil {
+ c = cron.New(cron.WithSeconds())
+ runIngs = make(map[*context.Context]struct{})
+ }
+ id, err := c.AddFunc(spec, cmd)
+ if err != nil {
+ log.Warnf("add cron [%s] fail", spec)
+ return -1, err
+ }
+ c.Start()
+ log.Infof("add cron [%d] spec:[%s].", id, spec)
+ return id, nil
+}
+
+// UnregisterCron 注销一个定时任务
+func UnregisterCron(id cron.EntryID) bool {
+ if id > 0 {
+ c.Remove(id)
+ log.Infof("remove cron id:[%d]", id)
+ }
+ log.Infof("remove cron [%d] not found", id)
+ return false
+}
+
+func taskRunIng(ctx *context.Context) bool {
+ r_lock.Lock()
+ defer r_lock.Unlock()
+ if _, ok := runIngs[ctx]; ok {
+ return false
+ }
+ runIngs[ctx] = struct{}{}
+ return true
+}
+func taskRunEnd(ctx *context.Context) {
+ r_lock.Lock()
+ defer r_lock.Unlock()
+ delete(runIngs, ctx)
+}
diff --git a/pub/common/cronx/cron.go b/pub/common/cronx/cron.go
new file mode 100644
index 0000000..5228ccf
--- /dev/null
+++ b/pub/common/cronx/cron.go
@@ -0,0 +1,148 @@
+package cronx
+
+import (
+ "context"
+ "sync"
+ "time"
+ "videoweb/pub/common/log"
+
+ "github.com/robfig/cron/v3"
+ "go.uber.org/zap"
+)
+
+type CronJob interface {
+ Run() error
+ Desc() string
+ Name() string
+}
+
+type CronJobAndSpec struct {
+ CronJob CronJob
+ Spec string
+}
+
+type BaseJob struct {
+ ctx context.Context
+ spec string
+ name string
+ desc string
+ l *zap.SugaredLogger
+}
+
+type cronFunc struct {
+ *BaseJob
+ fn func(ctx context.Context, l *zap.SugaredLogger) error
+}
+
+func addError(fn func(ctx context.Context, l *zap.SugaredLogger)) func(context.Context, *zap.SugaredLogger) error {
+ return func(ctx context.Context, l *zap.SugaredLogger) error {
+ l.Info("start")
+ defer l.Info("end")
+ fn(ctx, l)
+ return nil
+ }
+}
+
+func (f cronFunc) Run() error { return f.fn(f.ctx, f.l) }
+func NewCronFunc(ctx context.Context, name, desc string, fn func(ctx context.Context, l *zap.SugaredLogger)) cronFunc {
+ return cronFunc{
+ BaseJob: NewBaseJob(ctx, name, desc),
+ fn: addError(fn),
+ }
+}
+
+func NewCronFuncWithError(ctx context.Context, name, desc string, fn func(ctx context.Context, l *zap.SugaredLogger) error) cronFunc {
+ return cronFunc{
+ BaseJob: NewBaseJob(ctx, name, desc),
+ fn: fn,
+ }
+}
+
+func NewBaseJob(ctx context.Context, name, desc string) *BaseJob {
+ return &BaseJob{
+ ctx: ctx,
+ name: name,
+ desc: desc,
+ l: log.ZapLogS.Named(name),
+ }
+}
+
+func (j *BaseJob) Name() string { return j.name }
+func (j *BaseJob) Desc() string { return j.desc }
+func (j *BaseJob) L() *zap.SugaredLogger { return j.l }
+func (j *BaseJob) Context() context.Context { return j.ctx }
+
+type CronLogger struct {
+ l *zap.SugaredLogger
+}
+
+func NewCronLogger() *CronLogger {
+ return &CronLogger{
+ l: log.ZapLogS.Named("cron"),
+ }
+}
+
+func (l *CronLogger) Info(msg string, kvs ...interface{}) {
+ l.l.Infow(msg, kvs)
+}
+func (l *CronLogger) Error(err error, msg string, kvs ...interface{}) {
+ l.l.Errorw(err.Error()+msg, kvs)
+}
+
+func StartJobs(jobs ...CronJobAndSpec) (closer func()) {
+ var once sync.Once
+ l := NewCronLogger()
+ c := cron.New(cron.WithSeconds(), cron.WithLocation(time.Local), cron.WithChain(cron.Recover(l)))
+ for i := range jobs {
+ j := jobs[i].CronJob
+ spec := jobs[i].Spec
+ name := j.Name()
+ _, err := c.AddFunc(spec, func() {
+ defer log.Infof("%s end", name)
+ log.Infof("%s start", name)
+ if err := j.Run(); err != nil {
+ log.Errorf("%s run failed: %s", name, err.Error())
+ }
+ })
+ if err != nil {
+ log.Fatalf("%s add failed: %s", err)
+ }
+ }
+ c.Start()
+ return func() {
+ once.Do(func() {
+ c.Stop()
+ })
+ }
+}
+
+type CronFuncType func(ctx context.Context, l *zap.SugaredLogger)
+
+func TraceTime(fn CronFuncType) CronFuncType {
+ return func(ctx context.Context, l *zap.SugaredLogger) {
+ now := time.Now()
+ defer func() {
+ elapse := time.Since(now)
+ if elapse > time.Second {
+ l.Infof("takes %s", elapse.Truncate(time.Millisecond))
+ } else {
+ l.Infof("takes %d ms", elapse/time.Millisecond)
+ }
+ }()
+ l.Info("start run")
+ fn(ctx, l)
+ l.Info("end run")
+ }
+}
+
+// MustSecondSpec is a helper method to validate spec at compile time
+func MustSecondSpec(spec string) string {
+ parser := cron.NewParser(
+ cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
+ )
+ _, err := parser.Parse(spec)
+ if err != nil {
+ panic(err)
+ }
+ return spec
+}
diff --git a/pub/common/crypt/aesecb.go b/pub/common/crypt/aesecb.go
new file mode 100644
index 0000000..a75f817
--- /dev/null
+++ b/pub/common/crypt/aesecb.go
@@ -0,0 +1,43 @@
+package crypt
+
+import (
+ "bytes"
+ "crypto/aes"
+)
+
+func PKCS7PaddingEx(ciphertext []byte, blockSize int) []byte {
+ padding := blockSize - len(ciphertext)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ return append(ciphertext, padtext...)
+}
+
+func PKCS7UnPaddingEx(origData []byte) []byte {
+ length := len(origData)
+ unpadding := int(origData[length-1])
+ return origData[:(length - unpadding)]
+}
+
+func EcbDecrypt(data, key []byte) []byte {
+ block, _ := aes.NewCipher(key)
+ decrypted := make([]byte, len(data))
+ size := block.BlockSize()
+
+ for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
+ block.Decrypt(decrypted[bs:be], data[bs:be])
+ }
+
+ return PKCS7UnPaddingEx(decrypted)
+}
+
+func EcbEncrypt(data, key []byte) []byte {
+ block, _ := aes.NewCipher(key)
+ data = PKCS7PaddingEx(data, block.BlockSize())
+ decrypted := make([]byte, len(data))
+ size := block.BlockSize()
+
+ for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
+ block.Encrypt(decrypted[bs:be], data[bs:be])
+ }
+
+ return decrypted
+}
diff --git a/pub/common/crypt/crypt.go b/pub/common/crypt/crypt.go
new file mode 100644
index 0000000..68bb269
--- /dev/null
+++ b/pub/common/crypt/crypt.go
@@ -0,0 +1,566 @@
+package crypt
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/md5"
+ "crypto/rand"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "hash/crc32"
+ "io"
+ "net/url"
+ "os"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+// CreateToken 生成Token算法
+func CreateToken(secret string, tokenClaims map[string]interface{}) (tokeness string, err error) {
+ if secret == "" {
+ return "", errors.New("secret is empty")
+ }
+ claims := jwt.MapClaims(tokenClaims)
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokeness, err = token.SignedString([]byte(secret))
+ if err != nil {
+ return "", err
+ }
+ return tokeness, nil
+}
+
+func keyFunc(secret string) jwt.Keyfunc {
+ return func(token *jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ }
+}
+
+// ParseToken 解析Token
+func ParseToken(secret string, tokeness string) (map[string]interface{}, error) {
+ if secret == "" || tokeness == "" {
+ return nil, errors.New("secret or tokeness is empty")
+ }
+ token, err := jwt.Parse(tokeness, keyFunc(secret))
+ if err != nil {
+ return nil, err
+ }
+ //验证token,如果token被修改过则为false
+ if !token.Valid {
+ return nil, errors.New("token is invalid")
+ }
+ claimsToken, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return nil, errors.New("cannot convert claim to MapClaim")
+ }
+ return claimsToken, nil
+}
+
+// StructToStr 结构体转json 先对结构体按照字典顺序排序 并返回字符串
+func StructToStr(obj interface{}) (string, error) {
+ if obj == nil {
+ return "", errors.New("obj is not nil")
+ }
+ data := make(map[string]interface{})
+ bytes, err := json.Marshal(obj)
+ if err != nil {
+ return "", err
+ }
+ err = json.Unmarshal(bytes, &data)
+ if err != nil {
+ return "", err
+ }
+ str, err := MapToStr(data)
+ if err != nil {
+ return "", err
+ }
+ return str, nil
+}
+
+// MapToStr 对map按照字典顺序排序 并返回字符串
+func MapToStr(data map[string]interface{}) (string, error) {
+ if data == nil || len(data) == 0 {
+ return "", errors.New("data is nil or empty")
+ }
+ newData := make(map[string]interface{})
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ newData[k] = data[k]
+ }
+ b, err := json.Marshal(newData)
+ if err != nil {
+ return "", err
+ }
+ str := string(b)
+ return str, nil
+}
+
+// JSON2Struct json转struct
+func JSON2Struct(str string, obj interface{}) {
+ if err := json.Unmarshal([]byte(str), &obj); err != nil {
+ return
+ }
+}
+
+// JSONArray2Struct json转struct
+func JSONArray2Struct(str []string, obj interface{}) []interface{} {
+ var data []interface{}
+ for _, v := range str {
+ JSON2Struct(v, &obj)
+ data = append(data, obj)
+ }
+ return data
+}
+
+// JSON2Map json转map
+func JSON2Map(str string) (map[string]interface{}, error) {
+ var mapResult map[string]interface{}
+ if err := json.Unmarshal([]byte(str), &mapResult); err != nil {
+ return nil, err
+ }
+ return mapResult, nil
+}
+
+// StructToStrNormal 结构体转str
+func StructToStrNormal(obj interface{}) (string, error) {
+ str, err := json.Marshal(obj)
+ if err != nil {
+ return "", err
+ }
+ return string(str), nil
+}
+
+func UrlValueToStr(values url.Values, exclude string) (string, string) {
+ if values == nil {
+ return "", ""
+ }
+ var buf bytes.Buffer
+ var sign string
+ keys := make([]string, 0, len(values))
+ for k, _ := range values {
+ if k == exclude {
+ sign = strings.Join(values[k], "")
+ continue
+ }
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for i, k := range keys {
+ if i != 0 {
+ buf.WriteString("&")
+ }
+ buf.WriteString(k)
+ buf.WriteString("=")
+ buf.WriteString(strings.Join(values[k], ""))
+ }
+ return buf.String(), sign
+}
+
+// 对参数进行排序后 拼接成url字符串 只对一级字符串类型的参数做处理
+func MapToUrlOnlyStr(data map[string]interface{}) string {
+ if data == nil || len(data) == 0 {
+ return ""
+ }
+ var buf bytes.Buffer
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for i, k := range keys {
+ if i != 0 {
+ buf.WriteString("&")
+ }
+ buf.WriteString(k)
+ buf.WriteString("=")
+ if v, ok := data[k].(string); ok {
+ buf.WriteString(v)
+ }
+ }
+ return buf.String()
+}
+
+// MapToStr 对map按照字典顺序排序 并返回字符串url 拼接的字段
+func MapToUrlStr(data map[string]interface{}) string {
+ if data == nil || len(data) == 0 {
+ return ""
+ }
+ var buf bytes.Buffer
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for i, k := range keys {
+ if i != 0 {
+ buf.WriteString("&")
+ }
+ buf.WriteString(k)
+ buf.WriteString("=")
+ if v, ok := data[k].(string); ok {
+ buf.WriteString(v)
+ } else {
+ t := reflect.TypeOf(data[k])
+ if t != nil && t.Kind() == reflect.Map {
+ if v, ok := data[k].(map[string]interface{}); ok {
+ buf.WriteString("{")
+ str := MapToUrlStr(v)
+ buf.WriteString(str)
+ buf.WriteString("}")
+ }
+ }
+ if t != nil && (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) {
+ arraybyte, _ := json.Marshal(data[k])
+ buf.WriteString(string(arraybyte))
+ }
+ }
+ }
+ return buf.String()
+}
+
+// MapToURL 集合转URL
+func MapToURL(m map[string]string) string {
+ if m == nil || len(m) == 0 {
+ return ""
+ }
+ v := url.Values{}
+ for k := range m {
+ v.Set(k, string(m[k]))
+ }
+ return v.Encode()
+}
+
+func Strcut2UrlValue(obj interface{}) (string, error) {
+ t := reflect.TypeOf(obj)
+ v := reflect.ValueOf(obj)
+ if t.Kind() != reflect.Struct {
+ return "", errors.New("obj must be struct type")
+ }
+ u := url.Values{}
+ for i := 0; i < t.NumField(); i++ {
+ var value string
+ tag := t.Field(i).Tag.Get("json")
+ switch t.Field(i).Type.Kind() {
+ case reflect.String:
+ value = v.Field(i).String()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ value = strconv.Itoa(int(v.Field(i).Int()))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ value = strconv.FormatUint(v.Field(i).Uint(), 10)
+ case reflect.Slice, reflect.Array:
+ buf := bytes.Buffer{}
+ b, _ := json.Marshal(v.Field(i).Interface())
+ t := strings.TrimRight(strings.TrimLeft(string(b), "["), "]")
+ array := strings.Split(t, ",")
+ if len(array) == 0 {
+ value = ""
+ }
+ if len(array) == 1 {
+ value = array[0]
+ } else {
+ for i, v := range array {
+ if i == 0 {
+ buf.WriteString(v)
+ continue
+ }
+ buf.WriteString("&")
+ buf.WriteString(tag)
+ buf.WriteString("=")
+ buf.WriteString(v)
+ value = buf.String()
+ }
+ }
+ }
+ u.Add(tag, value)
+ }
+ dcodeurl, err := url.QueryUnescape(u.Encode())
+ return dcodeurl, err
+}
+
+// StrToMd5 字符串转md5
+func StrToMd5(str string) string {
+ data := []byte(str)
+ return ByteToMd5(data)
+}
+
+// ByteToMd5 数组转md5
+func ByteToMd5(data []byte) string {
+ md5Ctx := md5.New()
+ md5Ctx.Write(data)
+ cipherStr := md5Ctx.Sum(nil)
+ return hex.EncodeToString(cipherStr)
+}
+
+// FileToMd5 文件转MD5
+func FileToMd5(fileName string) string {
+ file, err := os.Open(fileName)
+ if err != nil {
+ return ""
+ }
+ defer file.Close()
+ md5Ctx := md5.New()
+ if _, err := io.Copy(md5Ctx, file); err != nil {
+ return ""
+ }
+ cipherStr := md5Ctx.Sum(nil)
+ return hex.EncodeToString(cipherStr)
+}
+
+// HashCode 对一个字符串生成唯一的hasHcode 码
+func HashCode(src string) int {
+ v := int(crc32.ChecksumIEEE([]byte(src)))
+ if v > 0 {
+ return v
+ }
+ if -v > 0 {
+ return -v
+ }
+ return 0
+}
+
+// HexToString converts
+func HexToString(data []byte) string {
+ return hex.EncodeToString(data)
+}
+
+// StrToSha256 字符串转sha256
+func StrToSha256(str string) string {
+ shaCtx := sha256.New()
+ shaCtx.Write([]byte(str))
+ shaBytes := shaCtx.Sum(nil)
+ cipherStr := hex.EncodeToString(shaBytes[:])
+ return cipherStr
+}
+
+// StrToSha1 字符串转sha1
+func StrToSha1(str string, secret string) []byte {
+ h := hmac.New(sha1.New, []byte(secret))
+ h.Write([]byte(str))
+ return h.Sum(nil)
+}
+
+// StrToSha1 字符串转sha1 转16进制后
+func StrToHmacSha1(str, secret string) string {
+ return hex.EncodeToString(StrToSha1(str, secret))
+}
+
+// StrToHmacSha256 加密 字符串转sha256加密字符串
+func StrToHmacSha256(str string, secret string) string {
+ h := hmac.New(sha256.New, []byte(secret))
+ h.Write([]byte(str))
+ cipherStr := hex.EncodeToString(h.Sum(nil))
+ return cipherStr
+}
+
+func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
+ padding := blockSize - len(ciphertext)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ return append(ciphertext, padtext...)
+}
+
+func PKCS7UnPadding(origData []byte) []byte {
+ length := len(origData)
+ unpadding := int(origData[length-1])
+ return origData[:(length - unpadding)]
+}
+
+// AesEncrypt AES加密
+func AesEncrypt(origData, key string) ([]byte, error) {
+ origDataByte := []byte(origData)
+ return CoreAesEncrypt(origDataByte, key)
+}
+
+// AesDecrypt AES解密
+func AesDecrypt(crypted, key string) (string, error) {
+ cryptedByte := []byte(crypted)
+ return CoreAesDecrypt(cryptedByte, key)
+}
+
+// AesDecrypt AES解密
+func AesDecryptByte(cryptedByte []byte, key string) (string, error) {
+ return CoreAesDecrypt(cryptedByte, key)
+}
+
+// CoreAesEncrypt AES加密
+func CoreAesEncrypt(origData []byte, key string) ([]byte, error) {
+ keybyte := []byte(key)
+ block, err := aes.NewCipher(keybyte)
+ if err != nil {
+ return nil, err
+ }
+ blockSize := block.BlockSize()
+ origData = PKCS7Padding(origData, blockSize)
+ blockMode := cipher.NewCBCEncrypter(block, keybyte[:blockSize])
+ crypted := make([]byte, len(origData))
+ blockMode.CryptBlocks(crypted, origData)
+ return crypted, nil
+}
+
+// CoreAesDecrypt AES解密
+func CoreAesDecrypt(crypted []byte, key string) (string, error) {
+ keybyte := []byte(key)
+ block, err := aes.NewCipher(keybyte)
+ if err != nil {
+ return "", err
+ }
+ blockSize := block.BlockSize()
+ blockMode := cipher.NewCBCDecrypter(block, keybyte[:blockSize])
+ origData := make([]byte, len(crypted))
+ blockMode.CryptBlocks(origData, crypted)
+ origData = PKCS7UnPadding(origData)
+ return string(origData), nil
+}
+
+// XorEnc
+func XorEnc(src string, xorKey string) string {
+ var result string
+ j := 0
+ bt := []rune(src)
+ xor := []rune(xorKey)
+ for i := 0; i < len(bt); i++ {
+ s := strconv.FormatInt(int64(bt[i]^xor[j]), 10)
+ result = result + s
+ j = 1 % len(xor)
+ }
+ return result
+}
+
+// XorDec
+func XorDec(src string, xorKey string) string {
+ var result string
+ j := 0
+ bt := []rune(src)
+ xor := []rune(xorKey)
+ for i := 0; i < len(bt); i++ {
+ s := strconv.FormatInt(int64(bt[i]^xor[j]), 10)
+ result = result + s
+ j = i % len(xor)
+ }
+ return result
+}
+
+func XorDecode(src, key string) string {
+ srcByte, _ := base64.StdEncoding.DecodeString(src)
+ keyByte, _ := base64.StdEncoding.DecodeString(key)
+ srcByte = xor(srcByte, keyByte)
+ return base64.StdEncoding.EncodeToString(srcByte)
+}
+
+func XorEncode(src, key string) string {
+ return XorDecode(src, key)
+}
+
+func xor(src []byte, key []byte) []byte {
+ for i := 0; i < len(src); i++ {
+ src[i] ^= key[i%len(key)]
+ }
+ return src
+}
+
+func XORLength(src []byte, key []byte, length int) []byte {
+ for i := 0; i < length; i++ {
+ src[i] ^= key[i%len(key)]
+ }
+ return src[:length]
+}
+
+// https://core.telegram.org/api/end-to-end#sending-and-receiving-messages-in-a-secret-chat
+func CoreAesEncryptEx(plain []byte, nonceLen int, key string) ([]byte, error) {
+ nonce := make([]byte, nonceLen)
+ _, err := io.ReadFull(rand.Reader, nonce)
+ if err != nil {
+ return nil, err
+ }
+ var largeShaRaw []byte
+ largeShaRaw = append(largeShaRaw, []byte(key)...)
+ largeShaRaw = append(largeShaRaw, nonce...)
+ largeShaRawMid := len(largeShaRaw) / 2
+ msgKeyLarge := sha256.Sum256(largeShaRaw)
+ msgKey := msgKeyLarge[8:24] //16 bytes
+
+ var shaRawA []byte
+ shaRawA = append(shaRawA, msgKey...)
+ shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...)
+ sha256a := sha256.Sum256(shaRawA) //32 bytes
+
+ var shaRawB []byte
+ shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...)
+ shaRawB = append(shaRawB, msgKey...)
+ sha256b := sha256.Sum256(shaRawB) //32 bytes
+
+ var aesKey []byte //32 bytes AES-256
+ aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes
+ aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes
+ aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes
+
+ var aesIV []byte // 16 bytes
+ aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes
+ aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes
+ aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes
+
+ block, err := aes.NewCipher(aesKey)
+ if err != nil {
+ return nil, err
+ }
+ blockSize := block.BlockSize()
+ ciphertext := PKCS7Padding(plain, blockSize)
+ mode := cipher.NewCBCEncrypter(block, aesIV)
+ cipher := make([]byte, nonceLen+len(ciphertext))
+ copy(cipher, nonce)
+ mode.CryptBlocks(cipher[nonceLen:], ciphertext)
+ return cipher, nil
+}
+
+func CoreAesDecryptEx(crypt []byte, nonceLen int, key string) ([]byte, error) {
+ nonce := make([]byte, nonceLen)
+ copy(nonce, crypt)
+ var largeShaRaw []byte
+ largeShaRaw = append(largeShaRaw, []byte(key)...)
+ largeShaRaw = append(largeShaRaw, nonce...)
+ largeShaRawMid := len(largeShaRaw) / 2
+ msgKeyLarge := sha256.Sum256(largeShaRaw)
+ msgKey := msgKeyLarge[8:24] //16 bytes
+
+ var shaRawA []byte
+ shaRawA = append(shaRawA, msgKey...)
+ shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...)
+ sha256a := sha256.Sum256(shaRawA) //32 bytes
+
+ var shaRawB []byte
+ shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...)
+ shaRawB = append(shaRawB, msgKey...)
+ sha256b := sha256.Sum256(shaRawB) //32 bytes
+
+ var aesKey []byte //32 bytes AES-256
+ aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes
+ aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes
+ aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes
+
+ var aesIV []byte // 16 bytes
+ aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes
+ aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes
+ aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes
+
+ block, err := aes.NewCipher(aesKey)
+ if err != nil {
+ return nil, err
+ }
+ realData := crypt[nonceLen:]
+ blockMode := cipher.NewCBCDecrypter(block, aesIV)
+ origData := make([]byte, len(realData))
+ blockMode.CryptBlocks(origData, realData)
+ return PKCS7UnPadding(origData), nil
+}
diff --git a/pub/common/crypt/crypt_test.go b/pub/common/crypt/crypt_test.go
new file mode 100644
index 0000000..f3b5c6b
--- /dev/null
+++ b/pub/common/crypt/crypt_test.go
@@ -0,0 +1,27 @@
+package crypt
+
+import (
+ "testing"
+ "videoweb/pub/common/log"
+)
+
+var (
+ PlatPublicKey = `
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWtWnWmTgLemO+GBMWIX9N5yx2
+YMc+4MczBogNdTFjF4rrd/+Y1G9qJ8ZDHR7qjBQgJjzcZrGYRUf2wiY6T9ble9+h
+s14pYPpXqkSoD2VOQI4+idb6nRgVQ6rXyMQps7ORcshmEtIS/Ra3aG3XaAFAUCfO
+E3nHGyldtNIGvuBTAQIDAQAB
+-----END PUBLIC KEY-----`
+)
+
+func TestRSA(t *testing.T) {
+ //data := make([]byte, 0)
+ testData := "是对的发送打赏打赏打赏打赏的"
+ rawContext, err := RSAEncryptByPublicKey(PlatPublicKey, []byte(testData))
+ if err != nil {
+ log.Error("wnth RSAEncryptByPublicKey err", log.Any("err", err))
+ return
+ }
+ log.Info(string(rawContext))
+}
diff --git a/pub/common/crypt/rsa.go b/pub/common/crypt/rsa.go
new file mode 100644
index 0000000..0ae93f7
--- /dev/null
+++ b/pub/common/crypt/rsa.go
@@ -0,0 +1,145 @@
+package crypt
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "videoweb/pub/common/log"
+)
+
+func splitBytesSlice(data []byte, n int) [][]byte {
+ var chunk []byte
+ chunks := make([][]byte, 0, len(data)/n+1)
+ for len(data) >= n {
+ chunk, data = data[:n], data[n:]
+ chunks = append(chunks, chunk)
+ }
+ if len(data) > 0 {
+ chunks = append(chunks, data[:])
+ }
+ return chunks
+}
+
+func rsaBytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
+ // fmt.Printf("pubKey is %s", string(pub))
+ block, _ := pem.Decode(pub)
+ enc := x509.IsEncryptedPEMBlock(block)
+ b := block.Bytes
+ var err error
+ if enc {
+ b, err = x509.DecryptPEMBlock(block, nil)
+ if err != nil {
+ log.Error("RSABytesToPublicKey DecryptPEMBlock err", log.E(err))
+ return nil, err
+ }
+ }
+
+ pk, err := x509.ParsePKIXPublicKey(b)
+ if err != nil {
+ log.Error("RSABytesToPublicKey ParsePKIXPublicKey err", log.E(err))
+ return nil, err
+ }
+ if pk_, ok := pk.(*rsa.PublicKey); ok {
+ return pk_, nil
+ }
+ log.Error("RSABytesToPublicKey error input not PublicKey")
+ return nil, errors.New("Not RSA PublicKey format")
+}
+
+func rsaBytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
+ block, _ := pem.Decode(priv)
+ enc := x509.IsEncryptedPEMBlock(block)
+ b := block.Bytes
+ var err error
+ if enc {
+ b, err = x509.DecryptPEMBlock(block, nil)
+ if err != nil {
+ log.Error("RSABytesToPrivateKey DecryptPEMBlock err", log.E(err))
+ return nil, err
+ }
+ }
+ key, err := x509.ParsePKCS8PrivateKey(b)
+ if err != nil {
+ log.Error("RSABytesToPrivateKey ParsePKCS8PrivateKey err", log.E(err))
+ return nil, err
+ }
+ if pk_, ok := key.(*rsa.PrivateKey); ok {
+ return pk_, nil
+ }
+ log.Error("RSABytesToPrivateKey error input not PrivateKey")
+ return nil, errors.New("Not RSA PrivateKey format")
+}
+
+// 公钥加密
+func RSAEncryptByPublicKey(pemPubKey string, data []byte) ([]byte, error) {
+ pubKey, err := rsaBytesToPublicKey([]byte(pemPubKey))
+ if err != nil {
+ log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err))
+ return nil, err
+ }
+ partLen := pubKey.N.BitLen()/8 - 11
+ chunks := splitBytesSlice(data, partLen)
+ result := bytes.Buffer{}
+ for _, chunk := range chunks {
+ cipherSilce, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, chunk)
+ if err != nil {
+ log.Error("RSAEncryptByPublicKey EncryptPKCS1v15 err", log.E(err))
+ return nil, err
+ }
+ result.Write(cipherSilce)
+ }
+ return result.Bytes(), nil
+}
+
+// 私玥签名
+func RSASignByPrivateKey(pemPrivateKey string, data []byte, hash crypto.Hash) ([]byte, error) {
+ privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey))
+ if err != nil {
+ log.Error("RSASignByPrivateKey rsaBytesToPrivateKey err", log.E(err))
+ return nil, err
+ }
+ h := hash.New()
+ h.Write(data)
+ hashed := make([]byte, 0)
+ hashed = h.Sum(hashed)
+
+ return rsa.SignPKCS1v15(rand.Reader, privateKey, hash, hashed)
+
+}
+func RSAVerifyByPublicKey(publicKey string, data, sign []byte, hash crypto.Hash) (err error) {
+ pubKey, err := rsaBytesToPublicKey([]byte(publicKey))
+ if err != nil {
+ log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err))
+ return err
+ }
+ h := hash.New()
+ h.Write(data)
+ hashed := make([]byte, 0)
+ hashed = h.Sum(hashed)
+ return rsa.VerifyPKCS1v15(pubKey, hash, hashed, sign)
+}
+
+// 公钥解密
+func RSADecryptByPrivateKey(pemPrivateKey string, cipherData []byte) ([]byte, error) {
+ privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey))
+ if err != nil {
+ log.Error("RSADecryptByPrivateKey rsaBytesToPrivateKey err", log.E(err))
+ return nil, err
+ }
+ blockLen := privateKey.N.BitLen() / 8
+ chunks := splitBytesSlice(cipherData, blockLen)
+ result := bytes.Buffer{}
+ for _, chunk := range chunks {
+ plain, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, chunk)
+ if err != nil {
+ log.Error("RSADecryptByPrivateKey DecryptPKCS1v15 err", log.E(err))
+ return nil, err
+ }
+ result.Write(plain)
+ }
+ return result.Bytes(), nil
+}
diff --git a/pub/common/enum/consts/consts.go b/pub/common/enum/consts/consts.go
new file mode 100644
index 0000000..13cdd03
--- /dev/null
+++ b/pub/common/enum/consts/consts.go
@@ -0,0 +1,126 @@
+package consts
+
+import (
+ "strings"
+ "time"
+)
+
+type RunMod string
+
+const (
+ RunModeDev RunMod = "dev"
+ RunModeTest RunMod = "test"
+ RunModeProd RunMod = "prod"
+ RunModRelease RunMod = "release"
+)
+
+// gin中ctx使用的key
+const (
+ CtxUserID = "USER_ID"
+ CtxUpUserID = "UP_USER_ID"
+ CtxUpInfo = "UP_INFO"
+ CtxUA = "UA"
+ CtxUAH5 = "UAH5"
+ CtxIP = "IP"
+ CtxDevId = "DEVID"
+ CtxDevType = "DEVTYPE"
+ CtxSysType = "SYSTYPE"
+ CtxVersion = "VERSION"
+ CtxBuildId = "BUILDID"
+ CtxToken = "TOKEN"
+ CtxOnlineTime = "ONLINE_TIME"
+)
+
+const (
+ AppSysType_And = ""
+ AppSysType_Ios = ""
+)
+
+// Etcd 的Key
+const (
+ EtcdWarmupDB = "/micro/config/com.sz.api.server/warmupdb"
+ EtcdSzDB = "/micro/config/com.sz.api.server/db"
+ EtcdRds = "/micro/config/com.sz.api.server/redis"
+)
+
+const (
+ AdminJwtTokenExpire = 30 * 24 * time.Hour
+ UserJwtTokenExpire = 30 * 24 * time.Hour
+ UserSignInExpire = 60 * 24 * time.Hour
+)
+
+const (
+ UAHeader = "User-Agent"
+)
+
+const (
+ IOS = "ios"
+ Android = "android"
+ H5 = "h5"
+ PC = "pc"
+ H5IOS = "h5_ios"
+ H5Android = "h5_android"
+ H5PC = "h5_pc"
+ Light = "light"
+)
+
+const (
+ CaptchaLen = 6 //验证码长度
+)
+
+const (
+ TypeIOS = "ios"
+ TypeAndroid = "and"
+ TypeWeb = "h5"
+)
+
+// 机型
+const (
+ WebTypeIOS = "ios"
+ WebTypeAndroid = "android"
+ WebSysTypeH5 = "h5"
+)
+
+// func HandleSysType(sysType string) (sys string) {
+// if strings.Contains(strings.ToLower(sysType), TypeIOS) {
+// sys = IOS
+// }
+//
+// if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
+// sys = Android
+// }
+//
+// if strings.Contains(strings.ToLower(sysType), TypeWeb) {
+// sys = H5
+// }
+// return
+// }
+
+func ToWebSysType(sysType string) (sys string) {
+ if strings.Contains(strings.ToLower(sysType), TypeIOS) {
+ sys = WebTypeIOS
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
+ sys = WebTypeAndroid
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeWeb) {
+ sys = WebSysTypeH5
+ }
+ return
+}
+func ToNewSysType(sysType string) (sys string) {
+ if strings.Contains(strings.ToLower(sysType), TypeIOS) {
+ sys = WebTypeIOS
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
+ sys = WebTypeAndroid
+ }
+
+ if strings.Contains(strings.ToLower(sysType), TypeWeb) {
+ sys = WebSysTypeH5
+ }
+ return
+}
\ No newline at end of file
diff --git a/pub/common/enum/redisconst/const.go b/pub/common/enum/redisconst/const.go
new file mode 100644
index 0000000..4c0496a
--- /dev/null
+++ b/pub/common/enum/redisconst/const.go
@@ -0,0 +1,100 @@
+package redisconst
+
+import (
+ "fmt"
+ "strings"
+ "time"
+)
+
+const (
+ visitLogFmt = "bd_visitlog:%s:%d"
+ FiveMVisit = "bd_visitFmt:%s"
+ BloomUser = "bd_bloom_user:%d"
+ AppUserToken = "bd_app_user_token:%d"
+ captchaKey = "bd_captcha_mobile_%s"
+ CaptchaExpire = 5 * time.Minute
+ replayNonceKey = "bd_replay:nonce:_%s"
+ mediaM3U8Fmt = "m3u8cache:%s"
+ MediaM3U8CacheExpire = time.Hour * 24 * 7
+
+ //禁止登陆设置
+ userForbit = "userForbit:%d"
+ ipForbit = "ipForbit:%s"
+
+ //每分钟Ip注册次数
+ registerLimist = "registerLimit:%s"
+ RegisterLimistExpire = time.Minute * 30
+
+ //用户关注限制
+ careLimitKey = "carelimit:%d"
+ CareLimitKeyExpire = 15 * time.Second
+
+ //用户评论限制
+ commentLimitKey = "commentlimit:%s"
+ CommentLimitKeyExpire = 1 * time.Minute
+ CommentDayLimitKeyExpire = 10 * time.Hour
+
+ //每用户访问次数限制
+ userCallLimit = "userCall:%d"
+ ipCallLimit = "ipCall:%s"
+ CallLimitExpire = time.Second * 10
+)
+
+func BloomUserKey(userId int64) string {
+ return fmt.Sprintf(BloomUser, userId)
+}
+
+func FiveMVisitKey(time string) string {
+ return fmt.Sprintf(FiveMVisit, time)
+}
+
+func UserVisitLogKey(uid int64, t time.Time) string {
+ return fmt.Sprintf(visitLogFmt, t.Format("20060102"), uid)
+}
+
+func MobileCaptchaKey(mobile string) string {
+ return fmt.Sprintf(captchaKey, mobile)
+}
+
+func AppUserTokenKey(userId int64) string {
+ return fmt.Sprintf(AppUserToken, userId)
+}
+
+func MediaM3U8Key(name string) string {
+ return fmt.Sprintf(mediaM3U8Fmt, name)
+}
+func UserForbitKey(userId int64) string {
+ return fmt.Sprintf(userForbit, userId)
+}
+
+func RegisterLimitKey(ip string) string {
+ ip = strings.ReplaceAll(ip, ":", "-")
+ return fmt.Sprintf(registerLimist, ip)
+}
+
+func CareLimitKey(userId int64) string {
+ return fmt.Sprintf(careLimitKey, userId)
+}
+func ReplayNonceKey(nonce string) string {
+ return fmt.Sprintf(replayNonceKey, nonce)
+}
+
+func CommentLimitKey(ip string) string {
+ return fmt.Sprintf(commentLimitKey, ip)
+}
+func CommentOneDayLimitKey(ip string) string {
+ return fmt.Sprintf("commentdaylimit:%s", ip)
+}
+
+func UserCallLimitKey(userId int64) string {
+ return fmt.Sprintf(userCallLimit, userId)
+}
+
+func IpCallLimitKey(ip string) string {
+ return fmt.Sprintf(ipCallLimit, ip)
+}
+
+func IpForbitKey(ip string) string {
+ ip = strings.ReplaceAll(ip, ":", "-")
+ return fmt.Sprintf(ipForbit, ip)
+}
\ No newline at end of file
diff --git a/pub/common/enum/video/video.go b/pub/common/enum/video/video.go
new file mode 100644
index 0000000..219758d
--- /dev/null
+++ b/pub/common/enum/video/video.go
@@ -0,0 +1,27 @@
+package video
+
+type VideoType int64
+type videoStatus int64
+
+// 0-上架、1-未上架
+const (
+ Enable videoStatus = iota
+ Disable
+)
+
+// 1是AV、2是SP
+const (
+ _ VideoType = iota
+ AV
+ SP
+)
+
+const AVLimit = 1
+
+type recordType int64
+
+const (
+ _ recordType = iota
+ Watch
+ Downlaoad
+)
diff --git a/pub/common/errors/code.go b/pub/common/errors/code.go
new file mode 100644
index 0000000..6af873c
--- /dev/null
+++ b/pub/common/errors/code.go
@@ -0,0 +1,169 @@
+package errors
+
+type CodeType int64
+
+const NotFind = "mongo: no documents in result"
+
+// 错误级别
+const (
+ ParamLevel = 1000 // 参数级
+ StoreLevel = 3000 // 存储级
+ ServerLevel = 4000 // 服务级
+ BusinessLevel = 6000 // 业务逻辑级
+)
+
+// app 端用到了
+// 1000, 重新登陆
+// 4002 短信发送失败
+// 6017 手机号码已绑定
+// 6023 user_video操作错误
+// 这里作出兼容
+
+func (c CodeType) Error() string {
+ return ""
+}
+
+const (
+ InvalidateToken CodeType = ParamLevel + iota // 不可用的token,客户端需要重新登陆
+ MethodNotFound // 方法为找到,指gin方法未找到
+ ApiNotFound // api未找到
+ Limiting // 限流
+ NoToken // 无token 1004
+)
+
+const (
+ DBQuery CodeType = StoreLevel + iota
+ DBInsert
+ DBUpdate
+ DBDelete
+ RedisSet
+ RedisGet
+ RedisDel
+ ProxyPutObject
+ EtcdGet
+ EtcdPut
+ RedisRPush
+ RedisLPop
+)
+
+const (
+ HttpClient CodeType = ServerLevel + iota // httpclient
+ TypePointer // 指针类型
+ SmsLimit // 短信限制
+ SmsSend // 短信发送失败
+ SmsTmp // 短信模版
+ SmsTmpParse // 短信模版解析
+ UserId // 用户ID
+ OpenFile // 打开文件
+ FsErr // seaweedfs
+ CtxTimeout // context超时
+ UnmarshalJson // json
+ FSFiler // fs filer
+ HttpGet //
+ ReadData // read data
+ IOCopy // io copy
+ Pass // pass
+ PassConfID // pass config id
+ Tips // 提示错误
+)
+
+const (
+ // 用户权限
+ UserPermission CodeType = BusinessLevel + iota
+ Unknown // 未知错误
+ InvalidKeyword // 无效的关键字
+ UserName // 用户名称
+ Mobile // 手机号码
+ Captcha // 验证码
+ CaptchaErr // 验证码错误
+ Psw // 密码
+ PayCode // 用户支付密码
+ Number // 数字类型
+ String // 字符串类型
+ VideoId // 视频ID
+ Progress // 进度
+ Ids // id数组
+ CatagoryExist // 视频种类已存在
+ MobileBinded // 手机号码已绑定
+ Rate // 返佣金比例
+ Brokerage // 返佣
+ Logo // 用户logo
+ UploadFile //
+ EmptyFile //
+ Operate // user_video操作错误
+ UserDisable //
+ NickName //
+ Gender //
+ InviteCode //
+ Coat //
+ ReLogin //
+ PopType //
+ AgentBinded //
+ M3u8FileName //
+ M3u8Read //
+ VideoName //
+ ExchangeCodeErr // 兑换码错误
+ ChanAcount //
+ Recharge //
+ Withdraw //
+ ExportErr // 导出参数错误
+ DomainErr // 主域名错误
+ ForbiddenWord // 禁言
+ ForbiddenLogin // 禁止登陆
+ DuplicateLike // 重复点赞
+ AliPayUnBind // 支付宝未绑定
+ BankCardUnBind // 银行卡未绑定
+ VideoUploadExist // 视频重复上传
+ Task //
+ PostsNewArgsInvalid // 帖子参数错误
+ PostDupi // 帖子重复发送
+ PostNotExisted // 帖子不存在
+ PostLikeOnLiked // 重复点赞帖子
+ PostUndoLikeOnUnliked // 尚未点赞,不可取消点赞
+ PostFavOnFaved // 重复 收藏
+ PostUndoFavOnUnfaved // 尚未收藏,不可取消收藏
+ PostDisLikeOnDisLiked // 重复屏蔽帖子
+ PostUndoDisLikeOnUnliked // 尚未屏蔽,不可取消屏蔽
+ UserDoNotForSelf // 用户不能对自己进行此项操作
+ UserWatchDupi // 不可重复关注同一用户
+ UserNoWatch // 没有关注该用户,不可取消关注
+ PostRepleNewArgsInvalid // 帖子 回复 参数错误
+ PostReplyNotExisted // 评论不存在
+ PostReplyAlready // 评论不存在
+ NoWatchableUsers // 没有可以关注的用户池
+ PostReplyLikeOnLiked // 重复点赞帖子
+ PostReplyUndoLikeOnUnliked // 尚未点赞,不可取消点赞
+ PostPriceInvalid // 帖子定价异常,不可购买
+ PostIsBuyed // 帖子已购买,不可重复购买
+ NoBalance // 余额不足
+ BalanceErr // 编辑余额错误
+ NoAccount // 没账户
+ AccountDisabled // 账户已被封禁
+ BuySelf // 自己帖子,不用购买
+ RedisError // 服务器繁忙
+ Button // 落地页按钮
+ ReplyTimeShort // 回复间隔时间太短
+ OperateLog // 日志出错
+ RoleErr // 角色权限不够
+ DocExists // 查找的文档已存在
+ PostUnknownError // 未知错误
+ SeekScoreConfirm // 商人追分确认
+ GetOrderInfo // 商人充值订单报错
+ PromSeq // 商区错误
+ SignErr // 签名错误
+ BadRequest // 传参错误
+ ReplyPoolExisted // 评论已存在
+ LimitIPErr // 限制IP登陆
+)
+
+// 代充专用
+const (
+ TakeChatFail CodeType = iota + 18000 //发起代充聊天失败 18000
+ GetOrderInfoFail //获取代理充值订单信息失败 18001
+ AppIDNotExist //代充AppID 不存在 18002
+ TraderNotExist //此商人不存在 18003
+ TraderBalanceNotEnough //商人余额不足 18004
+ TraderHasLocked //商人被禁止登陆 18005
+ TraderSeekScoreFail //商人追分失败 18006
+ TraderSeekScoreFailDueNotEnough //商人追分失败 18007
+)
diff --git a/pub/common/errors/error.go b/pub/common/errors/error.go
new file mode 100644
index 0000000..1421715
--- /dev/null
+++ b/pub/common/errors/error.go
@@ -0,0 +1,39 @@
+package errors
+
+import (
+ "fmt"
+)
+
+// SrvErr 错误定义
+type SrvErr struct {
+ Code CodeType `json:"code"`
+ Msg string `json:"msg"`
+ Tips string `json:"tip,omitempty"`
+ Cause string `json:"-"`
+}
+
+// New create an error
+func New(code CodeType, msg string, cause string) *SrvErr {
+ return &SrvErr{
+ Code: code,
+ Msg: msg,
+ Cause: cause,
+ }
+}
+
+// New create an error
+func NewTips(code CodeType, tips string, cause string) *SrvErr {
+ return &SrvErr{
+ Code: code,
+ Msg: tips,
+ Tips: tips,
+ Cause: cause,
+ }
+}
+
+func (s *SrvErr) Error() string {
+ if s == nil {
+ return ""
+ }
+ return fmt.Sprintf("code = %d, msg = %s ,case = %v", s.Code, s.Msg, s.Cause)
+}
diff --git a/pub/common/errors/make.go b/pub/common/errors/make.go
new file mode 100644
index 0000000..4d6cf54
--- /dev/null
+++ b/pub/common/errors/make.go
@@ -0,0 +1,24 @@
+package errors
+
+import (
+ "fmt"
+ "runtime"
+)
+
+func MakeWithErr(code CodeType, err error) error {
+ pc, fn, line, _ := runtime.Caller(1)
+ cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, err.Error())
+ return New(code, err.Error(), cause)
+}
+
+func MakeWithMsg(code CodeType, msg string) error {
+ pc, fn, line, _ := runtime.Caller(1)
+ cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, msg)
+ return New(code, msg, cause)
+}
+
+func MakeWithTips(code CodeType, tips string) error {
+ pc, fn, line, _ := runtime.Caller(1)
+ cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, tips)
+ return NewTips(code, tips, cause)
+}
diff --git a/pub/common/errors/msg_app.go b/pub/common/errors/msg_app.go
new file mode 100644
index 0000000..a74ccfe
--- /dev/null
+++ b/pub/common/errors/msg_app.go
@@ -0,0 +1,19 @@
+package errors
+
+// error tips
+const (
+
+ //基础错误提示
+ ET_Store = "存储错误"
+ ET_DBOp = "DB操作错误"
+ ET_RdsOp = "Rds操作错误"
+ ET_Param = "参数错误"
+ ET_DBQury = "DB查询错误"
+ ET_DBNoResult = "DB查询数据不存在"
+ ET_TransactionsErr = "事务运行错误"
+ //----------------------------------------------
+ ET_UserNoExist = "用户不存在"
+ ET_BloggerApply = "博主申请条件不满足"
+ ET_BloggerAlreadyIs = "用户已经是博主"
+ ET_BalanceNotEnough = "余额不足"
+)
diff --git a/pub/common/fconst/fconst.go b/pub/common/fconst/fconst.go
new file mode 100644
index 0000000..e100ba5
--- /dev/null
+++ b/pub/common/fconst/fconst.go
@@ -0,0 +1,6 @@
+package fconst
+
+const (
+ FileDir = "file-tmp"
+ FsDir = "/fs"
+)
diff --git a/pub/common/godef/godef.go b/pub/common/godef/godef.go
new file mode 100644
index 0000000..4a1f48e
--- /dev/null
+++ b/pub/common/godef/godef.go
@@ -0,0 +1,26 @@
+package godef
+
+import (
+ "runtime"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/tgbot"
+)
+
+func Go(f func()) {
+ //atomic.AddInt64(&routineCount, 1)
+ go func() {
+ defer func() {
+ //atomic.AddInt64(&routineCount, -1)
+ if r := recover(); r != nil {
+ buf := make([]byte, 2048)
+ length := runtime.Stack(buf, false)
+ info := string(buf[:length])
+ log.Error("cat a unkown excetion", log.Any("excetion :", info), log.Any("e", r))
+ //发送tg消息
+ msg := "goroutine crash,the stack:\n" + info
+ tgbot.SendToProgram("程序异常", msg)
+ }
+ }()
+ f()
+ }()
+}
diff --git a/pub/common/googauth/googauth.go b/pub/common/googauth/googauth.go
new file mode 100644
index 0000000..3c086b6
--- /dev/null
+++ b/pub/common/googauth/googauth.go
@@ -0,0 +1,34 @@
+package googauth
+
+import (
+ "github.com/dgryski/dgoogauth"
+)
+
+const windowSize = 30
+const hotpWindowSize = 5
+
+var HotpCfg = dgoogauth.OTPConfig{
+ Secret: "MRSHM6LTMFYHAMJSGM2DKNQ=",
+ HotpCounter: 1,
+ WindowSize: hotpWindowSize,
+}
+
+func New(secret string, name string) string {
+ cfg := &dgoogauth.OTPConfig{
+ Secret: secret,
+ WindowSize: windowSize,
+ }
+ return cfg.ProvisionURI(name)
+}
+
+func Verify(secret string, pwd string) (bool, error) {
+ cfg := &dgoogauth.OTPConfig{
+ Secret: secret,
+ WindowSize: windowSize,
+ }
+ return cfg.Authenticate(pwd)
+}
+
+func VerifyHotp(cfg *dgoogauth.OTPConfig, authCode string, counter int) (bool, error) {
+ return cfg.Authenticate(authCode)
+}
diff --git a/pub/common/googauth/htop.go b/pub/common/googauth/htop.go
new file mode 100644
index 0000000..fa1e02b
--- /dev/null
+++ b/pub/common/googauth/htop.go
@@ -0,0 +1,67 @@
+package googauth
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/binary"
+ "strconv"
+)
+
+// HOTP implementation
+type HOTP struct {
+ secret []byte
+ digits int
+}
+
+// At generate code
+func (h HOTP) At(counter uint64) string {
+ counterBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(counterBytes, counter)
+ hash := hmac.New(sha1.New, h.secret)
+ hash.Write(counterBytes)
+ hs := hash.Sum(nil)
+ offset := hs[19] & 0x0f
+ binCodeBytes := make([]byte, 4)
+ binCodeBytes[0] = hs[offset] & 0x7f
+ binCodeBytes[1] = hs[offset+1] & 0xff
+ binCodeBytes[2] = hs[offset+2] & 0xff
+ binCodeBytes[3] = hs[offset+3] & 0xff
+ binCode := binary.BigEndian.Uint32(binCodeBytes)
+ mod := uint32(1)
+ for i := 0; i < h.digits; i++ {
+ mod *= 10
+ }
+ code := binCode % mod
+ codeString := strconv.FormatUint(uint64(code), 10)
+ if len(codeString) < h.digits {
+ paddingByteLength := h.digits - len(codeString)
+ paddingBytes := make([]byte, paddingByteLength)
+ for i := 0; i < paddingByteLength; i++ {
+ paddingBytes[i] = '0'
+ }
+ codeString = string(paddingBytes) + codeString
+ }
+ return codeString
+}
+
+// Verify verify OTP code
+func (h HOTP) VerifyCode(code string, counter uint64) bool {
+ realCode := h.At(counter)
+ if realCode == code {
+ return true
+ }
+ return false
+}
+
+// Verify verify OTP code
+func (h HOTP) GenerateCode(counter uint64) string {
+ return h.At(counter)
+}
+
+// NewHOTP generate new HOTP instance
+func NewHOTP(secret []byte, digits int) (h *HOTP) {
+ h = new(HOTP)
+ h.secret = secret
+ h.digits = digits
+ return
+}
diff --git a/pub/common/http/client_headers.go b/pub/common/http/client_headers.go
new file mode 100644
index 0000000..749d670
--- /dev/null
+++ b/pub/common/http/client_headers.go
@@ -0,0 +1,27 @@
+package http
+
+import "strings"
+
+// ApplyClientForwardHeaders 向上游透传客户端 IP 与浏览器 UA。
+func ApplyClientForwardHeaders(header map[string]string, clientIP, userAgent string) {
+ if header == nil {
+ return
+ }
+ if ip := strings.TrimSpace(clientIP); ip != "" {
+ header["X-Real-IP"] = ip
+ }
+ if ua := strings.TrimSpace(userAgent); ua != "" {
+ header["User-Agent"] = ua
+ }
+}
+
+// ApplyClientForwardHeadersWithXUA 透传 IP、浏览器 UA,以及可选的 X-User-Agent。
+func ApplyClientForwardHeadersWithXUA(header map[string]string, clientIP, userAgent, xUserAgent string) {
+ ApplyClientForwardHeaders(header, clientIP, userAgent)
+ if header == nil {
+ return
+ }
+ if xua := strings.TrimSpace(xUserAgent); xua != "" {
+ header["X-User-Agent"] = xua
+ }
+}
diff --git a/pub/common/http/forward_headers.go b/pub/common/http/forward_headers.go
new file mode 100644
index 0000000..a89d16a
--- /dev/null
+++ b/pub/common/http/forward_headers.go
@@ -0,0 +1,24 @@
+package http
+
+// ForwardExcludedHeaders 是本服务转发请求到上游 serverAddr 时必须剔除的请求头集合。
+// key 使用 net/http.Header 的规范形式 (textproto.CanonicalMIMEHeaderKey),
+// 调用方可直接用 http.Header 的 key 做查找。
+//
+// 包含两类:
+// 1. RFC 7230 §6.1 定义的 hop-by-hop 头,不允许跨代理转发。
+// 2. 本进程内部注入的控制标志,只对本进程的中间件有意义,不能让上游看见。
+var ForwardExcludedHeaders = map[string]bool{
+ // hop-by-hop
+ "Connection": true,
+ "Keep-Alive": true,
+ "Proxy-Authenticate": true,
+ "Proxy-Authorization": true,
+ "Te": true,
+ "Trailers": true,
+ "Transfer-Encoding": true,
+ "Upgrade": true,
+
+ // 内部 DoS 防护标志,由 server.headToGet 注入,
+ // 仅供 templatecache / filecache 判断是否回填缓存。
+ "X-Origin-Head": true,
+}
diff --git a/pub/common/http/http.go b/pub/common/http/http.go
new file mode 100644
index 0000000..a7c4ccb
--- /dev/null
+++ b/pub/common/http/http.go
@@ -0,0 +1,242 @@
+package http
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "reflect"
+ "time"
+)
+
+var (
+ client = http.Client{
+ Timeout: 30 * time.Second,
+ Transport: &http.Transport{
+ MaxIdleConnsPerHost: 5,
+ MaxConnsPerHost: 100,
+ },
+ }
+)
+
+type ByteSize int64
+
+const (
+ _ = iota // ignore first value by assigning to blank identifier
+ KB ByteSize = 1 << (10 * iota)
+ MB
+)
+
+var (
+ outOfBodySizeErr = errors.New("out of body size limit")
+)
+
+type ResponseBody struct {
+ StatusCode int
+ Data []byte
+}
+type ResponseBody2 struct {
+ StatusCode int
+ Data []byte
+ Header http.Header
+}
+
+type RequestFormData map[string]string
+
+// Post
+// Note: 只会读取小于1M的数据
+func Post(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodPost, url, header, body, int64(1024*MB))
+ return
+}
+
+func PostJson(url string, body io.Reader) (fr *ResponseBody, err error) {
+ header := map[string]string{}
+ header["Content-Type"] = "application/json"
+ fr, err = do(http.MethodPost, url, header, body, int64(1024*KB))
+ return
+}
+
+// PostFormData 提交标准的form表单
+func PostFormData(url string, fromdata RequestFormData) (fr *ResponseBody, err error) {
+ var b bytes.Buffer
+ writer := multipart.NewWriter(&b)
+
+ for k, v := range fromdata {
+ if err = writer.WriteField(k, v); err != nil {
+ return
+ }
+ }
+ if err = writer.Close(); err != nil {
+ return
+ }
+
+ header := map[string]string{"Content-Type": writer.FormDataContentType()}
+ fr, err = do(http.MethodPost, url, header, &b, 2*int64(KB))
+ return
+}
+func DoHost(method string, url string, header map[string]string, body io.Reader, host string) (fr *ResponseBody2, err error) {
+ fr, err = doHost(method, url, header, body, int64(2*MB), host)
+ return
+}
+
+// Get
+// Note: 只会读取小于1M的数据
+func Get(url string, header map[string]string) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodGet, url, header, nil, int64(KB))
+ return
+}
+
+// GetLargeFile 获取大文件。2M
+func GetLargeFile(url string, header map[string]string) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodGet, url, header, nil, int64(20*MB))
+ return
+}
+func doHost(method string, url string, header map[string]string, body io.Reader, size int64, host string) (fr *ResponseBody2, err error) {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return
+ }
+ for k, v := range header {
+ req.Header.Set(k, v)
+ }
+ req.Host = host
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.ContentLength > size {
+ err = outOfBodySizeErr
+ return
+ }
+
+ data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size))
+ if err != nil {
+ return
+ }
+ fr = &ResponseBody2{
+ StatusCode: resp.StatusCode,
+ Data: data,
+ Header: resp.Header,
+ }
+ return
+}
+func do(method string, url string, header map[string]string, body io.Reader, size int64) (fr *ResponseBody, err error) {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return
+ }
+ for k, v := range header {
+ req.Header.Set(k, v)
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.ContentLength > size {
+ err = outOfBodySizeErr
+ return
+ }
+
+ data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size))
+ if err != nil {
+ return
+ }
+ fr = &ResponseBody{
+ StatusCode: resp.StatusCode,
+ Data: data,
+ }
+ return
+}
+
+// 推送对象到文件服务器
+func PostObject(ctx context.Context, url string, header map[string]string, body []byte, objName string) (string, error) {
+ base64data := base64.StdEncoding.EncodeToString(body)
+
+ req := struct {
+ Name string `json:"fileName"`
+ Data string `json:"fileData"`
+ }{
+ Name: objName,
+ Data: base64data,
+ }
+
+ jsonBytes, _ := json.Marshal(req)
+
+ resp, err := Post(url, header, bytes.NewReader(jsonBytes))
+ if err != nil {
+ return "", err
+ }
+
+ type Obj struct {
+ Domain string `json:"domain"`
+ Name string `json:"fileName"`
+ }
+
+ fsResp := struct {
+ Code int `json:"code"`
+ Object Obj `json:"data"`
+ Msg string `json:"msg"`
+ }{}
+
+ if err = json.Unmarshal(resp.Data, &fsResp); err != nil {
+ return "", err
+ }
+
+ if fsResp.Code != 200 || fsResp.Object.Name == "" {
+ return "", fmt.Errorf("上传fs失败")
+ }
+
+ return fsResp.Object.Name, nil
+}
+
+func PostWithBind(ctx context.Context, bind interface{}, url string, body io.Reader) error {
+ if err := verifyBind(bind); err != nil {
+ return err
+ }
+ req, err := http.NewRequestWithContext(ctx, "POST", url, body)
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("response status code:%d", resp.StatusCode)
+ }
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(data, bind)
+}
+
+func verifyBind(bind interface{}) error {
+ bType := reflect.TypeOf(bind)
+ if bType.Kind() != reflect.Ptr {
+ return errors.New("bind must be a Ptr")
+ }
+ return nil
+}
+
+func GetWithBody(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
+ fr, err = do(http.MethodGet, url, header, body, int64(20*MB))
+ return
+}
\ No newline at end of file
diff --git a/pub/common/http/http_test.go b/pub/common/http/http_test.go
new file mode 100644
index 0000000..d99de2f
--- /dev/null
+++ b/pub/common/http/http_test.go
@@ -0,0 +1,16 @@
+package http
+
+import "fmt"
+
+func ExampleGet() {
+ urlstr := "https://palce.hzbeisheng.com/api/sms"
+ data, err := Get(urlstr, nil)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Println(string(data.Data))
+
+ // Output:
+
+}
diff --git a/pub/common/httputil/httputil.go b/pub/common/httputil/httputil.go
new file mode 100644
index 0000000..bfadd58
--- /dev/null
+++ b/pub/common/httputil/httputil.go
@@ -0,0 +1,321 @@
+package httputil
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/url"
+ "strconv"
+ "videoweb/pub/common/log"
+
+ "github.com/ddliu/go-httpclient"
+)
+
+// HTTPClient htttp客户端
+type HTTPClient struct {
+ client *httpclient.HttpClient
+}
+
+type ProxyCfg struct {
+ Host string
+ Timeout int //秒
+ Source string
+}
+
+func (p *ProxyCfg) Build(rawurl string, rawHeaders map[string]string) (string, map[string]string, error) {
+ u, err := url.Parse(rawurl)
+ if err != nil {
+ return "", nil, err
+ }
+ target := url.URL{Scheme: u.Scheme, Host: u.Host}
+ u.Host = p.Host
+ u.Scheme = "http"
+ m := make(map[string]string)
+ m["X-Proxy-Target-Host"] = target.String()
+ m["X-Proxy-Source"] = "ys"
+ if p.Timeout > 0 {
+ m["X-Proxy-Timeout"] = strconv.Itoa(p.Timeout)
+ }
+ if len(rawHeaders) > 0 {
+ for k, v := range rawHeaders {
+ m[k] = v
+ }
+ }
+ return u.String(), m, nil
+}
+
+type httpClientPool struct {
+ cliPool map[string]*httpclient.HttpClient
+ poolSize int
+ offset int
+}
+
+// HTTPResponse http响应
+type HTTPResponse struct {
+ StatusCode int `json:"statusCode"`
+ Body *httpclient.Response `json:"boby"`
+}
+
+var httpCliPool = httpClientPool{cliPool: map[string]*httpclient.HttpClient{}, poolSize: 50}
+
+func New() *HTTPClient {
+ h := httpclient.NewHttpClient().Defaults(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 120,
+ })
+ return &HTTPClient{client: h}
+}
+
+func NewCtx(ctx context.Context) *HTTPClient {
+ h := httpclient.NewHttpClient().WithOptions(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 10,
+ httpclient.OPT_CONTEXT: ctx,
+ })
+ return &HTTPClient{client: h.Begin()}
+}
+
+func Begin() *HTTPClient {
+ h := httpclient.NewHttpClient().Defaults(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 60,
+ })
+ return &HTTPClient{client: h.Begin()}
+}
+
+// GetHTTPClient 获取httpclient
+func Client() *HTTPClient {
+ cliID := "CLIENT-ID-"
+ if len(httpCliPool.cliPool) < httpCliPool.poolSize {
+ h := httpclient.NewHttpClient().Defaults(httpclient.Map{
+ httpclient.OPT_CONNECTTIMEOUT: 10,
+ httpclient.OPT_TIMEOUT: 120,
+ })
+ cliID = cliID + strconv.FormatInt(int64(len(httpCliPool.cliPool)+1), 10)
+ httpCliPool.cliPool[cliID] = h
+ httpCliPool.offset = httpCliPool.poolSize
+ return &HTTPClient{client: h}
+ }
+ if httpCliPool.offset == httpCliPool.poolSize+1 {
+ httpCliPool.offset = 1
+ }
+ cliID = cliID + strconv.FormatInt(int64(httpCliPool.offset), 10)
+ httpCliPool.offset = httpCliPool.offset + 1
+ h, ok := httpCliPool.cliPool[cliID]
+ if !ok || h == nil {
+ h = httpclient.NewHttpClient().WithOption(httpclient.OPT_TIMEOUT, 60)
+ httpCliPool.cliPool[cliID] = h
+ return &HTTPClient{client: h}
+ }
+ return &HTTPClient{client: h}
+}
+
+func jsonUnmarshalResp(bind interface{}, resp *HTTPResponse) error {
+ defer func() { _ = resp.Body.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ b, err := resp.Body.ReadAll()
+ if err != nil {
+ log.Error("httputil jsonUnmarshalResp readall err", log.E(err))
+ return err
+ }
+ err = json.Unmarshal(b, bind)
+ if err != nil {
+ log.Error("httputil jsonUnmarshalResp Unmarshal err", log.E(err))
+ }
+ return err
+}
+
+// Get Get方法
+func (h *HTTPClient) Get(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Get(url, params...)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Get method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+func (h *HTTPClient) PGet(p *ProxyCfg, url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return nil, err
+ }
+ return h.Get(purl, pheaders, params)
+}
+
+// Get GetBytes
+func (h *HTTPClient) GetBytes(url string, headers map[string]string, params ...interface{}) ([]byte, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Get(url, params...)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Get method failed %+v:", err))
+ return nil, err
+ }
+ data, err := response.ReadAll()
+ if err != nil {
+ log.ZapLog.Error("resp readAll errror", log.Any("Error", err))
+ return data, err
+ }
+ defer func() {
+ _ = response.Body.Close()
+ }()
+ return data, nil
+}
+
+// GetWithJResp 结果json.Unmarshal到bind中
+func (h *HTTPClient) GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ resp, err := h.Get(url, headers, params...)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+func (h *HTTPClient) PGetWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return 0, err
+ }
+ return h.GetWithJResp(bind, purl, pheaders, params...)
+}
+
+// Get Get方法
+func (h *HTTPClient) Delete(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Delete(url, params...)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Delete method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// DeleteWithJResp 结果json.Unmarshal到bind中
+func (h *HTTPClient) DeleteWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ resp, err := h.Delete(url, headers, params...)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+// Post Post方法
+func (h *HTTPClient) Post(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Post(url, params)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client Post method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+func (h *HTTPClient) PPost(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return nil, err
+ }
+ return h.Post(purl, pheaders, params)
+}
+
+// POSTJson PostJson方法
+func (h *HTTPClient) POSTJson(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.PostJson(url, data)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client POSTJson method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// POSTJson PostJson方法, bind json.Unmarshal
+func (h *HTTPClient) POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
+ resp, err := h.POSTJson(url, headers, data)
+ if err != nil {
+ log.Error("POSTJsonWithResp err", log.E(err))
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+func (h *HTTPClient) PPOSTJsonWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
+ if p == nil {
+ return 0, fmt.Errorf("ProxyCfg is nil")
+ }
+ purl, pheaders, err := p.Build(url, headers)
+ if err != nil {
+ return 0, err
+ }
+ return h.POSTJsonWithJResp(bind, purl, pheaders, data)
+}
+
+// PostMultipart PostMultipart 上传文件
+func (h *HTTPClient) PostMultipart(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.PostMultipart(url, params)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// Put 流式传输 可用于上传文件
+func (h *HTTPClient) Put(url string, headers map[string]string, body io.Reader) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.Put(url, body)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// PutJSON PutJSON方法
+func (h *HTTPClient) PutJSON(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) {
+ h.client.WithHeaders(headers)
+ response, err := h.client.PutJson(url, data)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// Header Header
+func (h *HTTPClient) Header(url string) (*HTTPResponse, error) {
+ response, err := h.client.Head(url)
+ if err != nil {
+ log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
+ return nil, err
+ }
+ return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
+}
+
+// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件
+func (h *HTTPClient) POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ resp, err := h.Post(url, headers, params)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
+
+// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件
+func (h *HTTPClient) PPOSTWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ resp, err := h.PPost(p, bind, url, headers, params)
+ if err != nil {
+ return 0, err
+ }
+ err = jsonUnmarshalResp(bind, resp)
+ return resp.StatusCode, err
+}
diff --git a/pub/common/httputil/httputil_test.go b/pub/common/httputil/httputil_test.go
new file mode 100644
index 0000000..3c2ea11
--- /dev/null
+++ b/pub/common/httputil/httputil_test.go
@@ -0,0 +1,105 @@
+package httputil
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strconv"
+ "testing"
+)
+
+type ResponseInfo struct {
+ Gzipped bool `json:"gzipped"`
+ Method string `json:"method"`
+ Origin string `json:"origin"`
+ Useragent string `json:"user-agent"` // http://httpbin.org/user-agent
+ Form map[string]string `json:"form"`
+ Files map[string]string `json:"files"`
+ Headers map[string]string `json:"headers"`
+ Cookies map[string]string `json:"cookies"`
+}
+
+var info ResponseInfo
+
+type Resport struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data interface{} `json:"data"`
+}
+
+func Test_Get_Httpcli(t *testing.T) {
+ client := Client()
+ var headers = map[string]string{"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjZTJiNzNmZTI3Y2YyMmE4ZjQyMTVkMCIsInVzZXJOYW1lIjoidHVib2xpbmcxNjgiLCJpc0xvZ2luIjp0cnVlLCJsb2dpblRpbWUiOjE1NjQxNDYwNzI5MDUsImlhdCI6MTU2NDE0NjA3Mn0.VQICBk71KDITus83pZEq3C1y_WbKPZc0HUfmIm_eby8"}
+ respo, err := client.Get("https://no5.xiexiaojia.com/appjmall/api/showCore/appShowList", headers)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ resport := Resport{}
+ body, err := respo.Body.ReadAll()
+ //反序列化
+ err = json.Unmarshal(body, &resport)
+ fmt.Println("response", resport)
+ //案例获取数据
+ fmt.Println("response comName:", resport.Data.([]interface{})[0].(map[string]interface{})["comName"])
+}
+
+func Test_Post_Httpcli(t *testing.T) {
+ client := Client()
+ fmt.Println("http 客户端", client)
+ var headers = map[string]string{"auth_token": "xxxxx"}
+ var params = map[string]string{"userId": "5ce2b73fe27cf22a8f4215d0", "price": "5000", "count": "1", "commId": "5ce40e03e27cf22a8f4215d8",
+ "comName": "日本ceruru.b 脐带血面膜保湿补水修复5片",
+ "comImageCar": "http://oam.szbslhb.com/4,01fb06b4adf1.jpg",
+ "attributeCar": "",
+ }
+ respo, err := client.Post("https://no5.xiexiaojia.com/appjmall/api/cart/addCommToCart", headers, params)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ resport := Resport{}
+ body, err := respo.Body.ReadAll()
+ if err != nil {
+ res, _ := respo.Body.ToString()
+ fmt.Println("response", res)
+ return
+ }
+ //反序列化
+ err = json.Unmarshal(body, &resport)
+ fmt.Println("response", resport)
+}
+
+func Test_PostJson_Httpcli(t *testing.T) {
+ client := Client()
+ var headers = map[string]string{"Content-Type": "application/json"}
+ fmt.Println("http 客户端", client)
+ respo, err := client.Get("https://www.kancloud.cn/cattong/go_command_tutorial/261347", headers)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ body, err := respo.Body.ToString()
+ fmt.Println("response", body)
+}
+
+func Test_PutFile_Httpcli(t *testing.T) {
+ client := Client()
+ file, err := os.Open("/Users/mac/Downloads/1567069549ABBQCTFC.mp4")
+ info, _ := file.Stat()
+
+ var headers = map[string]string{
+ "Content-Type": "application/octet-stream",
+ "Authorization": "bucketName=bucket-01;accessKey=FSd58ed59fccc16812903c37068de8969b;ttl=1h",
+ "Content-Disposition": "fileName=test02.mp4;size=" + strconv.FormatInt(info.Size(), 10),
+ "Signature": "9cea21bf95f235e7d9c5a70888cf742cfb9c946ed25aef8b70ce77c953ccbff0",
+ }
+ fmt.Println("http 客户端", client)
+ respo, err := client.Put("http://localhost:8888/fs/api/uploadStream", headers, file)
+ defer respo.Body.Body.Close()
+ if err != nil {
+ fmt.Printf("%+v", err)
+ }
+ body, err := respo.Body.ToString()
+ fmt.Println("response", body)
+}
diff --git a/pub/common/linode/linode.go b/pub/common/linode/linode.go
new file mode 100644
index 0000000..b43230c
--- /dev/null
+++ b/pub/common/linode/linode.go
@@ -0,0 +1,212 @@
+package linode
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "mime"
+ "os"
+ "path"
+ "path/filepath"
+ "videoweb/pub/common/fconst"
+ "videoweb/pub/common/log"
+
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "github.com/karrick/godirwalk"
+ "github.com/xxjwxc/gowp/workpool"
+)
+
+const (
+ //region = "default"
+ //endpoint = "https://ap-south-1.linodeobjects.com"
+ //accessKey = "0RALF74H45ANVYPRQA1Y"
+ //secretKey = "S1yCKUqxFJHqlFPZXJ3BbD8T6P0Yz6loUDntUrsS"
+ //bucket = "fengshang"
+ M = 1024 * 1024
+ G = M * 1024
+ defaultConcurrency = 150
+ defaultTryMax = 5
+ defaultContentType = "application/octet-stream"
+)
+
+type Client struct {
+ session *session.Session
+ cfg Config
+}
+type UploadReq struct {
+ Key string
+ ContentType string
+ Data []byte
+}
+
+func (u *UploadReq) Valid() bool {
+ return u.Key != "" && len(u.Data) != 0
+}
+
+type Config struct {
+ Region string
+ Endpoint string
+ AccessKey string
+ SecretKey string
+ Bucket string
+ Concurrency int
+ TryMax int
+}
+
+func traverseDir(d string) ([]string, error) {
+ var fileNames []string
+ fn := func(name string, entry *godirwalk.Dirent) error {
+ if !entry.IsRegular() {
+ return nil
+ }
+ fileNames = append(fileNames, name)
+ return nil
+ }
+ pathName := d
+ err := godirwalk.Walk(pathName, &godirwalk.Options{
+ Unsorted: true,
+ Callback: fn,
+ })
+ if err != nil {
+ log.Info("list files failed", log.Any("path", pathName), log.E(err))
+ return nil, err
+ }
+ return fileNames, nil
+}
+
+func New(cfg Config) (*Client, error) {
+ if cfg.TryMax <= 0 {
+ cfg.TryMax = defaultTryMax
+ }
+ if cfg.Concurrency <= 0 {
+ cfg.Concurrency = defaultConcurrency
+ }
+ s, err := session.NewSession(&aws.Config{
+ Region: aws.String(cfg.Region),
+ Endpoint: aws.String(cfg.Endpoint),
+ Credentials: credentials.NewStaticCredentials(cfg.AccessKey, cfg.SecretKey, ""),
+ })
+ if err != nil {
+ log.Error("Linode NewSession err", log.E(err))
+ return nil, err
+ }
+ return &Client{session: s, cfg: cfg}, nil
+}
+
+//func WithDefault() (*Client, error) {
+// return New(&Config{Concurrency: defaultConcurrency, TryMax: defaultTryMax})
+//}
+
+func (c *Client) UploadFolder(folderName string) error {
+ absFiles, err := traverseDir(folderName)
+ if err != nil {
+ return err
+ }
+ if len(absFiles) == 0 {
+ return nil
+ }
+ wp := workpool.New(c.cfg.Concurrency)
+ for _, absFilename := range absFiles {
+ var absName = absFilename
+ wp.Do(func() error {
+ var err error
+ for i := 0; i < c.cfg.TryMax; i++ {
+ start := time.Now()
+ err = c.uploadToLinNode(absName)
+ diff := time.Now().Sub(start)
+ if diff < time.Second {
+ time.Sleep(time.Second - diff)
+ }
+ if err == nil {
+ break
+ }
+ }
+ return err
+ })
+ }
+ return wp.Wait()
+}
+
+func (c *Client) UploadRaw(key, contentType string, body io.Reader) (string, error) {
+ if contentType == "" {
+ contentType = defaultContentType
+ }
+ input := &s3manager.UploadInput{
+ Bucket: aws.String(c.cfg.Bucket),
+ Key: aws.String(key),
+ ACL: aws.String("public-read"),
+ Body: body,
+ ContentType: aws.String(contentType),
+ }
+ result, err := s3manager.NewUploader(c.session, func(uploader *s3manager.Uploader) {
+ uploader.PartSize = 1 * G
+ }).Upload(input)
+ if err != nil {
+ log.Info("UpdateRaw Upload Err", log.Any("key", key), log.E(err))
+ return "", err
+ }
+ log.Info("UpdateRaw Upload OK", log.Any("key", key), log.Any("Location", result.Location))
+ return "", nil
+}
+func (c *Client) Upload(req UploadReq) error {
+ if !req.Valid() {
+ return errors.New("invalid upload request")
+ }
+ if req.ContentType == "" {
+ req.ContentType = defaultContentType
+ ext := path.Ext(req.Key)
+ if ext != "" {
+ req.ContentType = mime.TypeByExtension(ext)
+ }
+ }
+ var err error
+ for i := 0; i < defaultTryMax; i++ {
+ buf := bytes.NewReader(req.Data)
+ input := &s3.PutObjectInput{
+ Bucket: aws.String(c.cfg.Bucket),
+ Key: aws.String(req.Key),
+ ACL: aws.String("public-read"),
+ Body: buf,
+ ContentType: aws.String(req.ContentType),
+ }
+
+ _, err := s3.New(c.session).PutObject(input)
+ if err == nil {
+ log.Info("Linode Upload OK", log.Any("key", req.Key))
+ return nil
+ }
+ time.Sleep(time.Millisecond) //出让CPU
+ log.Error("Linode Upload Try err", log.Any("key", req.Key), log.Any("tryCount", i+1), log.E(err))
+ }
+ log.Error("Linode Upload err", log.Any("key", req.Key), log.E(err))
+ return err
+}
+
+// 多线程调用
+func (m *Client) uploadToLinNodeWithKey(fileName, key string) error {
+ f, err := os.OpenFile(fileName, os.O_RDONLY, 0644)
+ if err != nil {
+ log.Info("uploadToLinNodeWithKey OpenFile", log.Any("fileName", fileName), log.E(err))
+ return err
+ }
+ defer func() { _ = f.Close() }()
+ ext := filepath.Ext(fileName)
+ contentType := ""
+ if ext != "" {
+ contentType = mime.TypeByExtension(ext)
+ }
+ _, err = m.UploadRaw(key, contentType, f)
+ return err
+}
+
+func (m *Client) uploadToLinNode(absFilename string) error {
+ key := strings.TrimPrefix(absFilename, fconst.FsDir)
+ return m.uploadToLinNodeWithKey(absFilename, key)
+}
diff --git a/pub/common/linode/linode_test.go b/pub/common/linode/linode_test.go
new file mode 100644
index 0000000..6d94a97
--- /dev/null
+++ b/pub/common/linode/linode_test.go
@@ -0,0 +1,28 @@
+package linode
+
+import (
+ "errors"
+ "fmt"
+ "github.com/xxjwxc/gowp/workpool"
+ "testing"
+)
+
+func TestAA(t *testing.T) {
+ wp := workpool.New(100)
+ for i := 0; i < 200000; i++ {
+ var t = i
+ wp.Do(func() error {
+ fmt.Println(t)
+ if t == 22 {
+ return errors.New("111")
+ }
+ return nil
+ })
+ }
+ err := wp.Wait()
+ if err != nil {
+ fmt.Println("fail", err)
+ } else {
+ fmt.Println("OK")
+ }
+}
diff --git a/pub/common/lock/cache.go b/pub/common/lock/cache.go
new file mode 100644
index 0000000..5d8a314
--- /dev/null
+++ b/pub/common/lock/cache.go
@@ -0,0 +1,86 @@
+package lock
+
+import (
+ "sync"
+)
+
+type IIdent interface {
+ GetID() int64
+}
+
+type Cache[T IIdent] struct {
+ dict map[int64]T
+ lock sync.RWMutex
+}
+
+func NewCache[T IIdent]() *Cache[T] {
+ return &Cache[T]{
+ dict: make(map[int64]T),
+ }
+}
+
+func (c *Cache[T]) Get(Id int64) (T, bool) {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ t, ok := c.dict[Id]
+ return t, ok
+}
+
+func (c *Cache[T]) Keys() []int64 {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ keys := make([]int64, 0, len(c.dict))
+ for k := range c.dict {
+ keys = append(keys, k)
+ }
+ return keys
+}
+
+func (c *Cache[T]) Set(items ...T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, item := range items {
+ c.dict[item.GetID()] = item
+ }
+}
+
+func (c *Cache[T]) Del(items ...T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, item := range items {
+ delete(c.dict, item.GetID())
+ }
+}
+
+func (c *Cache[T]) Update(dict map[int64]T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ c.dict = dict
+}
+
+func (c *Cache[T]) SetItems(items []T) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for _, item := range items {
+ c.dict[item.GetID()] = item
+ }
+}
+
+func (c *Cache[T]) Has(Id int64) bool {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ _, ok := c.dict[Id]
+ return ok
+}
+
+func (c *Cache[T]) GetItems(Ids []int64) []T {
+ ret := make([]T, 0)
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ for _, id := range Ids {
+ if item, ok := c.dict[id]; ok {
+ ret = append(ret, item)
+ }
+ }
+ return ret
+}
diff --git a/pub/common/lock/dict.go b/pub/common/lock/dict.go
new file mode 100644
index 0000000..10b7979
--- /dev/null
+++ b/pub/common/lock/dict.go
@@ -0,0 +1,143 @@
+package lock
+
+import (
+ "sync"
+)
+
+type Dict[K comparable, V any] struct {
+ dict map[K]V
+ lock sync.RWMutex
+}
+
+func NewDict[K comparable, V any]() *Dict[K, V] {
+ return &Dict[K, V]{
+ dict: make(map[K]V),
+ }
+}
+
+// Count get dict key count
+func (d *Dict[K, V]) Count() int {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ return len(d.dict)
+}
+
+// Keys get dict keys
+func (d *Dict[K, V]) Keys() []K {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ keys := make([]K, 0, len(d.dict))
+ for k := range d.dict {
+ keys = append(keys, k)
+ }
+ return keys
+}
+
+// Set set k v to dict, overwrite old value if exists
+func (d *Dict[K, V]) Set(key K, value V) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.dict[key] = value
+}
+
+// Has if exists, return true
+func (d *Dict[K, V]) Has(key K) bool {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ _, exists := d.dict[key]
+ return exists
+}
+
+// Get if exists, return value and true
+func (d *Dict[K, V]) Get(key K) (V, bool) {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ value, exists := d.dict[key]
+ return value, exists
+}
+
+// Del if exists and del success, return true
+func (d *Dict[K, V]) Del(key K) bool {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ _, exists := d.dict[key]
+ delete(d.dict, key)
+ return exists
+}
+
+// Remove if exists and remove success, return old value and true
+func (d *Dict[K, V]) Remove(key K) (V, bool) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ value, exists := d.dict[key]
+ delete(d.dict, key)
+ return value, exists
+}
+
+// SetIfNotExists if not exists and set success, return true
+func (d *Dict[K, V]) SetIfNotExists(key K, value V) bool {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ _, exists := d.dict[key]
+ if !exists {
+ d.dict[key] = value
+ return true
+ }
+ return false
+}
+
+// GetOrSetIfNotExists if key exists return old:true, or, set new value, and return new:false
+func (d *Dict[K, V]) GetOrSetIfNotExists(key K, new V) (V, bool) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ old, exists := d.dict[key]
+ if !exists {
+ d.dict[key] = new
+ return new, false
+ }
+ return old, true
+}
+
+// Travel travels all dict keys and values
+func (d *Dict[K, V]) Travel(travel func(key K, value V) bool) {
+ d.lock.RLock()
+ defer d.lock.RUnlock()
+ for key, value := range d.dict {
+ cont := travel(key, value)
+ if !cont {
+ break
+ }
+ }
+}
+
+// Clear dict directly
+func (d *Dict[K, V]) Clear() {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.dict = map[K]V{}
+}
+
+// Update replace dict directly
+func (d *Dict[K, V]) Update(dict map[K]V) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ d.dict = dict
+}
+
+// Replace replace dict directly and return old dict
+func (d *Dict[K, V]) Replace(dict map[K]V) map[K]V {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ old := d.dict
+ d.dict = dict
+ return old
+}
+
+// Append dict directly
+func (d *Dict[K, V]) Append(dict map[K]V) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ for k, v := range dict {
+ d.dict[k] = v
+ }
+}
diff --git a/pub/common/lock/queue.go b/pub/common/lock/queue.go
new file mode 100644
index 0000000..531d557
--- /dev/null
+++ b/pub/common/lock/queue.go
@@ -0,0 +1,75 @@
+package lock
+
+import "sync"
+
+// Queue 定义一个泛型队列结构
+type Queue[T any] struct {
+ items []T
+ lock sync.RWMutex // 添加读写锁
+}
+
+// NewQueue 创建一个新的队列
+func NewQueue[T any]() *Queue[T] {
+ return &Queue[T]{
+ items: make([]T, 0),
+ }
+}
+
+// Enqueue 入队操作 - 将元素添加到队列末尾
+func (q *Queue[T]) Enqueue(item T) {
+ q.lock.Lock() // 写操作需要写锁
+ defer q.lock.Unlock()
+
+ q.items = append(q.items, item)
+}
+
+// Dequeue 出队操作 - 从队列头部移除并返回元素
+func (q *Queue[T]) Dequeue() (T, bool) {
+ q.lock.Lock() // 写操作需要写锁
+ defer q.lock.Unlock()
+
+ var zero T
+ if len(q.items) == 0 {
+ return zero, false
+ }
+
+ item := q.items[0]
+ q.items = q.items[1:]
+ return item, true
+}
+
+// Peek 查看队列头部元素但不移除
+func (q *Queue[T]) Peek() (T, bool) {
+ q.lock.RLock() // 读操作使用读锁
+ defer q.lock.RUnlock()
+
+ var zero T
+ if len(q.items) == 0 {
+ return zero, false
+ }
+ return q.items[0], true
+}
+
+// IsEmpty 检查队列是否为空
+func (q *Queue[T]) IsEmpty() bool {
+ q.lock.RLock() // 读操作使用读锁
+ defer q.lock.RUnlock()
+
+ return len(q.items) == 0
+}
+
+// Size 返回队列中的元素个数
+func (q *Queue[T]) Size() int {
+ q.lock.RLock() // 读操作使用读锁
+ defer q.lock.RUnlock()
+
+ return len(q.items)
+}
+
+// Clear 清空队列
+func (q *Queue[T]) Clear() {
+ q.lock.Lock() // 写操作需要写锁
+ defer q.lock.Unlock()
+
+ q.items = make([]T, 0)
+}
diff --git a/pub/common/log/log.go b/pub/common/log/log.go
new file mode 100644
index 0000000..89cf271
--- /dev/null
+++ b/pub/common/log/log.go
@@ -0,0 +1,135 @@
+package log
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "time"
+ "videoweb/app/appg"
+ "videoweb/pub/common/log/zapcorex"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+var (
+ zapLogger *zap.Logger
+ zapLoggerS *zap.SugaredLogger
+ ZapLog *zap.Logger
+ ZapLogS *zap.SugaredLogger
+ Debug = zapLogger.Debug
+ Info = zapLogger.Info
+ Warn = zapLogger.Warn
+ Error = zapLogger.Error
+ Panic = zapLogger.Panic
+ Fatal = zapLogger.Fatal
+ Debugf = zapLoggerS.Debugf
+ Infof = zapLoggerS.Infof
+ Warnf = zapLoggerS.Warnf
+ Errorf = zapLoggerS.Errorf
+ Fatalf = zapLoggerS.Fatalf
+ Debugw = zapLoggerS.Debugw
+ Infow = zapLoggerS.Infow
+ Warnw = zapLoggerS.Warnw
+ Errorw = zapLoggerS.Errorw
+ Fatalw = zapLoggerS.Fatalw
+ TestLog = func(msg string, fields ...Field) {
+ if appg.Conf.RunMod == "prod" {
+ return
+ }
+ zapLogger.Info(msg, fields...)
+ }
+)
+
+func newEncoderConfig() zapcore.EncoderConfig {
+ return zapcore.EncoderConfig{
+ // Keys can be anything except the empty string.
+ TimeKey: "T",
+ LevelKey: "L",
+ NameKey: "N",
+ CallerKey: "C",
+ MessageKey: "M",
+ StacktraceKey: "S",
+ FunctionKey: "F",
+ LineEnding: zapcore.DefaultLineEnding,
+ EncodeLevel: zapcore.CapitalColorLevelEncoder,
+ EncodeTime: timeEncoder,
+ EncodeDuration: zapcore.StringDurationEncoder,
+ EncodeCaller: zapcore.ShortCallerEncoder,
+ ConsoleSeparator: " ",
+ }
+}
+
+func init() {
+ core := zapcore.NewCore(
+ zapcorex.NewConsoleEncoder(newEncoderConfig()),
+ zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
+ zap.DebugLevel,
+ )
+ zapLogger = zap.New(core, zap.AddCaller())
+ zapLoggerS = zapLogger.Sugar()
+ ZapLog = zapLogger
+ ZapLogS = zapLoggerS
+ Debug = zapLogger.Debug
+ Info = zapLogger.Info
+ Warn = zapLogger.Warn
+ Error = zapLogger.Error
+ Panic = zapLogger.Panic
+ Fatal = zapLogger.Fatal
+ Debugf = zapLoggerS.Debugf
+ Infof = zapLoggerS.Infof
+ Warnf = zapLoggerS.Warnf
+ Errorf = zapLoggerS.Errorf
+ Fatalf = zapLoggerS.Fatalf
+ Debugw = zapLoggerS.Debugw
+ Infow = zapLoggerS.Infow
+ Warnw = zapLoggerS.Warnw
+ Errorw = zapLoggerS.Errorw
+ Fatalw = zapLoggerS.Fatalw
+}
+
+func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+ enc.AppendString(t.In(time.Local).Format("2006-01-02 15:04:05"))
+}
+
+type Field = zap.Field
+
+func Any(key string, value interface{}) Field {
+ return zap.Any(key, value)
+}
+
+func E(err error) Field {
+ return Any("err", err)
+}
+
+type contextKey int64
+
+var (
+ loggerContextKey contextKey
+)
+
+func ContextWithLogger(ctx context.Context, l *zap.SugaredLogger) context.Context {
+ return context.WithValue(ctx, loggerContextKey, l)
+}
+
+func LoggerFromContext(ctx context.Context) *zap.SugaredLogger {
+ v := ctx.Value(loggerContextKey)
+ if v == nil {
+ return ZapLogS
+ }
+ return v.(*zap.SugaredLogger)
+}
+
+type Logger struct{}
+
+// Info logs routine messages about cron's operation.
+func (s *Logger) Info(msg string, keysAndValues ...interface{}) {
+ _ = log.Default().Output(2, msg+" "+fmt.Sprint(keysAndValues...))
+}
+
+// Error logs an error condition.
+func (s *Logger) Error(err error, msg string, keysAndValues ...interface{}) {
+ line := fmt.Sprintf("%s %v %s", msg, err, fmt.Sprint(keysAndValues...))
+ _ = log.Default().Output(2, line)
+}
diff --git a/pub/common/log/zapcorex/bufferpool/bufferpool.go b/pub/common/log/zapcorex/bufferpool/bufferpool.go
new file mode 100644
index 0000000..dad583a
--- /dev/null
+++ b/pub/common/log/zapcorex/bufferpool/bufferpool.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package bufferpool houses zap's shared internal buffer pool. Third-party
+// packages can recreate the same functionality with buffers.NewPool.
+package bufferpool
+
+import "go.uber.org/zap/buffer"
+
+var (
+ _pool = buffer.NewPool()
+ // Get retrieves a buffer from the pool, creating one if necessary.
+ Get = _pool.Get
+)
diff --git a/pub/common/log/zapcorex/color.go b/pub/common/log/zapcorex/color.go
new file mode 100644
index 0000000..6b8faa7
--- /dev/null
+++ b/pub/common/log/zapcorex/color.go
@@ -0,0 +1,44 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package color adds coloring functionality for TTY output.
+package zapcorex
+
+import "fmt"
+
+// Foreground colors.
+const (
+ Black Color = iota + 30
+ Red
+ Green
+ Yellow
+ Blue
+ Magenta
+ Cyan
+ White
+)
+
+// Color represents a text color.
+type Color uint8
+
+// Add adds the coloring to the given string.
+func (c Color) Add(s string) string {
+ return fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(c), s)
+}
diff --git a/pub/common/log/zapcorex/console_encoder.go b/pub/common/log/zapcorex/console_encoder.go
new file mode 100644
index 0000000..51ea796
--- /dev/null
+++ b/pub/common/log/zapcorex/console_encoder.go
@@ -0,0 +1,150 @@
+package zapcorex
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "videoweb/pub/common/log/zapcorex/bufferpool"
+
+ "go.uber.org/zap/buffer"
+ "go.uber.org/zap/zapcore"
+)
+
+var _sliceEncoderPool = sync.Pool{
+ New: func() interface{} {
+ return &sliceArrayEncoder{elems: make([]interface{}, 0, 2)}
+ },
+}
+
+func getSliceEncoder() *sliceArrayEncoder {
+ return _sliceEncoderPool.Get().(*sliceArrayEncoder)
+}
+
+func putSliceEncoder(e *sliceArrayEncoder) {
+ e.elems = e.elems[:0]
+ _sliceEncoderPool.Put(e)
+}
+
+type consoleEncoder struct {
+ *jsonEncoder
+}
+
+// NewConsoleEncoder creates an encoder whose output is designed for human -
+// rather than machine - consumption. It serializes the core log entry data
+// (message, level, timestamp, etc.) in a plain-text format and leaves the
+// structured context as JSON.
+//
+// Note that although the console encoder doesn't use the keys specified in the
+// encoder configuration, it will omit any element whose key is set to the empty
+// string.
+func NewConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
+ if cfg.ConsoleSeparator == "" {
+ // Use a default delimiter of '\t' for backwards compatibility
+ cfg.ConsoleSeparator = "\t"
+ }
+ return consoleEncoder{newJSONEncoder(cfg, true)}
+}
+
+func (c consoleEncoder) Clone() zapcore.Encoder {
+ return consoleEncoder{c.jsonEncoder.Clone().(*jsonEncoder)}
+}
+
+func (c consoleEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
+ line := bufferpool.Get()
+
+ // We don't want the entry's metadata to be quoted and escaped (if it's
+ // encoded as strings), which means that we can't use the JSON encoder. The
+ // simplest option is to use the memory encoder and fmt.Fprint.
+ //
+ // If this ever becomes a performance bottleneck, we can implement
+ // ArrayEncoder for our plain-text format.
+ arr := getSliceEncoder()
+ if c.TimeKey != "" && c.EncodeTime != nil {
+ c.EncodeTime(ent.Time, arr)
+ }
+ if c.LevelKey != "" && c.EncodeLevel != nil {
+ c.EncodeLevel(ent.Level, arr)
+ }
+ if ent.LoggerName != "" && c.NameKey != "" {
+ nameEncoder := c.EncodeName
+
+ if nameEncoder == nil {
+ // Fall back to FullNameEncoder for backward compatibility.
+ nameEncoder = zapcore.FullNameEncoder
+ }
+
+ nameEncoder(ent.LoggerName, arr)
+ }
+
+ for i := range arr.elems {
+ if i > 0 {
+ line.AppendString(c.ConsoleSeparator)
+ }
+ fmt.Fprint(line, arr.elems[i])
+ }
+
+ putSliceEncoder(arr)
+
+ // Add the message itself.
+ if c.MessageKey != "" {
+ c.addSeparatorIfNecessary(line)
+ line.AppendString(ent.Message)
+ }
+
+ // Add any structured context.
+ c.writeContext(line, fields)
+
+ if ent.Caller.Defined {
+ if c.FunctionKey != "" {
+ c.addSeparatorIfNecessary(line)
+ if idx := strings.LastIndex(ent.Caller.Function, "/"); idx > 0 {
+ line.AppendString(White.Add(ent.Caller.Function[idx+1:]))
+ } else {
+ line.AppendString(White.Add(ent.Caller.Function))
+ }
+ }
+
+ line.AppendString(c.ConsoleSeparator)
+ if c.CallerKey != "" && c.EncodeCaller != nil {
+ line.AppendString(White.Add(ent.Caller.TrimmedPath()))
+ }
+ }
+
+ // If there's no stacktrace key, honor that; this allows users to force
+ // single-line output.
+ if ent.Stack != "" && c.StacktraceKey != "" {
+ line.AppendByte('\n')
+ line.AppendString(ent.Stack)
+ }
+
+ line.AppendString(c.LineEnding)
+ return line, nil
+}
+
+func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []zapcore.Field) {
+ context := c.jsonEncoder.Clone().(*jsonEncoder)
+ defer func() {
+ // putJSONEncoder assumes the buffer is still used, but we write out the buffer so
+ // we can free it.
+ context.buf.Free()
+ putJSONEncoder(context)
+ }()
+
+ addFields(context, extra)
+ context.closeOpenNamespaces()
+ if context.buf.Len() == 0 {
+ return
+ }
+
+ c.addSeparatorIfNecessary(line)
+ line.AppendByte('{')
+ line.Write(context.buf.Bytes())
+ line.AppendByte('}')
+}
+
+func (c consoleEncoder) addSeparatorIfNecessary(line *buffer.Buffer) {
+ if line.Len() > 0 {
+ line.AppendString(c.ConsoleSeparator)
+ }
+}
diff --git a/pub/common/log/zapcorex/field.go b/pub/common/log/zapcorex/field.go
new file mode 100644
index 0000000..d5f91e8
--- /dev/null
+++ b/pub/common/log/zapcorex/field.go
@@ -0,0 +1,9 @@
+package zapcorex
+
+import "go.uber.org/zap/zapcore"
+
+func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
+ for i := range fields {
+ fields[i].AddTo(enc)
+ }
+}
diff --git a/pub/common/log/zapcorex/json_encoder.go b/pub/common/log/zapcorex/json_encoder.go
new file mode 100644
index 0000000..95f8975
--- /dev/null
+++ b/pub/common/log/zapcorex/json_encoder.go
@@ -0,0 +1,518 @@
+package zapcorex
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "math"
+ "sync"
+ "time"
+ "unicode/utf8"
+
+ "videoweb/pub/common/log/zapcorex/bufferpool"
+
+ "go.uber.org/zap/buffer"
+ "go.uber.org/zap/zapcore"
+)
+
+// For JSON-escaping; see jsonEncoder.safeAddString below.
+const _hex = "0123456789abcdef"
+
+var _jsonPool = sync.Pool{New: func() interface{} {
+ return &jsonEncoder{}
+}}
+
+func getJSONEncoder() *jsonEncoder {
+ return _jsonPool.Get().(*jsonEncoder)
+}
+
+func putJSONEncoder(enc *jsonEncoder) {
+ if enc.reflectBuf != nil {
+ enc.reflectBuf.Free()
+ }
+ enc.EncoderConfig = nil
+ enc.buf = nil
+ enc.spaced = false
+ enc.openNamespaces = 0
+ enc.reflectBuf = nil
+ enc.reflectEnc = nil
+ _jsonPool.Put(enc)
+}
+
+type jsonEncoder struct {
+ *zapcore.EncoderConfig
+ buf *buffer.Buffer
+ spaced bool // include spaces after colons and commas
+ openNamespaces int
+
+ // for encoding generic values by reflection
+ reflectBuf *buffer.Buffer
+ reflectEnc *json.Encoder
+}
+
+// NewJSONEncoder creates a fast, low-allocation JSON encoder. The encoder
+// appropriately escapes all field keys and values.
+//
+// Note that the encoder doesn't deduplicate keys, so it's possible to produce
+// a message like
+//
+// {"foo":"bar","foo":"baz"}
+//
+// This is permitted by the JSON specification, but not encouraged. Many
+// libraries will ignore duplicate key-value pairs (typically keeping the last
+// pair) when unmarshaling, but users should attempt to avoid adding duplicate
+// keys.
+func NewJSONEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
+ return newJSONEncoder(cfg, false)
+}
+
+func newJSONEncoder(cfg zapcore.EncoderConfig, spaced bool) *jsonEncoder {
+ return &jsonEncoder{
+ EncoderConfig: &cfg,
+ buf: bufferpool.Get(),
+ spaced: spaced,
+ }
+}
+
+func (enc *jsonEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error {
+ enc.addKey(key)
+ return enc.AppendArray(arr)
+}
+
+func (enc *jsonEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error {
+ enc.addKey(key)
+ return enc.AppendObject(obj)
+}
+
+func (enc *jsonEncoder) AddBinary(key string, val []byte) {
+ enc.AddString(key, base64.StdEncoding.EncodeToString(val))
+}
+
+func (enc *jsonEncoder) AddByteString(key string, val []byte) {
+ enc.addKey(key)
+ enc.AppendByteString(val)
+}
+
+func (enc *jsonEncoder) AddBool(key string, val bool) {
+ enc.addKey(key)
+ enc.AppendBool(val)
+}
+
+func (enc *jsonEncoder) AddComplex128(key string, val complex128) {
+ enc.addKey(key)
+ enc.AppendComplex128(val)
+}
+
+func (enc *jsonEncoder) AddDuration(key string, val time.Duration) {
+ enc.addKey(key)
+ enc.AppendDuration(val)
+}
+
+func (enc *jsonEncoder) AddFloat64(key string, val float64) {
+ enc.addKey(key)
+ enc.AppendFloat64(val)
+}
+
+func (enc *jsonEncoder) AddInt64(key string, val int64) {
+ enc.addKey(key)
+ enc.AppendInt64(val)
+}
+
+func (enc *jsonEncoder) resetReflectBuf() {
+ if enc.reflectBuf == nil {
+ enc.reflectBuf = bufferpool.Get()
+ enc.reflectEnc = json.NewEncoder(enc.reflectBuf)
+
+ // For consistency with our custom JSON encoder.
+ enc.reflectEnc.SetEscapeHTML(false)
+ } else {
+ enc.reflectBuf.Reset()
+ }
+}
+
+var nullLiteralBytes = []byte("null")
+
+// Only invoke the standard JSON encoder if there is actually something to
+// encode; otherwise write JSON null literal directly.
+func (enc *jsonEncoder) encodeReflected(obj interface{}) ([]byte, error) {
+ if obj == nil {
+ return nullLiteralBytes, nil
+ }
+ enc.resetReflectBuf()
+ if err := enc.reflectEnc.Encode(obj); err != nil {
+ return nil, err
+ }
+ enc.reflectBuf.TrimNewline()
+ return enc.reflectBuf.Bytes(), nil
+}
+
+func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error {
+ valueBytes, err := enc.encodeReflected(obj)
+ if err != nil {
+ return err
+ }
+ enc.addKey(key)
+ _, err = enc.buf.Write(valueBytes)
+ return err
+}
+
+func (enc *jsonEncoder) OpenNamespace(key string) {
+ enc.addKey(key)
+ enc.buf.AppendByte('{')
+ enc.openNamespaces++
+}
+
+func (enc *jsonEncoder) AddString(key, val string) {
+ enc.addKey(key)
+ enc.AppendString(val)
+}
+
+func (enc *jsonEncoder) AddTime(key string, val time.Time) {
+ enc.addKey(key)
+ enc.AppendTime(val)
+}
+
+func (enc *jsonEncoder) AddUint64(key string, val uint64) {
+ enc.addKey(key)
+ enc.AppendUint64(val)
+}
+
+func (enc *jsonEncoder) AppendArray(arr zapcore.ArrayMarshaler) error {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('[')
+ err := arr.MarshalLogArray(enc)
+ enc.buf.AppendByte(']')
+ return err
+}
+
+func (enc *jsonEncoder) AppendObject(obj zapcore.ObjectMarshaler) error {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('{')
+ err := obj.MarshalLogObject(enc)
+ enc.buf.AppendByte('}')
+ return err
+}
+
+func (enc *jsonEncoder) AppendBool(val bool) {
+ enc.addElementSeparator()
+ enc.buf.AppendBool(val)
+}
+
+func (enc *jsonEncoder) AppendByteString(val []byte) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.safeAddByteString(val)
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendComplex128(val complex128) {
+ enc.addElementSeparator()
+ // Cast to a platform-independent, fixed-size type.
+ r, i := float64(real(val)), float64(imag(val))
+ enc.buf.AppendByte('"')
+ // Because we're always in a quoted string, we can use strconv without
+ // special-casing NaN and +/-Inf.
+ enc.buf.AppendFloat(r, 64)
+ enc.buf.AppendByte('+')
+ enc.buf.AppendFloat(i, 64)
+ enc.buf.AppendByte('i')
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendDuration(val time.Duration) {
+ cur := enc.buf.Len()
+ if e := enc.EncodeDuration; e != nil {
+ e(val, enc)
+ }
+ if cur == enc.buf.Len() {
+ // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
+ // JSON valid.
+ enc.AppendInt64(int64(val))
+ }
+}
+
+func (enc *jsonEncoder) AppendInt64(val int64) {
+ enc.addElementSeparator()
+ enc.buf.AppendInt(val)
+}
+
+func (enc *jsonEncoder) AppendReflected(val interface{}) error {
+ valueBytes, err := enc.encodeReflected(val)
+ if err != nil {
+ return err
+ }
+ enc.addElementSeparator()
+ _, err = enc.buf.Write(valueBytes)
+ return err
+}
+
+func (enc *jsonEncoder) AppendString(val string) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.safeAddString(val)
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendTimeLayout(time time.Time, layout string) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.buf.AppendTime(time, layout)
+ enc.buf.AppendByte('"')
+}
+
+func (enc *jsonEncoder) AppendTime(val time.Time) {
+ cur := enc.buf.Len()
+ if e := enc.EncodeTime; e != nil {
+ e(val, enc)
+ }
+ if cur == enc.buf.Len() {
+ // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
+ // output JSON valid.
+ enc.AppendInt64(val.UnixNano())
+ }
+}
+
+func (enc *jsonEncoder) AppendUint64(val uint64) {
+ enc.addElementSeparator()
+ enc.buf.AppendUint(val)
+}
+
+func (enc *jsonEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) }
+func (enc *jsonEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) }
+func (enc *jsonEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) }
+func (enc *jsonEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) }
+func (enc *jsonEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) }
+func (enc *jsonEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) }
+func (enc *jsonEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) }
+func (enc *jsonEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) }
+func (enc *jsonEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) }
+func (enc *jsonEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) }
+
+func (enc *jsonEncoder) Clone() zapcore.Encoder {
+ clone := enc.clone()
+ clone.buf.Write(enc.buf.Bytes())
+ return clone
+}
+
+func (enc *jsonEncoder) clone() *jsonEncoder {
+ clone := getJSONEncoder()
+ clone.EncoderConfig = enc.EncoderConfig
+ clone.spaced = enc.spaced
+ clone.openNamespaces = enc.openNamespaces
+ clone.buf = bufferpool.Get()
+ return clone
+}
+
+func (enc *jsonEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
+ final := enc.clone()
+ final.buf.AppendByte('{')
+
+ if final.LevelKey != "" {
+ final.addKey(final.LevelKey)
+ cur := final.buf.Len()
+ final.EncodeLevel(ent.Level, final)
+ if cur == final.buf.Len() {
+ // User-supplied EncodeLevel was a no-op. Fall back to strings to keep
+ // output JSON valid.
+ final.AppendString(ent.Level.String())
+ }
+ }
+ if final.TimeKey != "" {
+ final.AddTime(final.TimeKey, ent.Time)
+ }
+ if ent.LoggerName != "" && final.NameKey != "" {
+ final.addKey(final.NameKey)
+ cur := final.buf.Len()
+ nameEncoder := final.EncodeName
+
+ // if no name encoder provided, fall back to FullNameEncoder for backwards
+ // compatibility
+ if nameEncoder == nil {
+ nameEncoder = zapcore.FullNameEncoder
+ }
+
+ nameEncoder(ent.LoggerName, final)
+ if cur == final.buf.Len() {
+ // User-supplied EncodeName was a no-op. Fall back to strings to
+ // keep output JSON valid.
+ final.AppendString(ent.LoggerName)
+ }
+ }
+ if ent.Caller.Defined {
+ if final.CallerKey != "" {
+ final.addKey(final.CallerKey)
+ cur := final.buf.Len()
+ final.EncodeCaller(ent.Caller, final)
+ if cur == final.buf.Len() {
+ // User-supplied EncodeCaller was a no-op. Fall back to strings to
+ // keep output JSON valid.
+ final.AppendString(ent.Caller.String())
+ }
+ }
+ if final.FunctionKey != "" {
+ final.addKey(final.FunctionKey)
+ final.AppendString(ent.Caller.Function)
+ }
+ }
+ if final.MessageKey != "" {
+ final.addKey(enc.MessageKey)
+ final.AppendString(ent.Message)
+ }
+ if enc.buf.Len() > 0 {
+ final.addElementSeparator()
+ final.buf.Write(enc.buf.Bytes())
+ }
+ addFields(final, fields)
+ final.closeOpenNamespaces()
+ if ent.Stack != "" && final.StacktraceKey != "" {
+ final.AddString(final.StacktraceKey, ent.Stack)
+ }
+ final.buf.AppendByte('}')
+ if final.LineEnding != "" {
+ final.buf.AppendString(final.LineEnding)
+ } else {
+ final.buf.AppendString(zapcore.DefaultLineEnding)
+ }
+
+ ret := final.buf
+ putJSONEncoder(final)
+ return ret, nil
+}
+
+func (enc *jsonEncoder) truncate() {
+ enc.buf.Reset()
+}
+
+func (enc *jsonEncoder) closeOpenNamespaces() {
+ for i := 0; i < enc.openNamespaces; i++ {
+ enc.buf.AppendByte('}')
+ }
+}
+
+func (enc *jsonEncoder) addKey(key string) {
+ enc.addElementSeparator()
+ enc.buf.AppendByte('"')
+ enc.safeAddString(key)
+ enc.buf.AppendByte('"')
+ enc.buf.AppendByte(':')
+ if enc.spaced {
+ enc.buf.AppendByte(' ')
+ }
+}
+
+func (enc *jsonEncoder) addElementSeparator() {
+ last := enc.buf.Len() - 1
+ if last < 0 {
+ return
+ }
+ switch enc.buf.Bytes()[last] {
+ case '{', '[', ':', ',', ' ':
+ return
+ default:
+ enc.buf.AppendByte(',')
+ if enc.spaced {
+ enc.buf.AppendByte(' ')
+ }
+ }
+}
+
+func (enc *jsonEncoder) appendFloat(val float64, bitSize int) {
+ enc.addElementSeparator()
+ switch {
+ case math.IsNaN(val):
+ enc.buf.AppendString(`"NaN"`)
+ case math.IsInf(val, 1):
+ enc.buf.AppendString(`"+Inf"`)
+ case math.IsInf(val, -1):
+ enc.buf.AppendString(`"-Inf"`)
+ default:
+ enc.buf.AppendFloat(val, bitSize)
+ }
+}
+
+// safeAddString JSON-escapes a string and appends it to the internal buffer.
+// Unlike the standard library's encoder, it doesn't attempt to protect the
+// user from browser vulnerabilities or JSONP-related problems.
+func (enc *jsonEncoder) safeAddString(s string) {
+ for i := 0; i < len(s); {
+ if enc.tryAddRuneSelf(s[i]) {
+ i++
+ continue
+ }
+ r, size := utf8.DecodeRuneInString(s[i:])
+ if enc.tryAddRuneError(r, size) {
+ i++
+ continue
+ }
+ enc.buf.AppendString(s[i : i+size])
+ i += size
+ }
+}
+
+// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte.
+func (enc *jsonEncoder) safeAddByteString(s []byte) {
+ for i := 0; i < len(s); {
+ if enc.tryAddRuneSelf(s[i]) {
+ i++
+ continue
+ }
+ r, size := utf8.DecodeRune(s[i:])
+ if enc.tryAddRuneError(r, size) {
+ i++
+ continue
+ }
+ enc.buf.Write(s[i : i+size])
+ i += size
+ }
+}
+
+// tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte.
+func (enc *jsonEncoder) tryAddRuneSelf(b byte) bool {
+ if b >= utf8.RuneSelf {
+ return false
+ }
+ if 0x20 <= b && b != '\\' && b != '"' {
+ enc.buf.AppendByte(b)
+ return true
+ }
+ switch b {
+ case '\\', '"':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte(b)
+ case '\n':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte('n')
+ case '\r':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte('r')
+ case '\t':
+ enc.buf.AppendByte('\\')
+ enc.buf.AppendByte('t')
+ default:
+ // Encode bytes < 0x20, except for the escape sequences above.
+ enc.buf.AppendString(`\u00`)
+ enc.buf.AppendByte(_hex[b>>4])
+ enc.buf.AppendByte(_hex[b&0xF])
+ }
+ return true
+}
+
+func (enc *jsonEncoder) tryAddRuneError(r rune, size int) bool {
+ if r == utf8.RuneError && size == 1 {
+ enc.buf.AppendString(`\ufffd`)
+ return true
+ }
+ return false
+}
diff --git a/pub/common/log/zapcorex/memory_encoder.go b/pub/common/log/zapcorex/memory_encoder.go
new file mode 100644
index 0000000..646a142
--- /dev/null
+++ b/pub/common/log/zapcorex/memory_encoder.go
@@ -0,0 +1,183 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zapcorex
+
+import (
+ "time"
+
+ "go.uber.org/zap/zapcore"
+)
+
+// MapObjectEncoder is an ObjectEncoder backed by a simple
+// map[string]interface{}. It's not fast enough for production use, but it's
+// helpful in tests.
+type MapObjectEncoder struct {
+ // Fields contains the entire encoded log context.
+ Fields map[string]interface{}
+ // cur is a pointer to the namespace we're currently writing to.
+ cur map[string]interface{}
+}
+
+// NewMapObjectEncoder creates a new map-backed ObjectEncoder.
+func NewMapObjectEncoder() *MapObjectEncoder {
+ m := make(map[string]interface{})
+ return &MapObjectEncoder{
+ Fields: m,
+ cur: m,
+ }
+}
+
+// AddArray implements ObjectEncoder.
+func (m *MapObjectEncoder) AddArray(key string, v zapcore.ArrayMarshaler) error {
+ arr := &sliceArrayEncoder{elems: make([]interface{}, 0)}
+ err := v.MarshalLogArray(arr)
+ m.cur[key] = arr.elems
+ return err
+}
+
+// AddObject implements ObjectEncoder.
+func (m *MapObjectEncoder) AddObject(k string, v zapcore.ObjectMarshaler) error {
+ newMap := NewMapObjectEncoder()
+ m.cur[k] = newMap.Fields
+ return v.MarshalLogObject(newMap)
+}
+
+// AddBinary implements ObjectEncoder.
+func (m *MapObjectEncoder) AddBinary(k string, v []byte) { m.cur[k] = v }
+
+// AddByteString implements ObjectEncoder.
+func (m *MapObjectEncoder) AddByteString(k string, v []byte) { m.cur[k] = string(v) }
+
+// AddBool implements ObjectEncoder.
+func (m *MapObjectEncoder) AddBool(k string, v bool) { m.cur[k] = v }
+
+// AddDuration implements ObjectEncoder.
+func (m MapObjectEncoder) AddDuration(k string, v time.Duration) { m.cur[k] = v }
+
+// AddComplex128 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddComplex128(k string, v complex128) { m.cur[k] = v }
+
+// AddComplex64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddComplex64(k string, v complex64) { m.cur[k] = v }
+
+// AddFloat64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddFloat64(k string, v float64) { m.cur[k] = v }
+
+// AddFloat32 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddFloat32(k string, v float32) { m.cur[k] = v }
+
+// AddInt implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt(k string, v int) { m.cur[k] = v }
+
+// AddInt64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt64(k string, v int64) { m.cur[k] = v }
+
+// AddInt32 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt32(k string, v int32) { m.cur[k] = v }
+
+// AddInt16 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt16(k string, v int16) { m.cur[k] = v }
+
+// AddInt8 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddInt8(k string, v int8) { m.cur[k] = v }
+
+// AddString implements ObjectEncoder.
+func (m *MapObjectEncoder) AddString(k string, v string) { m.cur[k] = v }
+
+// AddTime implements ObjectEncoder.
+func (m MapObjectEncoder) AddTime(k string, v time.Time) { m.cur[k] = v }
+
+// AddUint implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint(k string, v uint) { m.cur[k] = v }
+
+// AddUint64 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint64(k string, v uint64) { m.cur[k] = v }
+
+// AddUint32 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint32(k string, v uint32) { m.cur[k] = v }
+
+// AddUint16 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint16(k string, v uint16) { m.cur[k] = v }
+
+// AddUint8 implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUint8(k string, v uint8) { m.cur[k] = v }
+
+// AddUintptr implements ObjectEncoder.
+func (m *MapObjectEncoder) AddUintptr(k string, v uintptr) { m.cur[k] = v }
+
+// AddReflected implements ObjectEncoder.
+func (m *MapObjectEncoder) AddReflected(k string, v interface{}) error {
+ m.cur[k] = v
+ return nil
+}
+
+// OpenNamespace implements ObjectEncoder.
+func (m *MapObjectEncoder) OpenNamespace(k string) {
+ ns := make(map[string]interface{})
+ m.cur[k] = ns
+ m.cur = ns
+}
+
+// sliceArrayEncoder is an ArrayEncoder backed by a simple []interface{}. Like
+// the MapObjectEncoder, it's not designed for production use.
+type sliceArrayEncoder struct {
+ elems []interface{}
+}
+
+func (s *sliceArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error {
+ enc := &sliceArrayEncoder{}
+ err := v.MarshalLogArray(enc)
+ s.elems = append(s.elems, enc.elems)
+ return err
+}
+
+func (s *sliceArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error {
+ m := NewMapObjectEncoder()
+ err := v.MarshalLogObject(m)
+ s.elems = append(s.elems, m.Fields)
+ return err
+}
+
+func (s *sliceArrayEncoder) AppendReflected(v interface{}) error {
+ s.elems = append(s.elems, v)
+ return nil
+}
+
+func (s *sliceArrayEncoder) AppendBool(v bool) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, string(v)) }
+func (s *sliceArrayEncoder) AppendComplex128(v complex128) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendComplex64(v complex64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendDuration(v time.Duration) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendFloat64(v float64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendFloat32(v float32) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt(v int) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt64(v int64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt32(v int32) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt16(v int16) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendInt8(v int8) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendString(v string) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendTime(v time.Time) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint(v uint) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint64(v uint64) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint32(v uint32) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint16(v uint16) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUint8(v uint8) { s.elems = append(s.elems, v) }
+func (s *sliceArrayEncoder) AppendUintptr(v uintptr) { s.elems = append(s.elems, v) }
diff --git a/pub/common/m3u8/enkey.go b/pub/common/m3u8/enkey.go
new file mode 100644
index 0000000..28e798f
--- /dev/null
+++ b/pub/common/m3u8/enkey.go
@@ -0,0 +1,44 @@
+package m3u8
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "sync"
+)
+
+var (
+ cacheEnkey []byte
+ initOnce sync.Once
+ initErr error
+)
+
+// 安全初始化 m3u8 key(文件缺失时不再 panic,交由调用方降级)
+func initEnkey() {
+ initOnce.Do(func() {
+ data, err := ioutil.ReadFile("conf/m3u8.key")
+ if err != nil {
+ initErr = err
+ return
+ }
+ if len(data) == 0 {
+ initErr = fmt.Errorf("conf/m3u8.key is empty")
+ return
+ }
+ cacheEnkey = data
+ })
+}
+
+// Enkey 密匙 key
+func Enkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
+ initEnkey()
+ if initErr != nil {
+ return nil, initErr
+ }
+ if len(cacheEnkey) == 0 {
+ return nil, fmt.Errorf("m3u8 key not loaded")
+ }
+ buffer = bytes.NewBuffer(cacheEnkey)
+ return
+}
diff --git a/pub/common/m3u8/media_play_list.go b/pub/common/m3u8/media_play_list.go
new file mode 100644
index 0000000..cf26ae8
--- /dev/null
+++ b/pub/common/m3u8/media_play_list.go
@@ -0,0 +1,42 @@
+package m3u8
+
+import (
+ //"sz-app-server/pub/common/utils"
+
+ "time"
+ "videoweb/pub/common/utils"
+
+ "github.com/grafov/m3u8"
+)
+
+// makeMediaPlayList 根据from,重新生成播放列表
+func makeMediaPlayList(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
+ media, err = m3u8.NewMediaPlaylist(0, from.Count())
+ if err != nil {
+ return
+ }
+ base := utils.JoinUrlPath(cdnDomain, dirPath)
+ now := time.Now()
+
+ for _, segment := range from.Segments {
+ if segment == nil {
+ continue
+ }
+ sg := new(m3u8.MediaSegment)
+ sg.URI = utils.JoinUrlPath(base, segment.URI)
+ if len(authKey) > 0 {
+ //生成鉴权url
+ urlPath := utils.JoinUrlPath(dirPath, segment.URI)
+ urlAuth := generateUrlAuth(now, urlPath, authKey)
+ sg.URI = sg.URI + urlAuth
+ }
+ sg.Title = segment.Title
+ sg.Duration = segment.Duration
+ if err = media.AppendSegment(sg); err != nil {
+ return
+ }
+ }
+ _ = media.SetDefaultKey("AES-128", keyUri, "", "", "")
+ media.Close()
+ return
+}
diff --git a/pub/common/m3u8/play_list.go b/pub/common/m3u8/play_list.go
new file mode 100644
index 0000000..ebf8a34
--- /dev/null
+++ b/pub/common/m3u8/play_list.go
@@ -0,0 +1,146 @@
+package m3u8
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+ "videoweb/pub/common/enum/redisconst"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/http"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/redis"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/grafov/m3u8"
+)
+
+const (
+ keyUri = "/enkey"
+ webkeyUri = "/api/web/enkey"
+ h5KeyUri = "/enkey"
+ AuthKey = "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
+)
+
+// resolveTsAuthKey 优先使用传入的 tsAuthKey,为空则回退默认 AuthKey。
+func resolveTsAuthKey(tsAuthKey string) string {
+ if key := strings.TrimSpace(tsAuthKey); key != "" {
+ return key
+ }
+ return AuthKey
+}
+
+var headerMediaPlayList *m3u8.MediaPlaylist
+
+// TransFormWithSafely 被检查权限的播放列表
+// 主要检查用户是否有观看该视频的权限
+func TransFormWithSafely(req *Req, cdnDomain, origDomain string, web bool, tsAuthKey string) (*m3u8.MediaPlaylist, *bytes.Buffer, error) {
+ if req.Name == "" {
+ return nil, nil, errors.MakeWithMsg(errors.M3u8FileName, "file name is empty")
+ }
+ if cdnDomain == "" || origDomain == "" {
+ return nil, nil, errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
+ }
+
+ return transform(req.Name, cdnDomain, origDomain, web, tsAuthKey)
+}
+
+// transform m3u8文件转换,将不正确的m3u8文件,替换为正确的m3u8文件
+func transform(name string, cdnDomain, origDomain string, web bool, tsAuthKey string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+ m3u8reader, err := readFromRedis(name, origDomain)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+
+ // 处理媒资库 桶1(v1) 桶2(v2) 桶3(v3)
+ mediaVerson := proxy.AppendsMediaS3Version(name)
+ if len(mediaVerson) > 0 {
+ name = mediaVerson + name
+ }
+
+ authKey := resolveTsAuthKey(tsAuthKey)
+ keyurl := keyUri
+ if web {
+ keyurl = webkeyUri
+ }
+
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ dirPath := utils.PathForInfo(name).Dir
+ media, err = makeMediaPlayList(pl, cdnDomain, dirPath, keyurl, authKey)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ mpl := p.(*m3u8.MasterPlaylist)
+ buff = mpl.Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+func readFromCDN(name, domain string) (reader io.Reader, err error) {
+ requestUrl := utils.JoinUrlPath(domain, name)
+ resp, err := http.GetLargeFile(requestUrl, nil)
+ if err != nil {
+ return
+ }
+ if resp.StatusCode != 200 {
+ msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
+ err = errors.MakeWithMsg(errors.M3u8Read, msg)
+ return
+ }
+ reader = bytes.NewReader(resp.Data)
+ return
+}
+
+// 从redis缓存中读取
+func readFromRedis(name, domain string) (reader io.Reader, err error) {
+ m3u8Key := redisconst.MediaM3U8Key(name)
+ data, err := redis.GetBytes(m3u8Key)
+ if err == nil && len(data) > 0 {
+ reader = bytes.NewReader(data)
+ return
+ }
+ requestUrl := utils.JoinUrlPath(domain, name)
+ resp, err := http.GetLargeFile(requestUrl, nil)
+ if err != nil {
+ fmt.Println("requestUrl111", requestUrl)
+ return
+ }
+ fmt.Println("requestUrl", requestUrl, resp.StatusCode)
+ if resp.StatusCode != 200 {
+ msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
+ err = errors.MakeWithMsg(errors.M3u8Read, msg)
+ return
+ }
+ if err := redis.Set(m3u8Key, resp.Data,
+ redisconst.MediaM3U8CacheExpire); err != nil {
+ log.Info("m3u8:readFromRedis redis set err", log.E(err))
+ }
+ reader = bytes.NewReader(resp.Data)
+ return
+}
+
+func GetHeaderMediaSegUrls() []string {
+ urls := make([]string, 0)
+ if headerMediaPlayList == nil {
+ return urls
+ }
+ for _, segment := range headerMediaPlayList.Segments {
+ if segment == nil {
+ continue
+ }
+ urls = append(urls, segment.URI)
+ }
+ return urls
+}
diff --git a/pub/common/m3u8/transformer.go b/pub/common/m3u8/transformer.go
new file mode 100644
index 0000000..1096553
--- /dev/null
+++ b/pub/common/m3u8/transformer.go
@@ -0,0 +1,133 @@
+package m3u8
+
+import (
+ "bytes"
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "strconv"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/pubdef"
+ "videoweb/pub/storage/proxy"
+
+ "github.com/grafov/m3u8"
+)
+
+// transform m3u8文件转换,将不正确的m3u8文件,替换为正确的m3u8文件
+// apiType(1:app,2:h5app,3:web)
+func TransformEx(name string, cdnDomain, origDomain string, apiType int, authKey string) (
+ media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
+
+ if name == "" {
+ err = errors.MakeWithMsg(errors.M3u8FileName, "m3u8 file name is empty")
+ return
+ }
+ if cdnDomain == "" || origDomain == "" {
+ err = errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
+ return
+ }
+ // 处理媒资库 桶1(v1) 桶2(v2) 桶3(v3)
+ mediaVerson := proxy.AppendsMediaS3Version(name)
+ if len(mediaVerson) > 0 {
+ name = mediaVerson + name
+ }
+ authKey = resolveTsAuthKey(authKey)
+
+ //log.Info("Test", log.Any("name", name), log.Any("cdnDomain", cdnDomain), log.Any("origDomain", origDomain), log.Any("authKey", authKey))
+ m3u8reader, err := readFromRedis(name, origDomain)
+ if err != nil {
+ return
+ }
+ p, t, err := m3u8.DecodeFrom(m3u8reader, false)
+ if err != nil {
+ err = errors.MakeWithErr(errors.M3u8Read, err)
+ return
+ }
+ apiPath := keyUri
+ if apiType == 2 {
+ apiPath = h5KeyUri
+ } else if apiType == 3 {
+ apiPath = webkeyUri
+ }
+
+ switch t {
+ case m3u8.MEDIA:
+ pl := p.(*m3u8.MediaPlaylist)
+ dirPath := utils.PathForInfo(name).Dir
+ media, err = makeTSList(pl, cdnDomain, dirPath, apiPath, authKey)
+ if err != nil {
+ return
+ }
+ buff = media.Encode()
+ case m3u8.MASTER:
+ mpl := p.(*m3u8.MasterPlaylist)
+ buff = mpl.Encode()
+ default:
+ err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
+ }
+ return
+}
+
+// makeTSList 根据from,重新生成播放列表
+func makeTSList(from *m3u8.MediaPlaylist, cdnDomain string,
+ dirPath string, apiPath string, authKey string) (
+ media *m3u8.MediaPlaylist, err error) {
+ media, err = m3u8.NewMediaPlaylist(from.WinSize(), from.Count())
+ if err != nil {
+ return
+ }
+ now := time.Now()
+ base := utils.JoinUrlPath(cdnDomain, dirPath)
+ for _, segment := range from.Segments {
+ if segment == nil {
+ continue
+ }
+ sg := new(m3u8.MediaSegment)
+ sg.URI = utils.JoinUrlPath(base, segment.URI)
+ if len(authKey) > 0 {
+ //生成鉴权url
+ urlPath := utils.JoinUrlPath(dirPath, segment.URI)
+ urlAuth := generateUrlAuth(now, urlPath, authKey)
+ sg.URI = sg.URI + urlAuth
+ }
+ sg.Title = segment.Title
+ sg.Duration = segment.Duration
+ if err = media.AppendSegment(sg); err != nil {
+ return
+ }
+ }
+ _ = media.SetDefaultKey("AES-128", apiPath, "", "", "")
+ media.Close()
+ return
+}
+
+// 生成鉴权url
+func generateUrlAuth(now time.Time, path string, authKey string) string {
+ timestamp := now.Unix()
+ signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey)
+ md5Str := getMD5Sign(signStr)
+ // TS CDN 鉴权参数后追加应用标识,便于流量统计。
+ urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d", timestamp, md5Str, pubdef.AppId)
+ return urlAuth
+}
+
+// 生成鉴权url 七牛云
+func generateUrlAuthQiniu(now time.Time, path string, authKey string) string {
+ timestamp := strconv.FormatInt(now.Add(2*time.Hour).Unix(), 16)
+ base64 := path
+ signStr := authKey + base64 + timestamp
+ md5Str := getMD5Sign(signStr)
+ urlAuth := fmt.Sprintf("?sign=%s&t=%s", md5Str, timestamp)
+ return urlAuth
+}
+
+// getMD5Sign 得到签名
+func getMD5Sign(buf string) string {
+ md5Ctx := md5.New()
+ md5Ctx.Write([]byte(buf))
+ cipherStr := md5Ctx.Sum(nil)
+ nsign := hex.EncodeToString(cipherStr)
+ return nsign
+}
diff --git a/pub/common/m3u8/types.go b/pub/common/m3u8/types.go
new file mode 100644
index 0000000..97a1614
--- /dev/null
+++ b/pub/common/m3u8/types.go
@@ -0,0 +1,13 @@
+package m3u8
+
+type Req struct {
+ Token string `json:"token" form:"token"`
+ VId int64 `json:"vId" form:"vId"`
+ Name string `json:"name" form:"name"`
+ NeedHeader bool `json:"needHeader" form:"needHeader"`
+}
+
+type PostVideoReq struct {
+ Token string `json:"token" form:"token"`
+ Name string `json:"name" form:"name" binding:"required" comment:"must be set"`
+}
diff --git a/pub/common/m3u8/validate.go b/pub/common/m3u8/validate.go
new file mode 100644
index 0000000..a7b4927
--- /dev/null
+++ b/pub/common/m3u8/validate.go
@@ -0,0 +1,18 @@
+package m3u8
+
+//sErr "sz-app-server/pub/common/errors"
+
+// func CheckParams(req *Req) (err error) {
+// if req.Token == "" {
+// err = sErr.MakeWithMsg(sErr.NoToken, "no token")
+// return
+// }
+// if req.VId <= 0 {
+// err = sErr.MakeWithMsg(sErr.VideoId, "invalid video id")
+// return
+// }
+// if req.Name == "" {
+// err = sErr.MakeWithMsg(sErr.VideoName, "invalid name")
+// }
+// return
+// }
diff --git a/pub/common/maths/maths.go b/pub/common/maths/maths.go
new file mode 100644
index 0000000..523f9bd
--- /dev/null
+++ b/pub/common/maths/maths.go
@@ -0,0 +1,68 @@
+package maths
+
+import (
+ "math/big"
+ "strings"
+
+ uuid "github.com/satori/go.uuid"
+)
+
+const CODE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+const CodeLenth = 62
+
+/**
+ * 10进制 为 62进制 字符串。大数字类型
+ *
+ * param: *big.Int number 待转换的数字,非常大的
+ * return: string
+ */
+func From10To62Big(number *big.Int) string {
+ tmpNumber := big.NewInt(0)
+ var result []byte
+ for number.Cmp(big.NewInt(0)) != 0 {
+ var tmp []byte
+ tmpNumber.Rem(number, big.NewInt(CodeLenth))
+ tmp = append(tmp, CODE62[tmpNumber.Int64()])
+ result = append(tmp, result...)
+ number.Div(number, big.NewInt(CodeLenth))
+ }
+ return string(result)
+}
+
+/**
+ * 10进制 为 62进制 字符串。大数字类型
+ *
+ * param: int64 num 待转换的数字
+ * return: string
+ */
+func From10To62(number int64) string {
+ var result []byte
+ for number != 0 {
+ var tmp []byte
+ number1 := number % CodeLenth
+ tmp = append(tmp, CODE62[number1])
+ result = append(tmp, result...)
+ number = number / CodeLenth
+ }
+ return string(result)
+}
+
+func GenUUID() string {
+ u1 := uuid.Must(uuid.NewV4(), nil).String()
+ uuidStr := strings.Replace(u1, "-", "", -1)
+ number, _ := new(big.Int).SetString(uuidStr, 16)
+ return From10To62Big(number)
+}
+
+func Abs(n int64) int64 {
+ if n < 0 {
+ return -n
+ }
+ return n
+}
+
+func Gen32String() string {
+ u1 := uuid.Must(uuid.NewV4(), nil).String()
+ uuidStr := strings.Replace(u1, "-", "", -1)
+ return uuidStr
+}
diff --git a/pub/common/netutil/netutil.go b/pub/common/netutil/netutil.go
new file mode 100644
index 0000000..a2b68b8
--- /dev/null
+++ b/pub/common/netutil/netutil.go
@@ -0,0 +1,52 @@
+package netutil
+
+import (
+ "net"
+ "net/url"
+ "path"
+ "strings"
+)
+
+func GetLocalIP() string {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return ""
+ }
+ for _, address := range addrs {
+ if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+ if ipnet.IP.To4() != nil {
+ return ipnet.IP.String()
+ }
+ }
+ }
+ return ""
+}
+
+func MergeURL(baseURL, relativePath string) string {
+ // 解析基础URL
+ base, err := url.Parse(baseURL)
+ if err != nil {
+ return ""
+ }
+
+ // 如果相对路径是绝对URL,直接返回
+ if strings.HasPrefix(relativePath, "http://") || strings.HasPrefix(relativePath, "https://") {
+ return relativePath
+ }
+
+ // 合并路径
+ mergedPath := path.Join(base.Path, relativePath)
+
+ // 处理路径中的 ./ 和 ../
+ mergedPath = path.Clean(mergedPath)
+
+ // 确保路径以/开头(如果原始base.Path以/结尾)
+ if strings.HasSuffix(base.Path, "/") && !strings.HasPrefix(mergedPath, "/") {
+ mergedPath = "/" + mergedPath
+ }
+
+ // 更新URL的Path部分
+ base.Path = mergedPath
+
+ return base.String()
+}
diff --git a/pub/common/netutil/netutil_test.go b/pub/common/netutil/netutil_test.go
new file mode 100644
index 0000000..fc87a5a
--- /dev/null
+++ b/pub/common/netutil/netutil_test.go
@@ -0,0 +1,10 @@
+package netutil
+
+import (
+ "fmt"
+ "testing"
+)
+
+func Test_GetLocalIP(t *testing.T) {
+ fmt.Println(GetLocalIP())
+}
diff --git a/pub/common/pageopt/match.go b/pub/common/pageopt/match.go
new file mode 100644
index 0000000..e6185af
--- /dev/null
+++ b/pub/common/pageopt/match.go
@@ -0,0 +1,580 @@
+package pageopt
+
+import (
+ "reflect"
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// A A from Array
+type A = []interface{}
+
+// M M from Map
+type M = bson.M
+
+// *******************************************//
+// Matcher
+type Matcher interface {
+ Filter() M
+}
+
+// MergeM
+func MergeM(mats []Matcher) M {
+ merge := M{}
+ for _, m := range mats {
+ if m == nil {
+ continue
+ }
+ ql := m.Filter()
+ for k, v := range ql {
+ merge[k] = v
+ }
+ }
+ return merge
+}
+
+// *******************************************//
+type MergeMatch struct {
+ merge M
+}
+
+func NewMergeMatch(mats ...Matcher) Matcher {
+ mergeM := MergeM(mats)
+ return &MergeMatch{mergeM}
+}
+
+func (m *MergeMatch) Filter() M {
+ return m.merge
+}
+
+// *******************************************//
+// AssignMatch
+type AssignMatch struct {
+ Key string
+ Val interface{}
+}
+
+func NewAssignMatch(key string, val interface{}) Matcher {
+ return &AssignMatch{
+ key,
+ val,
+ }
+}
+
+func (m *AssignMatch) Filter() M {
+ if IsEmpt(m.Val) {
+ return M{}
+ }
+ return M{
+ m.Key: m.Val,
+ }
+}
+
+// *******************************************//
+
+type Sort = bson.D
+
+func SortDesc(Key string) Sort {
+ return Sort{{Key, -1}}
+}
+
+func SortAsc(Key string) Sort {
+ return Sort{{Key, 1}}
+}
+
+// *******************************************//
+// RegexMatch
+type RegexMatch struct {
+ Key string
+ Val interface{}
+ Options string
+}
+
+func NewRegexMatch(key string, val interface{}, opt string) Matcher {
+ return &RegexMatch{
+ Key: key,
+ Val: val,
+ Options: opt,
+ }
+}
+
+func (r *RegexMatch) Filter() M {
+ if IsEmpt(r.Val) {
+ return M{}
+ }
+ return M{
+ r.Key: M{"$regex": r.Val, "$options": r.Options},
+ }
+}
+
+// *******************************************//
+func IsNil(i interface{}) bool {
+ vi := reflect.ValueOf(i)
+ return vi.IsNil()
+}
+
+func IsEmpt(i interface{}) bool {
+ v := reflect.ValueOf(i)
+ switch v.Kind() {
+ case reflect.Ptr:
+ return v.IsNil()
+ case reflect.Slice, reflect.Array:
+ return v.Len() == 0
+ }
+ return false
+}
+
+// *******************************************//
+
+type NEMatch struct {
+ Key string
+ NE interface{}
+}
+
+func (m *NEMatch) Filter() M {
+ if IsEmpt(m.NE) {
+ return M{}
+ }
+ return M{m.Key: M{"$ne": m.NE}}
+}
+
+func NewNEMatch(Key string, ne interface{}) Matcher {
+ return &NEMatch{
+ Key: Key,
+ NE: ne,
+ }
+}
+
+// *******************************************//
+// GEMatch
+type GEMatch struct {
+ Key string
+ GT interface{}
+}
+
+func NewGEMatch(Key string, gt interface{}) Matcher {
+ return &GEMatch{
+ Key: Key,
+ GT: gt,
+ }
+}
+
+func (m *GEMatch) Filter() M {
+ if IsEmpt(m.GT) {
+ return M{}
+ }
+ return M{m.Key: M{"$gt": m.GT}}
+}
+
+// *******************************************//
+// GTEMatch
+type GTEMatch struct {
+ Key string
+ GTE interface{}
+}
+
+func NewGTEMatch(Key string, gte interface{}) Matcher {
+ return >EMatch{
+ Key: Key,
+ GTE: gte,
+ }
+}
+
+func (m *GTEMatch) Filter() M {
+ if IsEmpt(m.GTE) {
+ return M{}
+ }
+ return M{m.Key: M{"$gte": m.GTE}}
+}
+
+// *******************************************//
+
+// GTMatch
+type GTMatch struct {
+ Key string
+ GT interface{}
+}
+
+func NewGTMatch(Key string, gt interface{}) Matcher {
+ return >Match{
+ Key: Key,
+ GT: gt,
+ }
+}
+
+func (m *GTMatch) Filter() M {
+ if IsEmpt(m.GT) {
+ return M{}
+ }
+ return M{m.Key: M{"$gt": m.GT}}
+}
+
+// *******************************************//
+// LTMatch
+type LTMatch struct {
+ Key string
+ LT interface{}
+}
+
+func NewLTMatch(Key string, lt interface{}) Matcher {
+ return <Match{
+ Key: Key,
+ LT: lt,
+ }
+}
+
+func (m *LTMatch) Filter() M {
+ if IsEmpt(m.LT) {
+ return M{}
+ }
+ return M{m.Key: M{"$lt": m.LT}}
+}
+
+// *******************************************//
+
+// LTEMatch
+type LTEMatch struct {
+ Key string
+ LTE interface{}
+}
+
+func NewLTEMatch(Key string, lte interface{}) Matcher {
+ return <EMatch{
+ Key: Key,
+ LTE: lte,
+ }
+}
+
+func (m *LTEMatch) Filter() M {
+ if IsEmpt(m.LTE) {
+ return M{}
+ }
+ return M{m.Key: M{"$lte": m.LTE}}
+}
+
+// *******************************************//
+// GTEAndLTEMatch
+type GTEAndLTEMatch struct {
+ Key string
+ GTE interface{}
+ LTE interface{}
+}
+
+func NewGTEAndLTEMatch(Key string, gte, lte interface{}) Matcher {
+ return >EAndLTEMatch{
+ Key,
+ gte,
+ lte,
+ }
+}
+
+func (m *GTEAndLTEMatch) Filter() M {
+ if IsEmpt(m.GTE) || IsEmpt(m.LTE) {
+ return M{}
+ }
+
+ return M{
+ m.Key: M{
+ "$gte": m.GTE,
+ "$lte": m.LTE,
+ },
+ }
+}
+
+// *******************************************//
+// GTEAndLTMatch
+type GTEAndLTMatch struct {
+ Key string
+ GTE interface{}
+ LT interface{}
+}
+
+func NewGTEAndLTMatch(key string, gte, lt interface{}) Matcher {
+ return >EAndLTMatch{
+ key,
+ gte,
+ lt,
+ }
+}
+
+func (m *GTEAndLTMatch) Filter() M {
+ if IsEmpt(m.GTE) || IsEmpt(m.LT) {
+ return M{}
+ }
+
+ return M{
+ m.Key: M{
+ "$gte": m.GTE,
+ "$lt": m.LT,
+ },
+ }
+}
+
+// *******************************************//
+
+// ExistsMatch
+type ExistsMatch struct {
+ Key string
+ IsExisted *bool
+}
+
+func NewExistsMatch(key string, isExisted *bool) Matcher {
+ return &ExistsMatch{
+ Key: key,
+ IsExisted: isExisted,
+ }
+}
+
+func (e *ExistsMatch) Filter() M {
+ if IsEmpt(e.IsExisted) {
+ return M{}
+ }
+ return M{
+ e.Key: M{"$exists": e.IsExisted},
+ }
+}
+
+// *******************************************//
+// InMatch
+type InMatch struct {
+ Key string
+ Slice interface{}
+}
+
+func NewInMatch(key string, slice interface{}) Matcher {
+ return &InMatch{
+ Key: key,
+ Slice: slice,
+ }
+}
+
+func (i *InMatch) Filter() M {
+ if IsNil(i.Slice) {
+ return M{}
+ }
+ return M{
+ i.Key: M{"$in": i.Slice},
+ }
+}
+
+// *******************************************//
+// NinMatch
+type NinMatch struct {
+ Key string
+ Slice interface{}
+}
+
+func NewNinMatch(key string, slice interface{}) Matcher {
+ return &NinMatch{
+ Key: key,
+ Slice: slice,
+ }
+}
+
+// *******************************************//
+func (n *NinMatch) Filter() M {
+ if IsNil(n.Slice) {
+ return M{}
+ }
+ return M{
+ n.Key: M{"$nin": n.Slice},
+ }
+}
+
+// *******************************************//
+// ManualOrMatch
+type ManualOrMatch struct {
+ Key string
+ Mats []Matcher
+}
+
+func NewManualOrMatch(key string, mats []Matcher) Matcher {
+ return &ManualOrMatch{
+ Key: key,
+ Mats: mats,
+ }
+}
+
+func (o *ManualOrMatch) Filter() M {
+ if len(o.Mats) == 0 {
+ return M{}
+ }
+ fs := []interface{}{}
+ for _, v := range o.Mats {
+ f := v.Filter()
+ if len(f) != 0 {
+ fs = append(fs, v.Filter())
+ }
+ }
+ if len(fs) == 0 {
+ return M{}
+ }
+ return M{"$or": fs}
+}
+
+// *******************************************//
+// BitsAllSetMatch
+type BitsAllSetMatch struct {
+ Key string
+ Bits []int
+}
+
+func NewBitsAllSetMatch(key string, bits []int) Matcher {
+ return &BitsAllSetMatch{
+ Key: key,
+ Bits: bits,
+ }
+}
+
+func (b *BitsAllSetMatch) Filter() M {
+ if len(b.Bits) == 0 {
+ return M{}
+ }
+ return M{
+ b.Key: M{"$bitsAllSet": b.Bits},
+ }
+}
+
+// BitsAllClearMatch
+type BitsAllClearMatch struct {
+ Key string
+ Bits []int
+}
+
+// *******************************************//
+func NewBitsAllClearMatch(key string, bits []int) Matcher {
+ return &BitsAllClearMatch{
+ Key: key,
+ Bits: bits,
+ }
+}
+
+func (b *BitsAllClearMatch) Filter() M {
+ if len(b.Bits) == 0 {
+ return M{}
+ }
+ return M{
+ b.Key: M{"$bitsAllClear": b.Bits},
+ }
+}
+
+// *******************************************//
+
+// UIDMatch
+type UIDMatch struct {
+ UID *int64
+}
+
+func (u *UIDMatch) New() Matcher {
+ return NewAssignMatch("userId", u.UID)
+}
+
+// UIDInMatch
+type UIDInMatch struct {
+ UIDS []int64
+}
+
+func (u *UIDInMatch) New() Matcher {
+ return NewInMatch("userId", u.UIDS)
+}
+
+// *******************************************//
+// UIDMatch
+type UserUIDMatch struct {
+ UID *int64
+}
+
+func (u *UserUIDMatch) New() Matcher {
+ return NewAssignMatch("id", u.UID)
+}
+
+// UIDInMatch
+type UserUIDInMatch struct {
+ UIDS []int64
+}
+
+func (u *UserUIDInMatch) New() Matcher {
+ return NewInMatch("id", u.UIDS)
+}
+
+// *******************************************//
+
+// IDMatch
+type IDMatch struct {
+ ID *primitive.ObjectID
+}
+
+func (i *IDMatch) New() Matcher {
+ return NewAssignMatch("_id", i.ID)
+}
+
+// IDMatch
+type IDint64Match struct {
+ Id *int64
+}
+
+func (i *IDint64Match) New() Matcher {
+ return NewAssignMatch("id", i.Id)
+}
+
+// *******************************************//
+// IDInMatch
+type IDInMatch struct {
+ IDs []primitive.ObjectID
+}
+
+func (c *IDInMatch) New() Matcher {
+ return NewInMatch("_id", c.IDs)
+}
+
+// *******************************************//
+// CreatedAtGTEMatch
+type CreatedAtGTEMatch struct {
+ GTE *time.Time
+}
+
+func (c *CreatedAtGTEMatch) New() Matcher {
+ return NewGTEMatch("createdAt", c.GTE)
+}
+
+// *******************************************//
+// CreatedAtLTMatch
+type CreatedAtLTMatch struct {
+ LT *time.Time
+}
+
+func (c *CreatedAtLTMatch) New() Matcher {
+ return NewLTMatch("createdAt", c.LT)
+}
+
+// *******************************************//
+// CreatedAtGTEAndLTMatch
+type CreatedAtGTEAndLTMatch struct {
+ GTE *time.Time
+ LT *time.Time
+}
+
+func (c *CreatedAtGTEAndLTMatch) New() Matcher {
+ return NewGTEAndLTMatch("createdAt", c.GTE, c.LT)
+}
+
+// // *******************************************//
+// type Int32Match struct {
+// key string //值
+// value *int32
+// }
+//
+// func (i *Int32Match) New() Matcher {
+// return NewAssignMatch("_id", i.ID)
+// }
+//
+// type Int32InMatch struct {
+// key string //值
+// values []int32
+// }
+//
+// func (c *Int32InMatch) New() Matcher {
+// return NewInMatch("_id", c.IDs)
+// }
diff --git a/pub/common/pointers/pointers.go b/pub/common/pointers/pointers.go
new file mode 100644
index 0000000..908c79d
--- /dev/null
+++ b/pub/common/pointers/pointers.go
@@ -0,0 +1,83 @@
+package pointers
+
+import "time"
+
+func Bool(v bool) *bool { return &v }
+func Int(v int) *int { return &v }
+func Int32(v int32) *int32 { return &v }
+func Int64(v int64) *int64 { return &v }
+func Uint32(v uint32) *uint32 { return &v }
+func Uint64(v uint64) *uint64 { return &v }
+func Float32(v float32) *float32 { return &v }
+func Float64(v float64) *float64 { return &v }
+func String(v string) *string { return &v }
+func Time(v time.Time) *time.Time { return &v }
+func StringSlice(v []string) *[]string { return &v }
+func Int64Slice(v []int64) *[]int64 { return &v }
+
+func GetBool(v *bool) bool {
+ if v != nil {
+ return *v
+ }
+ return false
+}
+func GetInt(v *int) int {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetInt32(v *int32) int32 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetInt64(v *int64) int64 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetInt64Slice(v *[]int64) []int64 {
+ if v != nil {
+ return *v
+ }
+ return []int64{}
+}
+func GetUint32(v *uint32) uint32 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetUint64(v *uint64) uint64 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetFloat32(v *float32) float32 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetFloat64(v *float64) float64 {
+ if v != nil {
+ return *v
+ }
+ return 0
+}
+func GetString(v *string) string {
+ if v != nil {
+ return *v
+ }
+ return ""
+}
+func GetStringSlice(v *[]string) []string {
+ if v != nil {
+ return *v
+ }
+ return []string{}
+}
diff --git a/pub/common/random/number.go b/pub/common/random/number.go
new file mode 100644
index 0000000..af0d5c3
--- /dev/null
+++ b/pub/common/random/number.go
@@ -0,0 +1,53 @@
+package random
+
+import (
+ "fmt"
+ "math/rand"
+ "time"
+)
+
+const letterBytesAll = "0123456789abcdefghijklmnopqrstvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func init() {
+ rand.Seed(time.Now().Unix())
+}
+
+func Int(n int) int {
+ return rand.Intn(n)
+}
+
+func SliceRand(ids []int64) (result []int64) {
+ result = make([]int64, 0)
+ if len(ids) <= 0 {
+ return
+ }
+ copy := make([]int64, 0)
+ for _, id := range ids {
+ copy = append(copy, id)
+ }
+ num := Int(len(copy)) + 1
+ for i := 0; i < num; i++ {
+ index := Int(len(copy))
+ result = append(result, copy[index])
+ copy = append(copy[:index], copy[index+1:]...)
+ }
+ return
+}
+
+func RandDigits(n uint) string {
+ s := ""
+ for n != 0 {
+ s += fmt.Sprintf("%d", rand.Intn(10))
+ n--
+ }
+ return s
+}
+
+// RandStrRandrAll 生成指定位数的随机数
+func RandStrRandrAll(n int) string {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytesAll[rand.Int63()%int64(len(letterBytesAll))]
+ }
+ return string(b)
+}
diff --git a/pub/common/recomapi/define.go b/pub/common/recomapi/define.go
new file mode 100644
index 0000000..11969a4
--- /dev/null
+++ b/pub/common/recomapi/define.go
@@ -0,0 +1,42 @@
+package recomapi
+
+import "time"
+
+const (
+ FBT_Star string = "star"
+ FBT_Like string = "like"
+ FBT_Read string = "read"
+)
+
+type RSItemInfo struct {
+ Comment string `json:"Comment"`
+ ItemId string `json:"ItemId"`
+ Labels []string `json:"Labels"`
+ Timestamp time.Time `json:"Timestamp"`
+}
+
+type RSItemInfoList []*RSItemInfo
+
+type RSUserInfo struct {
+ Comment string `json:"Comment"`
+ UserId string `json:"UserId"`
+ Labels []string `json:"Labels"`
+ Subscribe []string `json:"Subscribe"`
+ Timestamp time.Time `json:"Timestamp"`
+}
+
+type RSUserInfoList []*RSUserInfo
+
+type APIResult struct {
+ RowAffected int64 `json:"RowAffected"`
+}
+
+type FeedbackInfo struct {
+ Comment string `json:"Comment"`
+ FeedbackType string `json:"FeedbackType"`
+ ItemId string `json:"ItemId"`
+ Timestamp time.Time `json:"Timestamp"`
+ UserId string `json:"UserId"`
+}
+
+type FeedbackInfoList []*FeedbackInfo
diff --git a/pub/common/recomapi/feedback.go b/pub/common/recomapi/feedback.go
new file mode 100644
index 0000000..c933a08
--- /dev/null
+++ b/pub/common/recomapi/feedback.go
@@ -0,0 +1,45 @@
+package recomapi
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+)
+
+// AddOneFeedback 添加一个推荐反馈
+func AddOneFeedback(url string, key string,
+ userId int64, mediaId int64, fbType string, comment string) (
+ int64, *srvdef.SrvErr) {
+ feedBack := &FeedbackInfo{
+ UserId: strconv.FormatInt(userId, 10),
+ ItemId: strconv.FormatInt(mediaId, 10),
+ FeedbackType: fbType,
+ Comment: comment,
+ Timestamp: time.Now(),
+ }
+ return AddManyFeedbacks(url, key, FeedbackInfoList{feedBack})
+}
+
+func AddManyFeedbacks(url string, key string, feedbacks FeedbackInfoList) (
+ int64, *srvdef.SrvErr) {
+ reqUrl := url + "/api/feedback"
+ resp := &APIResult{}
+ reqBody, _ := json.Marshal(feedbacks)
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.POSTJsonWithJResp(resp, reqUrl, header, reqBody)
+ if err != nil {
+ log.Error("recomapi:AddManyFeedbacks POSTJsonWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:AddManyFeedbacks POSTJsonWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ return resp.RowAffected, nil
+}
diff --git a/pub/common/recomapi/itemapi.go b/pub/common/recomapi/itemapi.go
new file mode 100644
index 0000000..51578a7
--- /dev/null
+++ b/pub/common/recomapi/itemapi.go
@@ -0,0 +1,106 @@
+package recomapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+)
+
+func InsertOneItem(url string, key string, itemInfo *RSItemInfo) *srvdef.SrvErr {
+ reqUrl := url + "/api/item"
+ resp := &APIResult{}
+ reqBody, _ := json.Marshal(itemInfo)
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.POSTJsonWithJResp(resp, reqUrl, header, reqBody)
+ if err != nil {
+ log.Error("recomapi:InsertOneItem POSTJsonWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:InsertOneItem POSTJsonWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ return nil
+}
+
+func InsertManyItems(url string, key string, itemInfos RSItemInfoList) *srvdef.SrvErr {
+ reqUrl := url + "/api/items"
+ reqBody, _ := json.Marshal(itemInfos)
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.POSTJsonWithJResp(nil, reqUrl, header, reqBody)
+ if err != nil {
+ log.Error("recomapi:InsertManyItems POSTJsonWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:InsertManyItems POSTJsonWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return srvdef.New(srvdef.Err, "", "")
+ }
+ return nil
+}
+
+func GetOneItem(url string, key string, itemId string) (*RSItemInfo, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/item/%s", url, itemId)
+ resp := &RSItemInfo{}
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.GetWithJResp(resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:GetOneItem GetWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:GetOneItem GetWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ log.Info("Test", log.Any("resp", resp))
+ return resp, nil
+}
+
+func GetManyItems(url string, key string, cursor string, size int64) (RSItemInfoList, string, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/items?n=%d&cursor=%s", url, size, cursor)
+ type Resp struct {
+ Cursor string `json:"Cursor"`
+ Items RSItemInfoList `json:"Items"`
+ }
+ var resp Resp
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.GetWithJResp(&resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:GetOneItem GetWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return nil, "", srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:GetOneItem GetWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return nil, "", srvdef.New(srvdef.Err, "", "")
+ }
+ return resp.Items, resp.Cursor, nil
+}
+
+func DeleteOneItem(url string, key string, itemId string) (int64, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/item/%s", url, itemId)
+ resp := &APIResult{}
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.DeleteWithJResp(resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:DeleteOneItem DeleteWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:DeleteOneItem DeleteWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return 0, srvdef.New(srvdef.Err, "", "")
+ }
+ return resp.RowAffected, nil
+}
diff --git a/pub/common/recomapi/recomapi.go b/pub/common/recomapi/recomapi.go
new file mode 100644
index 0000000..b4356a6
--- /dev/null
+++ b/pub/common/recomapi/recomapi.go
@@ -0,0 +1,36 @@
+package recomapi
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+)
+
+func GetRecomItems(url string, key string, userId int64, writeBack string, size int64) (
+ []int64, *srvdef.SrvErr) {
+ reqUrl := fmt.Sprintf("%s/api/recommend/%d?n=%d&write-back=%s", url, userId, size, writeBack)
+ var resp []string
+ header := map[string]string{"X-API-Key": key}
+ code, err := utils.GetWithJResp(&resp, reqUrl, header, nil)
+ if err != nil {
+ log.Error("recomapi:GetRecomItems GetWithJResp failed",
+ log.Any("reqUrl", reqUrl), log.E(err))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ if code != http.StatusOK {
+ log.Error("recomapi:GetRecomItems GetWithJResp code error.",
+ log.Any("reqUrl", reqUrl), log.Any("code", code))
+ return nil, srvdef.New(srvdef.Err, "", "")
+ }
+ itemIds := make([]int64, 0)
+ for _, v := range resp {
+ id, err := strconv.ParseInt(v, 10, 64)
+ if err == nil {
+ itemIds = append(itemIds, id)
+ }
+ }
+ return itemIds, nil
+}
diff --git a/pub/common/recomapi/recomapi_test.go b/pub/common/recomapi/recomapi_test.go
new file mode 100644
index 0000000..7bdccd1
--- /dev/null
+++ b/pub/common/recomapi/recomapi_test.go
@@ -0,0 +1,77 @@
+package recomapi
+
+import (
+ "fmt"
+ "testing"
+ "time"
+ "videoweb/pub/common/log"
+)
+
+var (
+ testUrl = "http://139.177.187.219:8087"
+ testKey = ""
+)
+
+func Test_InsertOneItem(t *testing.T) {
+ itemInfo := &RSItemInfo{
+ Comment: "Comment1",
+ ItemId: "10004",
+ Labels: []string{"lable1", "lable2", "lable3"},
+ Timestamp: time.Now(),
+ }
+ serr := InsertOneItem(testUrl, testKey, itemInfo)
+ fmt.Printf("\nTest_pingReq %s %v\n", itemInfo, serr)
+}
+
+func Test_InsertManyItems(t *testing.T) {
+ list := make(RSItemInfoList, 0)
+ itemInfo1 := &RSItemInfo{
+ Comment: "Comment1",
+ ItemId: "10005",
+ Labels: []string{"lable1", "lable2", "lable3"},
+ Timestamp: time.Now(),
+ }
+ list = append(list, itemInfo1)
+ itemInfo2 := &RSItemInfo{
+ Comment: "Comment1",
+ ItemId: "10006",
+ Labels: []string{"lable1", "lable2", "lable3"},
+ Timestamp: time.Now(),
+ }
+ list = append(list, itemInfo2)
+ serr := InsertManyItems(testUrl, testKey, list)
+ fmt.Printf("\nTest_InsertManyItems %v\n", serr)
+}
+
+func Test_GetOneItem(t *testing.T) {
+ itemInfo, serr := GetOneItem(testUrl, testKey, "10004")
+ fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfo, serr)
+}
+
+func Test_GetManyItem(t *testing.T) {
+ itemInfos, cursor, serr := GetManyItems(testUrl, testKey, "10003", 2)
+ log.Info("Test_GetManyItem", log.Any("itemInfos", itemInfos),
+ log.Any("cursor", cursor), log.E(serr))
+ //fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
+}
+
+func Test_DeleteOneItem(t *testing.T) {
+ count, serr := DeleteOneItem(testUrl, testKey, "10004")
+ log.Info("Test_DeleteOneItem", log.Any("count", count), log.E(serr))
+ //fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
+}
+
+//////////////////////////////////////////////////////////////
+//feedback
+
+func Test_AddOneFeedback(t *testing.T) {
+ count, serr := AddOneFeedback(testUrl, testKey, 18531, 16346, FBT_Star, "feedback1")
+ fmt.Printf("\nTest_pingReq %d %v\n", count, serr)
+}
+
+// /reccommend
+func Test_GetManyRecomItems(t *testing.T) {
+ itemIds, serr := GetRecomItems(testUrl, testKey, 18531, "test", 10)
+ log.Info("Test_GetManyRecomItems", log.Any("itemIds", itemIds), log.E(serr))
+ //fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
+}
diff --git a/pub/common/recomapi/usersapi.go b/pub/common/recomapi/usersapi.go
new file mode 100644
index 0000000..e8699bd
--- /dev/null
+++ b/pub/common/recomapi/usersapi.go
@@ -0,0 +1 @@
+package recomapi
diff --git a/pub/common/regapi.go b/pub/common/regapi.go
new file mode 100644
index 0000000..aba3d64
--- /dev/null
+++ b/pub/common/regapi.go
@@ -0,0 +1,26 @@
+package common
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// type FunHandle func(*gin.Context) (proto.Message, string, string, pkt_pub.EType)
+
+type ApiOpType int
+
+const (
+ ApiGet ApiOpType = 1
+ ApiPost ApiOpType = 2
+ ApiPut ApiOpType = 3
+)
+
+// API接口设置
+type ApiSet struct {
+ Path string // 路径
+ OpType ApiOpType // 操作类型
+ Fun gin.HandlerFunc // 函数句柄
+ IsWhite bool // 是否白名单
+ Desc string // 描述
+}
+
+var ApiSetMaps = map[string]ApiSet{}
diff --git a/pub/common/sms/fengniao/fengniao.go b/pub/common/sms/fengniao/fengniao.go
new file mode 100644
index 0000000..5d11c13
--- /dev/null
+++ b/pub/common/sms/fengniao/fengniao.go
@@ -0,0 +1,76 @@
+package fengniao
+
+import (
+ "crypto/sha1"
+ "errors"
+ "fmt"
+ "net/http"
+ "videoweb/pub/common/httputil"
+ "videoweb/pub/common/log"
+)
+
+type requestBody struct {
+ MercId string `json:"mercId"`
+ Secret string `json:"secret"`
+ TemplateId string `json:"templateId"`
+ Code string `json:"smsCode"`
+ Mobile string `json:"mobile"`
+ SignType string `json:"signType"` // 固定sha1
+ Sign string `json:"sign"` // 签名
+}
+
+type msg struct {
+ Status string `json:"status"`
+ TaskId string `json:"taskId"`
+}
+
+type responseBody struct {
+ Code int `json:"code"`
+ Msg msg `json:"msg"`
+ Err string `json:"err"`
+}
+
+type FengNiao struct {
+ Url string
+ MercId string
+ Secret string
+ Tmpl string
+}
+
+// Send 发送, 目前蜂鸟貌似只支持数字
+func (f *FengNiao) Send(mobile string, code string) error {
+ content := fmt.Sprintf(f.Tmpl, code)
+ data := fmt.Sprintf("mercId=%s&templateId=%s&smsCode=%s&mobile=%s&secret=%s", f.MercId, f.Tmpl, code, mobile, f.Secret)
+
+ sha := sha1.New()
+ _, err := sha.Write([]byte(data))
+ if err != nil {
+ log.Error("fengniao send error", log.Any("mobile", mobile), log.Any("content", content), log.E(err))
+ return err
+ }
+
+ body := &requestBody{
+ MercId: f.MercId,
+ Secret: f.Secret,
+ TemplateId: f.Tmpl,
+ Code: code,
+ Mobile: mobile,
+ SignType: "sha1",
+ Sign: fmt.Sprintf("%x", sha.Sum(nil)),
+ }
+ rp := responseBody{}
+ statusCode, err := httputil.Client().POSTJsonWithJResp(&rp, f.Url, nil, body)
+ if err != nil {
+ log.Error("fengniao POSTJsonWithResp err", log.E(err))
+ return err
+ }
+ if statusCode != http.StatusOK {
+ log.Error("fengniao status code not 200")
+ return errors.New("fengniao statusCode not 200")
+ }
+ if rp.Code != 200 {
+ log.Error("fengniao code not 200", log.Any("errdesc", rp.Err))
+ err = errors.New(rp.Err)
+ }
+ return err
+}
diff --git a/pub/common/sms/sms.go b/pub/common/sms/sms.go
new file mode 100644
index 0000000..e0c5514
--- /dev/null
+++ b/pub/common/sms/sms.go
@@ -0,0 +1,45 @@
+package sms
+
+import (
+ "strings"
+ "videoweb/pub/common/sms/fengniao"
+)
+
+type Provider = int
+
+const (
+ tmplID1 = "6029f19a99fd8b404062e4d2" // 大陆
+ tmplID2 = "5f8412c39396d16f787e561b" // 非大陆
+)
+
+const (
+ FengNiao Provider = 1 //蜂鸟
+ YunPian Provider = 2 //云片
+)
+
+func sendByFengNiao(mobile string, code string) error {
+ var tmplID string
+ if strings.HasPrefix(mobile, "+86") {
+ tmplID = tmplID1
+ return (&fengniao.FengNiao{
+ Url: "https://t9x6k.cc/api/sms/sendSms",
+ MercId: "10177",
+ Secret: "2dfb7ec44ed59ee3c1fe8dc533384e16",
+ Tmpl: tmplID,
+ }).Send(mobile, code)
+ } else {
+ tmplID = tmplID2
+ return (&fengniao.FengNiao{
+ Url: "https://t9x6k.cc/api/sms/sendSms",
+ MercId: "10228",
+ Secret: "3ea58a27a162dad4ed3e18c076648742",
+ Tmpl: tmplID,
+ }).Send(mobile, code)
+ }
+ // return
+}
+
+// Send 发送短信
+func Send(mobile string, code string) error {
+ return sendByFengNiao(mobile, code)
+}
diff --git a/pub/common/sms/sms_test.go b/pub/common/sms/sms_test.go
new file mode 100644
index 0000000..d0a80ba
--- /dev/null
+++ b/pub/common/sms/sms_test.go
@@ -0,0 +1,8 @@
+package sms
+
+import "testing"
+
+func TestSend(t *testing.T) {
+ err := Send("15008279990", "654321")
+ t.Log(err)
+}
diff --git a/pub/common/srvdef/codedef.go b/pub/common/srvdef/codedef.go
new file mode 100644
index 0000000..e56b9a4
--- /dev/null
+++ b/pub/common/srvdef/codedef.go
@@ -0,0 +1,328 @@
+package srvdef
+
+import "fmt"
+
+type CodeType int
+
+const NotFind = "mongo: no documents in result"
+
+// 错误级别
+const (
+ ParamLevel = 1000 // 参数级
+ StoreLevel = 3000 // 存储级
+ DBLevel = 5000 // 数据库
+ ServerLevel = 4000 // 服务级
+ BusinessLevel = 6000 // 业务逻辑级
+ NewErrorLevelV2 = 7000 // 新版错误
+ NewErrorLevelV3 = 8000 // 新版错误
+)
+
+// 通用code
+const (
+ Success CodeType = 200 //Success 通用成功
+ Failure CodeType = 400 //Failure 通用失败
+)
+
+func (c CodeType) Msg() string {
+ msg, ok := codeAndMsg[c]
+ if ok {
+ return msg
+ }
+ return ""
+}
+
+func (c CodeType) Tip() string {
+ tip, ok := codeAndTip[c]
+ if ok {
+ return tip
+ }
+ return ""
+}
+
+func (c CodeType) Error() string {
+ return fmt.Sprintf("code=%d,msg=%v,tip=%v", c, c.Msg(), c.Tip())
+}
+
+type ErrInfoer interface {
+ GetMsg() string
+ GetTip() string
+ GetCode() CodeType
+}
+
+// GetMsg alias of Msg()
+func (c CodeType) GetMsg() string { return c.Msg() }
+
+// GetTip alias of Tip()
+func (c CodeType) GetTip() string { return c.Tip() }
+func (c CodeType) GetCode() CodeType { return c }
+
+const (
+ InvalidToken CodeType = ParamLevel + iota // 框架错误
+ ErrParamError // 参数错误
+ NoToken // 无token
+ RoleErr // 角色错误
+ ReLogin // 重复登陆
+ UploadToFileSrvFailed // 文件上传失败
+ ErrCaptcha // 验证码为空
+ ErrCaptchaInvalid // 验证码非法
+ ErrCaptchaGet // 验证码获取失败
+ ErrCaptchaResend // 验证码重复发送
+ ErrCaptchaSendFail // 验证码发送失败
+ InvalidateToken // 非法token
+ NoUA // 没有UA信息
+ UploadFile // 文件上传失败
+ AlreadyRegisered // 已经组册
+ TokenRemoved // 你已经挤下线,请重新登陆
+ PostCommentNotUse //发送失败,服务器错误,请稍后再试
+ DBTransaction // 事务错误
+ ErrCrawlerUrl // 爬虫URL错误
+ ErrCrawler // 爬取数据错误
+)
+
+const (
+ DBQuery CodeType = StoreLevel + iota
+ DBInsert
+ DBUpdate
+ DBDelete
+ RedisSet // 设置redis错误
+ RedisGet // 获取redis值错误
+ RedisDel // 删除redis值错误
+ EtcdGet // 获取etcd错误
+ MongoDBQueryErr // 数据库查询失败
+)
+
+const (
+ ServerError CodeType = ServerLevel + iota // 用户相关错误
+)
+
+const (
+ _ CodeType = DBLevel + iota
+ ErrDbQueryError //ErrDbQueryError 数据库查询错误 5001
+ ErrDbInsertError //ErrDbInsertError 数据库插入错误 5002
+ ErrDbUpdateError //ErrDbUpdateError 数据库修改错误 5003
+ ErrDbDeleteError //ErrDbDeleteError 数据库删除错误 5004
+ ErrDbInputExist //ErrDbInputExist 数据库输入值已经存在 5005
+ ErrRdsOperError //ErrRdsOperError redis操作错误 5006
+ ErrElsOperError //ErrElsOperError redis操作错误 5007
+ ErrTransactionsError //ErrTransactionsError 数据库事务操作错误
+ ErrNoData //无数据
+)
+const (
+ ErrUserPermission CodeType = BusinessLevel + iota // 用户权限
+ ErrUserNotExsit // 用户不存在
+ ErrUserTypeNotUP // 用户不是UP主
+ AccountIsNotExists // 账户不存在
+ AccountErrAccessForbid // 账户禁止登陆
+ AccountErrPasswordError // 账户密码错误
+ GenerateTokenFailed // 生成二维码错误
+ ErrCardNotExists // 没有会员卡信息
+ ErrMadouCoinNotExists // 没有钻石信息
+ ErrMediaNotExsit // 视频不存在
+ ErrTopicInfoNotExsit // 视频专题不存在
+ ErrData // 数据错误
+ Err // 一般错误
+ ErrRchgOrderNotExist // 充值订单不存在
+ ErrSubmitRchgOrder // 发起充值订单错误
+ InvalidRequestReplayAttack // 非法请求
+ ErrBuyVIPCardFail // 购买会员卡失败
+ ErrOperationTooFrequently // 用户操作太频繁
+ ErrMediaSyncServer // 视频同步服务器错误
+ ErrUserType // 用户无改权限
+ DownloadM3u8File // 文件服务器访问失败
+ MediaSyncing // 视频同步中
+ MediaConnectServerFaild // 媒资库链接失败
+ AtressNotExist // 演员不存在
+ ErrAnnouncementNotExsit // 公告不存在
+ ErrContactDetailsNotExsit // 联系方式不存在
+ ErrPostCommentNotExsit // 评论不存在
+ HistoryNotExist // 记录不存在
+ ErrHotsearchNotExsit // 热搜不存在
+ ActivityDoesNotExist // 活动不存在
+ UserWatchNumNotEnough // 免费观看次数不足
+ MediaIsVip // 用户不是会员
+ MediaIsNeedPay // 视频需要付费
+ RepeatBuy // 重复购买
+ MovieTicketsNotExits // 观影券不足
+ MediaIsNotNeedPay // 视频不需要支付
+ PostNoNeed2Pay // 帖子不需要支付
+ SystemMsgNotExist // 消息不存在
+ ForbidlogNotExist // 封禁消息不存在
+ AttachmentDoesNotExist // 导出记录
+ UserDownloadNotEnough // 下载次数不足
+ UserDownloadNeedPay // 付费视频需要先购买
+ RedeemCodeNotExists // 兑换码不存在
+ RedeemCodeTimesUsedUp // 兑换码次数使用完了
+ RedeemCodeAlreadyUsed // 兑换码你已经使用过兑换码了
+ RedeemCodeRecordNotExists // 兑换记录不存在
+ MediaIsNotNeedPreSale // 视频不支持预售
+ MediaIsPreSale // 预售视频
+ QRcodeError // 二维码错误
+ ModifyUserNameError // 修改
+ ErrSkdRecordNotExsit // 任务不存在
+ ErrContainIllegalWord // 包含非法字符
+ DatingOrderNotExist // 约会购买记录不存在
+ DatingHasBuy // 重复购买
+ DatingCollectNotExist // 收藏记录不存在
+ DatingBossNotExist // 经纪人不存在
+ DatingReportNotExist // 验证报告不存在
+ DatingReportDouble // 重复验证
+ DatingAlreadyComplaint // 已经提交举报
+ SortNotExist // 视频排序不存在
+ MessageDialogNotExist // 私信对话框不存在
+ VideoUploadFaild // 文件上传失败
+ PosoStatusFaild // 帖子状态错误
+ WtdrFrequencyLimits // 提现频率限制
+ WtdrErroTip1 // 提现不是100整数倍
+ WtdrErroTip2 // 赠送3元金额要满500才能提现
+ WtdrErroTip3 // 请填写银行名字或升级App版本
+ GameWtdrErroTip7 // 银行卡必须与您以前的提现卡一致,如需更改请联系客服
+ ProxyUserNotExist // 代理不存在
+ ErrPostReportNotExsit // 举报信息不存在
+ FileUploadDouble // 文件重复上传
+ UserRepeatSubmitRchgOrder // 未支付订单太多,有问题请联系客服
+ Limiting // 限流
+ ErrIpForbid // Ip禁止
+ DeviceIdInvalid // 设备id无效
+ UserScoreNotEnough // 用户积分不足
+ UserSocreLVLimit // 用户积分等级限制
+ UploaderNeedActive // Up主没有激活
+ UploaderNeedBind // 后台用户没有up主绑定
+ PostCollectionNotExists // 图文专辑不存在
+ MediaCollectionNotExist // 该合集或选集不存在
+ ContestNotExists // 该比赛不存在
+ CannotJoinContest // 该比赛无法参加
+ ContestIsVip // 该比赛只限VIP参加
+ AppInfoNotExist // 该App不存在
+ MediaNotVerified // 视频未上架
+ PCCategoryUnmatch // 专辑的分类和作品的分类不一致
+ ErrMediaRecommendNotExsit // 推荐不存在
+ DatingBuyHistoryNotExist // 购买记录不存在
+ DatingWatchHistoryNotExist // 观看记录不存在
+ CollectIsExist // 收藏已经存在
+ CollectNotExist // 收藏不存在
+ DiscussRankDoesNotExist // 话题视频排名
+ DiscussActivityDoesNotExist // 话题活动不存在
+ UpDayStatDoesNotExist // 创作者收益
+ DiscussObjectRankDoesNotExist // 话题视频
+ DiscussCategoryDoesNotExist // 话题分类不存在
+ ErrCareNotExist // 取消关注对象不存在
+ ErrUpMasterTaskNotCompleted // up主申请任务未完成
+ HGameNotExist // 游戏不存在
+ HGameIsVip // 游戏需要会员
+ HGameIsNeedPay // 游戏需要付费
+ ErrApplyUpIsVip // UP主申请需要vip
+ UserLevelCardNotExist // 用户等级卡不存在
+ UserCheckinSetupNotExist // 签到配置不存在
+ PriceMediaNotCancel // 钻石视频不能下架
+ PriceMediaHasBuy // 钻石视频已经被购买
+ GiftWeekIsExist // 礼包已领取
+ UserLevelPointTooLow // 用户等级积分太低
+ UserGiftHaveReceive // 任务奖励已经领取
+ UserGiftExistNoReceive // 没有领取资格
+ DatingUpMasterNotExist // up主不存在
+ UserIsUpMaster // 用户已经是up主
+ UserUpCerting // up主认证中
+ MissionSetupNotExist // 任务配置不存在
+ UserIsUpMasterBind // 用户已被up主绑定
+ ErrCareIsExist // 不可重复关注
+ ObjectDayStatDoesNotExist // 作品统计记录不存在
+ ObjectDayViewIncStatDoesNotExist // 作品增量记录不存在
+ UserNotEnoughFreeLotteryCount // 免费抽奖次数不足
+ PrizeOutOfStore // 奖品库存不足
+ MediaSubscribedNotNeedPay // 订阅的视频不需要支付
+ DatingComplaintNotExist // 举报帖子不存在
+ DatingUserNotExist // 楼凤不存在
+ UserDauInfoNotExist // 搜寻日期没有资料
+ ParseJsonError // 数据解析错误
+ AlreadyRefund // 已退款
+ AiCountTypeNotSupport // ai内容不支持
+ TodayPontNotExist // 积分记录不存在
+ RedeemPrizeHistoryNotExist // 奖品兑换记录不存在
+ PrizeNotExist // 奖品不存在
+ ErrUserPointsNotExist // 积分不足
+ AIFreeCountNotEnough // ai免费次使用数不足
+ AIFreeCountNOtSupportRefund // AI免费使用次数不支持退款
+ CrowdfundStatusFail // 众筹女神的状态不在可修改的范围内
+ CrowdfundNotOpenPrizeFail // 众筹女神的订单开奖失败
+ CrowdfundPrizeNumberError // 众筹女神的开奖号码不对
+ RedeemFail // 积分兑换失败
+ CrowdfundNotBuy // 当前女神状态不能购买
+ CrowdfundNumberHaveBuy // 这个号码已经被其他人购买了
+ UserCheckinDisabled // 用户签到功能已关闭
+ PrizeTypeUnsupported // 奖品类型不支持自动发放
+ CouponExpired // 卡券已过期
+ CouponUsed // 卡券已被使用
+ CouponNotFound // 没有卡券信息
+ JumpTabNotExist // 首页跳转tab不存在
+ CouponCanNotUse // 卡券无法使用
+ SysConfigError //系统配置错误
+ ComicsCateNotExist // 漫画分类不存在
+ ComicsTagNotExist // 漫画标签不存在
+ ComicsTopicNotExist // 漫画主题不存在
+ ErrComicsChapterNotExist // 漫画章节不存在
+ ComicsNotExist // 漫画不存在
+ ComicsCVNotExist // 漫画CV不存在
+ ComicsMediaNotExist // 漫画视频不存在
+ ComicsRankNotExist // 排行榜不存在
+ VomicsTopicNotExist // 有声漫不存在
+ ComicsIsBlack // 漫画在黑名单中
+ CommentTextIsEmpty // 评论内容为空
+ ComicsIsVip // 漫画需要会员
+ ComicsIsNeedPay // 漫画需要付费
+ ComicsChapterExist // 漫画章节已存在
+ ObjectNotExist // 数据不存在
+ GameWtdrErroTip3 // 请填写银行名字或升级App版本
+ UserDoTaskErr // 用户任务出错
+ GetPrizeFail // 获取奖品失败
+ NovelIsNeedPay // 小说需要付费
+ NovelIsVip // 小说需要vip
+ LotteryActivityNotEnable // 抽奖活动没有启用
+ CurrTimeNotActivity // 当前时间没有活动
+ AIOrderStripsCountLimit // 今天AI脱衣提交次数达到上限
+ NotWtdrChannel // 没有匹配到提现通过
+ ExcelExportErr // 文件导出错误
+ ComicsNotJoinActivity // 这本漫画没有参与活动
+ NoGiftToReceive // 没有奖品可领取
+ UserRepeatDrawDiscountCard // 用户重复翻牌
+ NoUseDiscountCard // 不可使用折扣卡
+ AIOrderNotDel // 未完成的订单不能删除
+ AIWaitOrderTooMuch // 排队中的AI订单太多,请稍后再试
+ ActorNotExist // 演员不存在
+ MediaFSServerHandleIng // 文件服务器处理中
+ VideoRepeat // 视频重复
+ ErrReqForbidden // 请求被拒
+ APPIDError // APPID异常
+)
+const (
+ ErrRecharge CodeType = iota + 18000 // 充值相关错误
+ ErrGetRechargeType // 充值方式获取错误
+ UserPhoneNotBind // 手机号码未注册
+ PostCommentPermissionDenied // 禁止评论
+ UserLoginPasswordErr // 密码错误
+ ErrMediaRecordNotExsit // 视频记录不存在
+ ErrThressServerHttpErr // 第三方服务错误
+ UserPhoneBeenBound // 手机号码已被绑定
+ WeekCommentNumNotEnough // 评论条数不足
+ ErrAccessForbid //ErrAccessForbid 没有访问权限 1000
+ MediaAlreadyInEpisode // 该视频已经有选集
+ MediaNotInEpisode // 选集中不包含该视频
+ MediaCollectNotNull // 合集视频不为空
+)
+
+const (
+ //
+ CircleErrCode CodeType = iota + 20000 // 圈子错误码 20000
+ PublistNotVIPPermissionDenied // 不是会员禁止发布
+ PublistContentErrDenied // 包含敏感词汇
+ PostRepeatBuy // 帖子重复购买
+ UserNotEnoughMoney // 用户的钻石数量不足
+ PostStatusError // 订单状态错误
+ PostNotAllowComplaint // 不允许举报
+ UserNotEnoughScore // 用户的钻石数量不足
+ ErrUserAccExist // 账号已存在
+ UserAccountNotBind // 账号未注册
+ VerifyOKPostCountLimit //上架的帖子数量达到限制
+ ErrUserBeBanned //ErrUserBeBanned 用户已经禁言 1004
+ ErrCustomerBanned //ErrCustomerBanned 客服已休息 1005
+ ErrVersionUpdate //ErrVersionUpdate 版本强制更新 1006
+)
\ No newline at end of file
diff --git a/pub/common/srvdef/msgdef.go b/pub/common/srvdef/msgdef.go
new file mode 100644
index 0000000..98bcf53
--- /dev/null
+++ b/pub/common/srvdef/msgdef.go
@@ -0,0 +1,58 @@
+package srvdef
+
+// error tips
+const (
+ Ok = "成功"
+ //基础错误提示
+ ET_Store = "存储错误"
+ ET_DBOp = "DB操作错误"
+ ET_DBUpdate = "DB更新失败"
+ ET_RdsOp = "Rds操作错误"
+ ET_ESOp = "ES操作错误"
+ ET_Param = "参数错误"
+ ET_NoDocment = "无数据不存在"
+ ET_NoCache = "缓存数据不存在"
+ ET_MoneyNotEnough = "余额不足"
+ //----------------------------------------------
+ ET_UserNoExist = "用户不存在"
+ ET_IsntBlogger = "用户不是博主"
+ ET_BloggerApply = "博主申请条件不满足"
+ ET_BloggerAlreadyIs = "用户已经是博主"
+ ET_VipLevel2Low = "用户的VIP等级不够"
+ //----------------------care------------------------
+ ET_StatusEqual = "已经是请求状态"
+ ET_AlreadyCare = "已关注"
+ ET_AlreadyJoinFG = "您已加入了该粉丝团"
+)
+
+var codeAndMsg = map[CodeType]string{
+ Success: "success",
+ Failure: "Failed",
+ ErrDbQueryError: "database query error",
+ ErrDbInsertError: "database insert error",
+ ErrDbUpdateError: "database update error",
+ ErrDbDeleteError: "database delete error",
+ ErrDbInputExist: "current input has already exists",
+ ErrRdsOperError: "redis operate error",
+ ErrElsOperError: "elastic operate error",
+ BusinessLevel: "business error",
+ ErrCardNotExists: "card does not exist",
+ ErrMadouCoinNotExists: "madou coin does not exist",
+ ErrGetRechargeType: "get recharge type error",
+ ErrRchgOrderNotExist: "recharge order does not exist",
+ ErrBuyVIPCardFail: "buy vip card failed",
+ ErrOperationTooFrequently: "user operation is too frequent",
+ UserNotEnoughMoney: "user's coin is not enough",
+ RedeemCodeNotExists: "code does not exist",
+ RedeemCodeTimesUsedUp: "code times used up",
+ RedeemCodeAlreadyUsed: "you have used this code",
+ RedeemCodeRecordNotExists: "code use record does not exist",
+ ErrNoData: "no data",
+ UserPhoneBeenBound: "phone number has been bound",
+ WtdrFrequencyLimits: "wtdr frequency limits,please try again later",
+ MediaIsNeedPay: "You have to buy the video.",
+ UserWatchNumNotEnough: "More than three times",
+ UserScoreNotEnough: "user score is not enough",
+ UserSocreLVLimit: "user score level limit",
+ AlreadyRefund: "already refund",
+}
diff --git a/pub/common/srvdef/srvdef.go b/pub/common/srvdef/srvdef.go
new file mode 100644
index 0000000..70382d4
--- /dev/null
+++ b/pub/common/srvdef/srvdef.go
@@ -0,0 +1,67 @@
+package srvdef
+
+import (
+ "fmt"
+)
+
+// SrvErr 代替/common/error中的定义,逐渐在代码中弃掉(common/error模块)
+type SrvErr struct {
+ Code CodeType `json:"code"`
+ Msg string `json:"msg"`
+ Tips string `json:"tips"`
+}
+
+// New create an error
+func New(code CodeType, msgAndTips ...string) *SrvErr {
+ msg := code.Msg()
+ tips := code.Tip()
+ if len(msgAndTips) == 1 {
+ msg = msgAndTips[0]
+ } else if len(msgAndTips) == 2 {
+ msg, tips = msgAndTips[0], msgAndTips[1]
+ }
+ return &SrvErr{
+ Code: code,
+ Msg: msg,
+ Tips: tips,
+ }
+}
+
+// SrvErr 返回字符串
+func (s *SrvErr) Error() string {
+ if s == nil {
+ return ""
+ }
+ return fmt.Sprintf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips)
+}
+
+// SrvErr 返回error
+func (s *SrvErr) ToError() error {
+ if s == nil {
+ return nil
+ }
+ return fmt.Errorf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips)
+}
+
+func (c *SrvErr) GetMsg() string {
+ // 有自定义的msg就用自定义的msg
+ if c.Msg != "" {
+ return c.Msg
+ }
+ return c.Code.Msg()
+}
+func (c *SrvErr) GetTip() string {
+ // 有自定义的tip就用自定义的tip
+ if c.Tips != "" {
+ return c.Tips
+ }
+ return c.Code.Tip()
+}
+func (c *SrvErr) GetCode() CodeType { return c.Code }
+
+var (
+ ErrDbInsert = New(ErrDbInsertError, "", "")
+ ErrDbQuery = New(ErrDbQueryError, "", "")
+ ErrDbUpdate = New(ErrDbUpdateError, "", "")
+ ErrDbDelete = New(ErrDbDeleteError, "", "")
+)
diff --git a/pub/common/srvdef/tipdef.go b/pub/common/srvdef/tipdef.go
new file mode 100644
index 0000000..5a849f1
--- /dev/null
+++ b/pub/common/srvdef/tipdef.go
@@ -0,0 +1,162 @@
+package srvdef
+
+var codeAndTip = map[CodeType]string{
+ Success: "成功",
+ Failure: "失敗",
+ ErrCardNotExists: "沒有會員卡信息",
+ ErrMadouCoinNotExists: "沒有金幣信息",
+ ErrGetRechargeType: "充值方式獲取錯誤",
+ ErrRchgOrderNotExist: "充值訂單不存在",
+ ErrBuyVIPCardFail: "購買會員卡失敗",
+ ErrOperationTooFrequently: "用戶操作太頻繁",
+ UserNotEnoughMoney: "用户余额不足",
+ UserPhoneNotBind: "手机号码未注册",
+ PostCommentPermissionDenied: "非会员禁止发贴和评论",
+ UserLoginPasswordErr: "密码错误",
+ ErrUserTypeNotUP: "用户不是UP主",
+ AccountIsNotExists: "账户不存在",
+ AccountErrAccessForbid: "账户禁止登陆",
+ AccountErrPasswordError: "账户密码错误",
+ GenerateTokenFailed: "生成二维码错误",
+ ErrMediaNotExsit: "视频不存在",
+ ErrTopicInfoNotExsit: "视频专题不存在",
+ ErrData: "数据错误",
+ Err: "一般错误",
+ ErrSubmitRchgOrder: "发起充值订单错误",
+ InvalidRequestReplayAttack: "非法请求",
+ ErrMediaSyncServer: "视频同步服务器错误",
+ ErrUserType: "用户无改权限",
+ DownloadM3u8File: "文件服务器访问失败",
+ MediaSyncing: "视频同步中",
+ MediaConnectServerFaild: "媒资库链接失败",
+ AtressNotExist: "演员不存在",
+ ErrAnnouncementNotExsit: "公告不存在",
+ ErrContactDetailsNotExsit: "联系方式不存在",
+ ErrPostCommentNotExsit: "评论不存在",
+ HistoryNotExist: "记录不存在",
+ ErrHotsearchNotExsit: "热搜不存在",
+ ActivityDoesNotExist: "活动不存在",
+ UserWatchNumNotEnough: "免费观看次数不足",
+ MediaIsVip: "用户不是会员",
+ MediaIsNeedPay: "视频需要付费",
+ RepeatBuy: "重复购买",
+ MovieTicketsNotExits: "观影券不足",
+ MediaIsNotNeedPay: "视频不需要支付",
+ PostNoNeed2Pay: "帖子不需要支付",
+ SystemMsgNotExist: "消息不存在",
+ ForbidlogNotExist: "封禁消息不存在",
+ AttachmentDoesNotExist: "导出记录",
+ UserDownloadNotEnough: "下载次数不足",
+ UserDownloadNeedPay: "付费视频需要先购买",
+ ErrNoData: "无数据",
+ ModifyUserNameError: "昵称!每月只能修改一次",
+ RedeemCodeTimesUsedUp: "該兌換碼已使用",
+ RedeemCodeNotExists: "兌換碼已使用不存在",
+ RedeemCodeAlreadyUsed: "該兌換碼已使用",
+ InvalidToken: "框架错误",
+ ErrParamError: "参数错误",
+ NoToken: "无token",
+ RoleErr: "角色错误",
+ ReLogin: "重复登陆",
+ UploadToFileSrvFailed: "文件上传失败",
+ ErrCaptcha: "验证码为空",
+ ErrCaptchaInvalid: "验证码非法",
+ ErrCaptchaGet: "验证码获取失败",
+ ErrCaptchaResend: "验证码重复发送",
+ ErrCaptchaSendFail: "验证码发送失败",
+ InvalidateToken: "非法token",
+ NoUA: "没有UA信息",
+ UploadFile: "文件上传失败",
+ RedisSet: "设置redis错误",
+ RedisGet: "获取redis值错误",
+ RedisDel: "删除redis值错误",
+ EtcdGet: "获取etcd错误",
+ MongoDBQueryErr: "数据库查询失败",
+ ErrDbQueryError: "数据库查询错误",
+ ErrDbInsertError: "数据库插入错误",
+ ErrDbUpdateError: "数据库修改错误",
+ ErrDbDeleteError: "数据库删除错误",
+ ErrDbInputExist: "数据库输入值已经存在",
+ ErrRdsOperError: "redis操作错误",
+ ErrElsOperError: "redis操作错误",
+ ErrTransactionsError: "数据库事务操作错误",
+ ErrUserPermission: "用户权限",
+ ErrUserNotExsit: "用户不存在",
+ RedeemCodeRecordNotExists: "兑换记录不存在",
+ MediaIsNotNeedPreSale: "视频不支持预售",
+ MediaIsPreSale: "预售视频",
+ QRcodeError: "二维码错误",
+ ErrRecharge: "充值相关错误",
+ ErrMediaRecordNotExsit: "视频记录不存在",
+ UserPhoneBeenBound: "手机号码已被绑定",
+ DatingAlreadyComplaint: "已经举报过",
+ WtdrFrequencyLimits: "提现操作频率限制,请稍后再试",
+ WtdrErroTip1: "提现数额必须是100的整数倍",
+ WtdrErroTip2: "赠送6元体验金,满500可兑换钻石",
+ WtdrErroTip3: "请填写银行名字或升级App版本",
+ GameWtdrErroTip7: "银行卡必须与您以前的提现卡一致,如需更改请联系客服",
+ FileUploadDouble: "文件重复上传",
+ UserRepeatSubmitRchgOrder: "未支付订单太多,有问题请联系客服",
+ ErrIpForbid: "IP被禁止",
+ UserScoreNotEnough: "用户积分不足",
+ UserSocreLVLimit: "用户积分等级限制",
+ UploaderNeedBind: "后台用户没有up主绑定",
+ UploaderNeedActive: "Up主没有激活",
+ PostCollectionNotExists: "图文专辑不存在",
+ MediaCollectionNotExist: "该合集或选集不存在",
+ ContestNotExists: "该比赛不存在",
+ CannotJoinContest: "该比赛无法参加",
+ ContestIsVip: "该比赛只限VIP参加",
+ MediaNotVerified: "视频未上架",
+ PCCategoryUnmatch: "专辑的分类和作品的分类不一致",
+ ErrCareNotExist: "取消关注对象不存在",
+ ErrUpMasterTaskNotCompleted: "up主申请任务未完成",
+ HGameIsVip: "游戏需要会员",
+ HGameIsNeedPay: "游戏需要付费",
+ HGameNotExist: "游戏不存在",
+ ErrApplyUpIsVip: "UP主申请需要vip",
+ UserLevelCardNotExist: "用户等级卡不存在",
+ UserCheckinSetupNotExist: "签到配置不存在",
+ GiftWeekIsExist: "礼包已领取",
+ UserLevelPointTooLow: "用户等级积分太低",
+ UserGiftHaveReceive: "任务奖励已领取",
+ UserGiftExistNoReceive: "没有领取资格",
+ DatingUpMasterNotExist: "up主不存在",
+ UserIsUpMaster: "用户已经是up主",
+ UserUpCerting: "up主认证中",
+ MissionSetupNotExist: "任务配置不存在",
+ UserIsUpMasterBind: "用户已被up主绑定",
+ ErrCareIsExist: "不可重复关注",
+ ObjectDayStatDoesNotExist: "作品统计记录不存在",
+ ObjectDayViewIncStatDoesNotExist: "作品增量记录不存在",
+ UserNotEnoughFreeLotteryCount: "免费抽奖次数不足",
+ PrizeOutOfStore: "奖品库存不足",
+ MediaSubscribedNotNeedPay: "订阅的视频无需支付",
+ DatingComplaintNotExist: "举报帖子不存在",
+ DatingUserNotExist: "楼凤不存在",
+ WeekCommentNumNotEnough: "评论条数不足",
+ UserDauInfoNotExist: "搜寻日期没有资料",
+ PostCommentNotUse: "发送失败,服务器错误,请稍后再试",
+ AlreadyRefund: "已退款",
+ ErrUserPointsNotExist: "积分不足",
+ RedeemFail: "积分兑换失败",
+ CrowdfundNumberHaveBuy: "购买的号码已经出售",
+ CouponExpired: "卡券已过期",
+ CouponUsed: "卡券已使用",
+ CouponNotFound: "没有卡券信息",
+ VerifyOKPostCountLimit: "上架的帖子数量达到限制",
+ CouponCanNotUse: "卡券无法使用",
+ LotteryActivityNotEnable: "抽奖活动没有启用",
+ CurrTimeNotActivity: "当前时间没有活动",
+ AIOrderStripsCountLimit: "今天AI脱衣提交次数达到上限",
+ NotWtdrChannel: "没有匹配到提现通过",
+ ComicsNotJoinActivity: "这本漫画没有参与活动",
+ UserRepeatDrawDiscountCard: "重复翻牌",
+ NoUseDiscountCard: "不可使用折扣卡",
+ ErrUserAccExist: "账号已存在",
+ AIOrderNotDel: "未完成的订单不能删除",
+ AIWaitOrderTooMuch: "排队中的AI订单太多,请稍后再试",
+ ActorNotExist: "演员不存在",
+ VideoRepeat: "视频重复",
+ ErrReqForbidden: "请求被拒",
+}
\ No newline at end of file
diff --git a/pub/common/srvversion/version.go b/pub/common/srvversion/version.go
new file mode 100644
index 0000000..3cafe3a
--- /dev/null
+++ b/pub/common/srvversion/version.go
@@ -0,0 +1,46 @@
+package srvversion
+
+import (
+ "fmt"
+ "os"
+ //"os"
+)
+
+var (
+ //每次编译时需要手动修改本字段,否则每次编译得到的服务版本号都会是默认值
+ SERVICE_VERSION = "V1.001.T"
+
+ //以下几个字段编译期间自动替换,无需手动操作
+ GIT_BRANCH = "GIT_BRANCH_FLAG"
+ GIT_VERSION = "GIT_VERSION_FLAG"
+ BUILD_DATE = "BUILD_DATE_FLAG"
+ BUILD_IP = "BUILD_IP_FLAG"
+)
+
+func Info() {
+ fmt.Printf("version:%s\n", SERVICE_VERSION)
+ fmt.Printf("git branch:%s\n", GIT_BRANCH)
+ fmt.Printf("git version:%s\n", GIT_VERSION)
+ fmt.Printf("build date:%s\n", BUILD_DATE)
+ fmt.Printf("build ip:%s\n", BUILD_IP)
+
+ os.Exit(0) //退出进程
+}
+
+func GetVersion() *Version {
+ return &Version{
+ ServiceVer: SERVICE_VERSION,
+ GitVer: GIT_VERSION,
+ BuildDate: BUILD_DATE,
+ BuildIp: BUILD_IP,
+ Branch: GIT_BRANCH,
+ }
+}
+
+type Version struct {
+ ServiceVer string `json:"serviceVer"`
+ GitVer string `json:"gitVer"`
+ BuildDate string `json:"buildDate"`
+ BuildIp string `json:"buildIp"`
+ Branch string `json:"branch"`
+}
diff --git a/pub/common/swaggerutil/swagger.go b/pub/common/swaggerutil/swagger.go
new file mode 100644
index 0000000..741f3bd
--- /dev/null
+++ b/pub/common/swaggerutil/swagger.go
@@ -0,0 +1,27 @@
+package swaggerutil
+
+import (
+ "fmt"
+ "videoweb/pub/common/netutil"
+
+ "github.com/gin-gonic/gin"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+)
+
+func isDevHost(runMode string, svcListenIp string) bool {
+ localIp := netutil.GetLocalIP()
+ return (runMode == "dev" || runMode == "debug") && localIp != svcListenIp
+}
+
+func SetupGinSwagger(runMode, svcListenIp, svcListenPort string) func(r *gin.Engine) {
+ return func(r *gin.Engine) {
+ if isDevHost(runMode, svcListenIp) {
+ url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", netutil.GetLocalIP(), svcListenPort))
+ r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
+ } else {
+ url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", svcListenIp, svcListenPort))
+ r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
+ }
+ }
+}
diff --git a/pub/common/synclock/lock.go b/pub/common/synclock/lock.go
new file mode 100644
index 0000000..9245e70
--- /dev/null
+++ b/pub/common/synclock/lock.go
@@ -0,0 +1,102 @@
+package synclock
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/redis"
+)
+
+const (
+ UserExtSyncLockFmt = "userExtSyncLock:%d" // 用户账户扩展信息
+ UserExtSyncLockExpire = 5 * time.Minute // 用户账户扩展信息 过期时间
+
+ UserAccountLockFmt = "UserAccountLockFmt:%d" // 用户账户
+ UserAccountLockExpire = 5 * time.Minute // 用户账户 过期时间
+ TraderAccountLockFmt = "TraderAccountLockFmt:%s" //商人账户
+ SpinLockExpire = 3 * time.Second
+)
+
+// UserExtSyncLock 通过redis按照用户ID加锁
+func UserExtcLock(userId int64) bool {
+ v, e := redis.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire)
+ if !v || e != nil {
+ return false
+ }
+
+ return true
+}
+
+// UserExtSpinLock 通过redis按照用户ID加锁
+func UserExtSpinLock(userId int64, lockTimeOut time.Duration) (bool, error) {
+ endTime := time.Now().Add(lockTimeOut)
+ for time.Now().Before(endTime) {
+ value, err := redis.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire)
+ if err != nil {
+ return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
+ }
+ if value {
+ return true, nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+
+ return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
+}
+
+// UserExtUnlock 按照用户ID释放锁
+func UserExtUnlock(userId int64) {
+ redis.Del(fmt.Sprintf(UserExtSyncLockFmt, userId))
+}
+
+// UserAccountLock 通过redis按照用户ID加锁
+func UserAccountLock(userId int64) bool {
+ v, e := redis.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire)
+ if !v || e != nil {
+ return false
+ }
+
+ return true
+}
+
+// UserAccountSpinLock 通过redis按照用户ID加锁
+func UserAccountSpinLock(userId int64, lockTimeOut time.Duration) (bool, error) {
+ endTime := time.Now().Add(lockTimeOut)
+ for time.Now().Before(endTime) {
+ value, err := redis.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire)
+ if err != nil {
+ return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
+ }
+ if value {
+ return true, nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+ return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
+}
+
+// 通过redis按照商人ID加锁
+func TraderAccountSpinLock(userId string, lockTimeOut time.Duration) (bool, error) {
+ endTime := time.Now().Add(lockTimeOut)
+ for time.Now().Before(endTime) {
+ value, err := redis.Setnx_NewOK(fmt.Sprintf(TraderAccountLockFmt, userId), true, SpinLockExpire)
+ if err != nil {
+ return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
+ }
+ if value {
+ return true, nil
+ }
+ time.Sleep(time.Millisecond)
+ }
+ return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
+}
+
+// 按照商人ID释放锁
+func TraderAccountUnlock(userId string) {
+ redis.Del(fmt.Sprintf(TraderAccountLockFmt, userId))
+}
+
+// UserAccountUnlock 按照用户ID释放锁
+func UserAccountUnlock(userId int64) {
+ redis.Del(fmt.Sprintf(UserAccountLockFmt, userId))
+}
diff --git a/pub/common/tgbot/helper.go b/pub/common/tgbot/helper.go
new file mode 100644
index 0000000..58ce1de
--- /dev/null
+++ b/pub/common/tgbot/helper.go
@@ -0,0 +1,94 @@
+package tgbot
+
+import (
+ "videoweb/pub/common/srvdef"
+)
+
+//获取群聊id
+//https://api.telegram.org/bot1803190372:AAGmrrN6xUoh7IKiv4qCVJYdCs8wLt1ffCw/getUpdates
+
+var (
+ //配置
+ //zhihu app机器人 2048234434:AAF3XoW4iE6vAOmttTT_bofgan_PleKuaEk
+ //zhihu web机器人 1973289619:AAHjCZ8KQOewVfr20foHKlKVwJoulrf_OFc
+ tgToken = "2048234434:AAF3XoW4iE6vAOmttTT_bofgan_PleKuaEk"
+ //程序支付监控群tgids
+ programTgGroupIds = []int64{-778146822}
+ //天行监控组tgids
+ monitorTgGroupIds = []int64{-1001373963166}
+ //管理人员tgid(yl:1652126945)
+ adminTgIds = []int64{1652126945}
+ prefix = "[汁乎App]"
+ tgBot *TgBot = nil
+)
+
+func InitTgBot(token string, prefixStr string) *srvdef.SrvErr {
+ if tgBot != nil {
+ return nil
+ }
+ if prefixStr != "" {
+ prefix = prefixStr
+ }
+ tgBot = NewTelegramBot(token)
+ if serr := tgBot.Init(); nil != serr {
+ return serr
+ }
+
+ //SendToAll("支付监控测试", "Test")
+ return nil
+}
+
+func ReleaseTgBot() {
+ if tgBot == nil {
+ return
+ }
+ tgBot.Release()
+}
+
+// SendToAdmins 发到管理人员
+func SendToAdmins(title string, info string) {
+ sendTo(adminTgIds, title, info)
+}
+
+// SendToProgram 发到程序
+func SendToProgram(title string, info string) {
+ sendTo(programTgGroupIds, title, info)
+}
+
+// SendToMonitor 发到监控群组
+func SendToMonitor(title string, info string) {
+ sendTo(monitorTgGroupIds, title, info)
+}
+
+// SendToProgram 发到所有
+func SendToAll(title string, info string) {
+ groupIds := programTgGroupIds
+ groupIds = append(groupIds, monitorTgGroupIds...)
+ //groupIds = append(groupIds, adminTgIds...)
+ sendTo(groupIds, title, info)
+}
+
+func sendTo(ids []int64, title string, info string) {
+ if tgBot == nil {
+ return
+ }
+ //异步发送
+ go func() {
+ msg := prefix + "\n" + title + "\n" + info
+ tgBot.Send(ids, msg)
+ }()
+}
+
+func RegisterCommands(commands []*BotCmd) {
+ if tgBot == nil {
+ return
+ }
+ tgBot.RegisterCommands(commands)
+}
+
+func Receive() {
+ if tgBot == nil {
+ return
+ }
+ tgBot.Receive()
+}
diff --git a/pub/common/tgbot/tgbot.go b/pub/common/tgbot/tgbot.go
new file mode 100644
index 0000000..de5989c
--- /dev/null
+++ b/pub/common/tgbot/tgbot.go
@@ -0,0 +1,169 @@
+package tgbot
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+type RSP_TYPE int
+
+const (
+ TextType RSP_TYPE = 0
+ FileType RSP_TYPE = 1
+)
+
+type TgCmdRsp struct {
+ Buf []byte
+ BufType RSP_TYPE
+}
+
+// TgBot Telegram机器人.
+type TgBot struct {
+ // 机器人标示;
+ token string
+ // // 管理员分组;
+ // admins []int64
+ // // 管理群分组;
+ // groups []int64
+ // 机器人引擎;
+ engine *tgbotapi.BotAPI
+ // 命令存储集;
+ command map[string]func(string, int64) TgCmdRsp
+ help string
+ exitRcv chan interface{}
+}
+
+// BotCmd 命令.
+type BotCmd struct {
+ // 指令;
+ Key string
+ // 摘要;
+ Summary string
+ // 处理;
+ Handle func(string, int64) TgCmdRsp
+}
+
+type BotCmdList []*BotCmd
+
+// NewBotCmd 新的命令.
+func NewBotCmd(key, summary string, handle func(string, int64) TgCmdRsp) *BotCmd {
+ return &BotCmd{
+ Key: key,
+ Summary: summary,
+ Handle: handle,
+ }
+}
+
+// NewTelegramBot 新的Telegram机器人.
+func NewTelegramBot(token string) *TgBot {
+ return &TgBot{
+ token: token,
+ exitRcv: make(chan interface{}),
+ }
+}
+
+// Init 机器人初始化.
+func (tb *TgBot) Init() *srvdef.SrvErr {
+ var err error
+ tb.engine, err = tgbotapi.NewBotAPI(tb.token)
+ if err != nil {
+ log.Warn("(tgbot:Init) tgbotapi.NewBotAPI failed", log.E(err))
+ return srvdef.New(srvdef.BusinessLevel, "", "")
+ }
+ tb.command = make(map[string]func(string, int64) TgCmdRsp)
+ tb.command["help"] = tb.menu
+ return nil
+}
+
+// Release 机器人退出释放
+func (tb *TgBot) Release() *srvdef.SrvErr {
+ tb.engine.StopReceivingUpdates()
+ close(tb.exitRcv)
+ return nil
+}
+
+func (tb *TgBot) menu(string, int64) TgCmdRsp {
+ return TgCmdRsp{Buf: []byte(tb.help), BufType: TextType}
+}
+
+// RegisterCommands 注册命令并且自动生成help.
+func (tb *TgBot) RegisterCommands(commands []*BotCmd) {
+ for _, cmd := range commands {
+ tb.command[cmd.Key] = cmd.Handle
+ tb.help += fmt.Sprintln("/" + cmd.Key + ": " + cmd.Summary)
+ }
+}
+
+// Receive 接收消息(阻塞).
+func (tb *TgBot) Receive() error {
+ log.Info("(tgbot:Receive) start")
+ // 设置更新频率;
+ cfg := tgbotapi.NewUpdate(0)
+ //cfg.Timeout = 1
+
+ // 得到更新管道;
+ updates, err := tb.engine.GetUpdatesChan(cfg)
+ if err != nil {
+ log.Warn("(tgbot:Receive) tb.engine.GetUpdatesChan failed", log.E(err))
+ return srvdef.New(srvdef.BusinessLevel, "", "")
+ }
+ for {
+ select {
+ case update := <-updates:
+ tb.ProcessMessage(&update)
+ case <-tb.exitRcv:
+ log.Info("(tgbot:Receive) exist")
+ break
+ default:
+ }
+ time.Sleep(time.Second * 1)
+ }
+ return nil
+}
+
+func (tb *TgBot) ProcessMessage(update *tgbotapi.Update) {
+ // 忽略非消息更新;
+ if update.Message == nil {
+ log.Info("(tgbot:ProcessMessage) message is nil")
+ return
+ }
+ info := fmt.Sprintf("[Name: %s%s][ID: %d][Text: %s]\n",
+ update.Message.From.FirstName, update.Message.From.LastName,
+ update.Message.Chat.ID, update.Message.Text)
+ log.Info(info)
+ if update.Message.IsCommand() {
+ message := tgbotapi.NewMessage(update.Message.Chat.ID, "")
+ handle, ok := tb.command[update.Message.Command()]
+ if !ok {
+ message.Text = "unknown command"
+ } else {
+ param := update.Message.CommandArguments()
+ rsp := handle(param, update.Message.Chat.ID)
+ switch rsp.BufType {
+ case FileType:
+
+ default:
+ message.Text = string(rsp.Buf)
+ }
+ //message.Text = handle(param)
+ }
+ tb.engine.Send(message)
+ }
+}
+
+func (tb *TgBot) StopReceive() {
+ tb.engine.StopReceivingUpdates()
+ close(tb.exitRcv)
+}
+
+// Send 发送消息.
+func (tb *TgBot) Send(ids []int64, text string) {
+ for _, id := range ids {
+ message := tgbotapi.NewMessage(id, text)
+ tb.engine.Send(message)
+ }
+}
diff --git a/pub/common/tgbot/tgbot_test.go b/pub/common/tgbot/tgbot_test.go
new file mode 100644
index 0000000..9f1e8a6
--- /dev/null
+++ b/pub/common/tgbot/tgbot_test.go
@@ -0,0 +1,35 @@
+package tgbot
+
+import (
+ "testing"
+)
+
+func create(t *testing.T) *TgBot {
+ token := "1014745091:AAFkw5j3Fw4ytvWgIll6iRH1q5WooS6JO74"
+ robot := NewTelegramBot(token)
+ if err := robot.Init(); err != nil {
+ t.Error(err)
+ t.Fail()
+ }
+ // 注册命令.
+ cmds := BotCmdList{NewBotCmd("test", "return testfunc result", testfunc)}
+ robot.RegisterCommands(cmds)
+ return robot
+}
+
+func testfunc(string, int64) TgCmdRsp {
+ return TgCmdRsp{Buf: []byte("help"), BufType: TextType}
+}
+
+func TestReceive(t *testing.T) {
+ robot := create(t)
+ if err := robot.Receive(); err != nil {
+ t.Error(err)
+ t.Fail()
+ }
+}
+
+func TestSend(t *testing.T) {
+ robot := create(t)
+ robot.Send([]int64{790603940}, "test send")
+}
diff --git a/pub/common/timehelper.go b/pub/common/timehelper.go
new file mode 100644
index 0000000..2d6b21b
--- /dev/null
+++ b/pub/common/timehelper.go
@@ -0,0 +1,55 @@
+package common
+
+import (
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func BindUrl(hostname string, path ...string) string {
+ if hostname == "" {
+ return strings.TrimLeft(filepath.Join(path...), "/")
+ }
+ if len(path) == 0 {
+ return hostname
+ }
+ su := strings.TrimLeft(filepath.Join(path...), "/")
+ pr := strings.TrimRight(hostname, "/")
+ return pr + "/" + su
+}
+
+// 将time.Time转换成int64
+func Time2Stamp(t time.Time) int64 {
+ return t.UnixNano() / 1e9
+}
+
+/**时间对象->字符串*/
+func Time2Str() string {
+ const shortForm = "2006-01-01 15:04:05"
+ t := time.Now()
+ temp := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.Local)
+ str := temp.Format(shortForm)
+ return str
+}
+
+/*时间戳->字符串*/
+func Stamp2Str(stamp int64) string {
+ timeLayout := "2006-01-02 15:04:05"
+ str := time.Unix(stamp/1000, 0).Format(timeLayout)
+ return str
+}
+
+/*时间戳->时间对象*/
+func Stamp2Time(stamp int64) time.Time {
+ stampStr := Stamp2Str(stamp)
+ timer := Str2Time(stampStr)
+ return timer
+}
+
+/**字符串->时间对象*/
+func Str2Time(formatTimeStr string) time.Time {
+ timeLayout := "2006-01-02 15:04:05"
+ loc, _ := time.LoadLocation("Local")
+ theTime, _ := time.ParseInLocation(timeLayout, formatTimeStr, loc) //使用模板在对应时区转化为time.time类型
+ return theTime
+}
diff --git a/pub/common/topicmessage/topicMessage.go b/pub/common/topicmessage/topicMessage.go
new file mode 100644
index 0000000..b4a4876
--- /dev/null
+++ b/pub/common/topicmessage/topicMessage.go
@@ -0,0 +1,79 @@
+package topicmessage
+
+import (
+ "time"
+)
+
+/* 通信协议,第一版*/
+
+// 用户用户注册消息
+type UserRegisterMsg struct {
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ Name string `json:"name" bson:"name"` // 名称
+ IsDirect bool `json:"isDirect" bson:"isDirect"` // true:是直推用户
+ DeviceId string `json:"deviceId" bson:"deviceId"` // 设备Id
+ IP string `json:"ip,omitempty" bson:"ip"` // IP
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+ InviteCode string `json:"inviteCode" bson:"inviteCode"` // 被邀请码
+ RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间
+ UserAgent string `json:"userAgent" bson:"userAgent"` // User-Agent识别
+}
+
+// 用户第一次访问
+type UserVisitMsg struct {
+ UserId int64 `bson:"userId" json:"userId"` // 用户Id
+ SumDate time.Time `bson:"sumDate" json:"sumDate"` // 记录时间
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ IP string `json:"ip,omitempty" bson:"ip"` // IP
+ Version string `json:"version,omitempty" bson:"version"` // APP版本号
+ VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+}
+
+type UserOrderMsg struct {
+ UserId int64 `json:"userId" bson:"userId"` // 用户ID
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
+ SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
+ DevType string `json:"devType" bson:"devType"` // 设备类型
+ ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字
+ CID string `json:"cid" bson:"cid"` // 渠道id
+ Type string `bson:"type" json:"type"` // 充值方式
+ RchgUse int64 `json:"rchgUse" bson:"rchgUse"` // 充值用途 1:金币 2:VIP
+ Repeat bool `bson:"repeat" json:"repeat"` // 复冲: true 重复充值 false 第一次
+ OrderId string `json:"orderId" bson:"orderId"` // 流水id
+ Money int64 `json:"money" bson:"money"` // 充值金额 订单金额
+ PayMoney int64 `json:"payMoney" bson:"payMoney"` // 实际到账金额 用户实际支付金额
+ Status int `json:"status" bson:"status"` // 2付款失败 3付款成功(目前只有成功才发送)
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+ Rate string `json:"rate" bson:"rate"` // 渠道费率
+ SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type UserChannelEvent struct {
+ UserId int64 `bson:"userId" json:"userId"` // 用户Id
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
+ WatchCount int64 `json:"watchCount,omitempty" bson:"watchCount"` // 观看次数
+ WatchTime int64 `json:"watchTime,omitempty" bson:"watchTime"` // 观看时长
+ AdClick int64 `json:"adClick,omitempty" bson:"adClick"` // 广告点击
+ AppClick int64 `json:"appClick,omitempty" bson:"appClick"` // APP点击
+ RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数
+}
+
+type MediaPlayEvent struct {
+ MediaId int64 `json:"mediaId" bson:"mediaId"` // 视频Id
+ Title string `json:"title" bson:"title"` // 标题
+ FileId string `json:"fileId" bson:"fileId"` // 媒资库ID
+ ActionType int64 `json:"actionType" bson:"actionType"` // 类型:0:播放 1:点赞 2:购买
+ UserId int64 `json:"userId" bson:"userId"` // 用户Id
+ AppId int32 `json:"appId" bson:"appId"` // appID
+ WatchAt time.Time `json:"watchAt" bson:"watchAt"` // 时间
+}
diff --git a/pub/common/typeConv.go b/pub/common/typeConv.go
new file mode 100644
index 0000000..7f51bb4
--- /dev/null
+++ b/pub/common/typeConv.go
@@ -0,0 +1,64 @@
+package common
+
+import (
+ "reflect"
+ "unicode"
+)
+
+// StructToMap 对象转map,去掉无效值
+func StructToMap(u interface{}) (m map[string]interface{}) {
+ t := reflect.TypeOf(u)
+ v := reflect.ValueOf(u)
+ p := make(map[string]interface{})
+ for i := 0; i < t.NumField(); i++ {
+ fv := v.Field(i)
+ ft := t.Field(i)
+ jsonTag, _ := ft.Tag.Lookup("json")
+ switch fv.Kind() {
+ case reflect.String:
+ if fv.String() != "" {
+ p[jsonTag] = fv.Interface()
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if fv.Int() != 0 {
+ p[jsonTag] = fv.Interface()
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ if fv.Uint() != 0 {
+ p[jsonTag] = fv.Interface()
+ }
+ case reflect.Float32, reflect.Float64:
+ if fv.Float() != 0 {
+ p[jsonTag] = fv.Interface()
+ }
+ default:
+ p[jsonTag] = fv.Interface()
+
+ }
+
+ }
+ return p
+}
+
+// Ucfirst () 字符串首字母大写
+func Ucfirst(str string) string {
+ for i, v := range str {
+ return string(unicode.ToUpper(v)) + str[i+1:]
+ }
+ return ""
+}
+
+// Lcfirst () 字符串首字母小写
+func Lcfirst(str string) string {
+ for i, v := range str {
+ return string(unicode.ToLower(v)) + str[i+1:]
+ }
+ return ""
+}
+
+// MobileBlurry ()
+func MobileBlurry(str string) string {
+
+ return str[0:3] + "****" + str[7:]
+
+}
diff --git a/pub/common/useragent/build.go b/pub/common/useragent/build.go
new file mode 100644
index 0000000..a8cf04b
--- /dev/null
+++ b/pub/common/useragent/build.go
@@ -0,0 +1,340 @@
+package useragent
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+const DefaultVer = "1.0.0"
+
+// Info 与前端 X-User-Agent 字段对齐,供 SSR 组装上游请求头。
+type Info struct {
+ RawUA string
+ DevType string
+ SysType string
+ Ver string
+ BuildID string
+ DeviceBrand string
+ DeviceModel string
+ SystemName string
+ SystemVersion string
+ SID string
+}
+
+// BuildOptions SSR / 代理侧可选覆盖项。
+type BuildOptions struct {
+ Ver string
+ SID string
+ // RawUA 为空时从 Request.UserAgent 读取
+ RawUA string
+}
+
+var (
+ reIOSVer = regexp.MustCompile(`OS (\d+[_\d]*) like Mac OS X`)
+ reAndroidVer = regexp.MustCompile(`Android ([\d.]+)`)
+ reAndroidModel = regexp.MustCompile(`Android\s+[\d.]+;\s*([^;)]+?)(?:\s+Build\/|\))`)
+ reWinNT = regexp.MustCompile(`Windows NT ([\d.]+)`)
+ reMacVer = regexp.MustCompile(`Mac OS X ([\d_]+)`)
+ reEdge = regexp.MustCompile(`(?i)Edg/([\d.]+)`)
+ reOpera = regexp.MustCompile(`(?i)(?:OPR|Opera)/([\d.]+)`)
+ reChrome = regexp.MustCompile(`(?i)Chrome/([\d.]+)`)
+ reSafari = regexp.MustCompile(`(?i)Version/([\d.]+).*Safari`)
+ reFirefox = regexp.MustCompile(`(?i)Firefox/([\d.]+)`)
+ reIE = regexp.MustCompile(`(?i)(?:MSIE\s|Trident.*rv:)([\d.]+)`)
+)
+
+// ParseDeviceFromUA 从浏览器原生 User-Agent 解析设备信息(服务端无 navigator)。
+func ParseDeviceFromUA(ua string) (brand, model, deviceType, systemName, systemVersion string) {
+ ua = strings.TrimSpace(ua)
+ switch {
+ case strings.Contains(ua, "iPhone") || strings.Contains(ua, "iPad") || strings.Contains(ua, "iPod"):
+ systemName = "iOS"
+ brand = "Apple"
+ if m := reIOSVer.FindStringSubmatch(ua); len(m) > 1 {
+ systemVersion = strings.ReplaceAll(m[1], "_", ".")
+ }
+ switch {
+ case strings.Contains(ua, "iPad"):
+ model, deviceType = "iPad", "tablet"
+ case strings.Contains(ua, "iPod"):
+ model, deviceType = "iPod", "mobile"
+ default:
+ model, deviceType = "iPhone", "mobile"
+ }
+ case strings.Contains(ua, "Android"):
+ systemName = "Android"
+ if m := reAndroidVer.FindStringSubmatch(ua); len(m) > 1 {
+ systemVersion = m[1]
+ }
+ if m := reAndroidModel.FindStringSubmatch(ua); len(m) > 1 {
+ model = strings.TrimSpace(m[1])
+ }
+ brand = guessAndroidBrand(model, ua)
+ if strings.Contains(ua, "Mobile") {
+ deviceType = "mobile"
+ } else {
+ deviceType = "tablet"
+ }
+ case strings.Contains(ua, "Windows"):
+ systemName = "Windows"
+ deviceType = "desktop"
+ if m := reWinNT.FindStringSubmatch(ua); len(m) > 1 {
+ systemVersion = m[1]
+ }
+ case strings.Contains(ua, "Mac OS X") || strings.Contains(ua, "Macintosh"):
+ systemName = "macOS"
+ brand = "Apple"
+ model = "Mac"
+ deviceType = "desktop"
+ if m := reMacVer.FindStringSubmatch(ua); len(m) > 1 {
+ systemVersion = strings.ReplaceAll(m[1], "_", ".")
+ }
+ case strings.Contains(ua, "Linux"):
+ systemName = "Linux"
+ deviceType = "desktop"
+ default:
+ systemName = "Other"
+ deviceType = "desktop"
+ }
+ return
+}
+
+func guessAndroidBrand(model, ua string) string {
+ s := model + " " + ua
+ rules := []struct {
+ re *regexp.Regexp
+ brand string
+ }{
+ {regexp.MustCompile(`(?i)Build/HONOR|\bHONOR\b`), "Honor"},
+ {regexp.MustCompile(`(?i)Build/HUAWEI|HUAWEI|HW-`), "Huawei"},
+ {regexp.MustCompile(`(?i)SM-|samsung|Galaxy`), "Samsung"},
+ {regexp.MustCompile(`(?i)Pixel`), "Google"},
+ {regexp.MustCompile(`(?i)Redmi|POCO|XiaoMi|\bMi\b`), "Xiaomi"},
+ {regexp.MustCompile(`(?i)OPPO|\bCPH\d{3,4}`), "OPPO"},
+ {regexp.MustCompile(`(?i)vivo|\bV\d{4}`), "vivo"},
+ {regexp.MustCompile(`(?i)OnePlus|ONEPLUS`), "OnePlus"},
+ {regexp.MustCompile(`(?i)realme|\bRMX\d{3,4}`), "realme"},
+ }
+ for _, r := range rules {
+ if r.re.MatchString(s) {
+ return r.brand
+ }
+ }
+ return ""
+}
+
+func detectSysType(ua string) string {
+ uaLower := strings.ToLower(ua)
+ switch {
+ case strings.Contains(uaLower, "android"):
+ return "h5_android"
+ case strings.Contains(uaLower, "iphone"), strings.Contains(uaLower, "ipad"), strings.Contains(uaLower, "ipod"):
+ return "h5_ios"
+ default:
+ return "h5_pc"
+ }
+}
+
+func parseBrowser(ua string) (name, version string) {
+ switch {
+ case reEdge.MatchString(ua):
+ m := reEdge.FindStringSubmatch(ua)
+ return "Edge", m[1]
+ case reOpera.MatchString(ua):
+ m := reOpera.FindStringSubmatch(ua)
+ return "Opera", m[1]
+ case reChrome.MatchString(ua) && !reEdge.MatchString(ua):
+ m := reChrome.FindStringSubmatch(ua)
+ return "Chrome", m[1]
+ case reSafari.MatchString(ua):
+ m := reSafari.FindStringSubmatch(ua)
+ return "Safari", m[1]
+ case reFirefox.MatchString(ua):
+ m := reFirefox.FindStringSubmatch(ua)
+ return "Firefox", m[1]
+ case reIE.MatchString(ua):
+ m := reIE.FindStringSubmatch(ua)
+ return "IE", m[1]
+ default:
+ return "", ""
+ }
+}
+
+func buildDevType(brand, model, deviceType string) string {
+ parts := make([]string, 0, 3)
+ for _, v := range []string{brand, model, deviceType} {
+ v = strings.TrimSpace(v)
+ if v != "" {
+ parts = append(parts, v)
+ }
+ }
+ if len(parts) < 3 {
+ return "Apple iPhone mobile"
+ }
+ return strings.Join(parts, " ")
+}
+
+// BuildInfo 根据原始 UA 组装结构化信息。
+func BuildInfo(opts BuildOptions) Info {
+ ua := strings.TrimSpace(opts.RawUA)
+ ver := strings.TrimSpace(opts.Ver)
+ if ver == "" {
+ ver = DefaultVer
+ }
+ brand, model, deviceType, systemName, systemVersion := ParseDeviceFromUA(ua)
+ browserName, browserVer := parseBrowser(ua)
+ buildID := strings.TrimSpace(strings.TrimSpace(browserName + " " + browserVer))
+
+ return Info{
+ RawUA: ua,
+ DevType: buildDevType(brand, model, deviceType),
+ SysType: detectSysType(ua),
+ Ver: ver,
+ BuildID: buildID,
+ DeviceBrand: brand,
+ DeviceModel: model,
+ SystemName: systemName,
+ SystemVersion: systemVersion,
+ SID: strings.TrimSpace(opts.SID),
+ }
+}
+
+// String 输出 X-User-Agent Header 值。
+func (info Info) String() string {
+ return fmt.Sprintf(
+ "DevType=%s;SysType=%s;Ver=%s;BuildID=%s;device_brand=%s;device_model=%s;system_name=%s;system_version=%s;sid=%s",
+ info.DevType,
+ info.SysType,
+ info.Ver,
+ info.BuildID,
+ info.DeviceBrand,
+ info.DeviceModel,
+ info.SystemName,
+ info.SystemVersion,
+ info.SID,
+ )
+}
+
+// BuildXUserAgent 直接返回 Header 字符串。
+func BuildXUserAgent(opts BuildOptions) string {
+ return BuildInfo(opts).String()
+}
+
+// SIDFromRequest 从 cookie 读取 sid(与前端约定:sid / _s_token_)。
+func SIDFromRequest(r *http.Request) string {
+ if r == nil {
+ return ""
+ }
+ if c, err := r.Cookie("sid"); err == nil && c != nil {
+ if v := strings.TrimSpace(c.Value); v != "" {
+ return v
+ }
+ }
+ if c, err := r.Cookie("_s_token_"); err == nil && c != nil {
+ v := strings.TrimSpace(c.Value)
+ // 可能是 JSON:{"sid":"..."} 或纯字符串
+ if strings.HasPrefix(v, "{") {
+ const key = `"sid"`
+ if i := strings.Index(v, key); i >= 0 {
+ rest := v[i+len(key):]
+ if j := strings.Index(rest, `"`); j >= 0 {
+ rest = rest[j+1:]
+ if k := strings.Index(rest, `"`); k >= 0 {
+ return rest[:k]
+ }
+ }
+ }
+ }
+ return v
+ }
+ return ""
+}
+
+// BuildXUserAgentFromRequest 供 SSR 反向代理上游 API 使用。
+// 优先透传客户端已带的 X-User-Agent;否则用浏览器 UA 组装。
+func BuildXUserAgentFromRequest(c *gin.Context, ver string) string {
+ if c == nil || c.Request == nil {
+ return BuildXUserAgent(BuildOptions{Ver: ver})
+ }
+ if xua := strings.TrimSpace(c.GetHeader("X-User-Agent")); xua != "" {
+ return xua
+ }
+ return BuildXUserAgent(BuildOptions{
+ Ver: ver,
+ SID: SIDFromRequest(c.Request),
+ RawUA: c.Request.UserAgent(),
+ })
+}
+
+// TokenFromRequest 从 cookie 读取登录 token(与前端约定:token / _token_)。
+func TokenFromRequest(r *http.Request) string {
+ if r == nil {
+ return ""
+ }
+ for _, name := range []string{"token", "_token_"} {
+ if c, err := r.Cookie(name); err == nil && c != nil {
+ if v := cookieUnescape(c.Value); v != "" {
+ return v
+ }
+ }
+ }
+ return ""
+}
+
+func cookieUnescape(raw string) string {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return ""
+ }
+ if decoded, err := url.QueryUnescape(raw); err == nil {
+ return strings.TrimSpace(decoded)
+ }
+ return raw
+}
+
+// ClientIPFromRequest 优先透传已有 X-Real-IP / X-Forwarded-For,否则用远端地址。
+func ClientIPFromRequest(c *gin.Context) string {
+ if c == nil || c.Request == nil {
+ return ""
+ }
+ if ip := strings.TrimSpace(c.GetHeader("X-Real-IP")); ip != "" {
+ return ip
+ }
+ if xff := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); xff != "" {
+ if i := strings.Index(xff, ","); i >= 0 {
+ return strings.TrimSpace(xff[:i])
+ }
+ return xff
+ }
+ return strings.TrimSpace(c.ClientIP())
+}
+
+// BuildUpstreamHeader 组装上游 API 请求头:X-Real-IP、User-Agent、X-User-Agent。
+// 登录 token / sid 已在 cookie 中时由调用方按需写入 Authorization 等字段。
+func BuildUpstreamHeader(c *gin.Context, ver string) map[string]string {
+ header := make(map[string]string, 8)
+ ApplyToHeader(header, c, ver)
+ return header
+}
+
+// ApplyToHeader 将上游必需头写入 header map:
+// X-Real-IP、User-Agent、X-User-Agent。
+func ApplyToHeader(header map[string]string, c *gin.Context, ver string) {
+ if header == nil {
+ return
+ }
+ if c != nil && c.Request != nil {
+ if ip := ClientIPFromRequest(c); ip != "" {
+ header["X-Real-IP"] = ip
+ }
+ if ua := strings.TrimSpace(c.Request.UserAgent()); ua != "" {
+ header["User-Agent"] = ua
+ }
+ }
+ header["X-User-Agent"] = BuildXUserAgentFromRequest(c, ver)
+}
diff --git a/pub/common/useragent/build_test.go b/pub/common/useragent/build_test.go
new file mode 100644
index 0000000..0f88da6
--- /dev/null
+++ b/pub/common/useragent/build_test.go
@@ -0,0 +1,53 @@
+package useragent
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestBuildXUserAgent_iPhone(t *testing.T) {
+ ua := "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1"
+ got := BuildXUserAgent(BuildOptions{RawUA: ua, Ver: "1.0.0", SID: "abc"})
+ wantParts := []string{
+ "DevType=Apple iPhone mobile",
+ "SysType=h5_ios",
+ "Ver=1.0.0",
+ "BuildID=Safari 17.2",
+ "device_brand=Apple",
+ "device_model=iPhone",
+ "system_name=iOS",
+ "system_version=17.2",
+ "sid=abc",
+ }
+ for _, p := range wantParts {
+ if !strings.Contains(got, p) {
+ t.Fatalf("missing %q in %s", p, got)
+ }
+ }
+}
+
+func TestBuildXUserAgent_Android(t *testing.T) {
+ ua := "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
+ got := BuildXUserAgent(BuildOptions{RawUA: ua, Ver: "1.0.0", SID: "s1"})
+ wantParts := []string{
+ "SysType=h5_android",
+ "device_brand=Google",
+ "device_model=Pixel 7",
+ "system_name=Android",
+ "system_version=13",
+ "BuildID=Chrome 120.0.0.0",
+ "sid=s1",
+ }
+ for _, p := range wantParts {
+ if !strings.Contains(got, p) {
+ t.Fatalf("missing %q in %s", p, got)
+ }
+ }
+}
+
+func TestBuildXUserAgent_EmptyFallsBackDevType(t *testing.T) {
+ got := BuildXUserAgent(BuildOptions{RawUA: "", Ver: "1.0.0"})
+ if !strings.Contains(got, "DevType=Apple iPhone mobile") || !strings.Contains(got, "SysType=h5_pc") {
+ t.Fatalf("unexpected fallback: %s", got)
+ }
+}
diff --git a/pub/common/useragent/ua.go b/pub/common/useragent/ua.go
new file mode 100644
index 0000000..349640a
--- /dev/null
+++ b/pub/common/useragent/ua.go
@@ -0,0 +1,97 @@
+package useragent
+
+import (
+ "net/url"
+ "strings"
+ "videoweb/pub/common/enum/consts"
+ "videoweb/pub/common/log"
+
+ "github.com/gin-gonic/gin"
+)
+
+type UA struct {
+ DevID string
+ DevType string
+ SysType string
+ Ver string
+ BuildID string
+}
+
+func IsH5(sysType string) bool {
+ if sysType == consts.H5IOS || sysType == consts.H5Android || sysType == consts.H5PC || sysType == consts.H5 || sysType == consts.Light {
+ return true
+ } else {
+ return false
+ }
+}
+
+func (s UA) IsH5() bool {
+ return IsH5(s.SysType)
+}
+
+func UserAgent(c *gin.Context) {
+ var uastr string
+ ua := UA{}
+ userAgent := c.GetHeader("X-User-Agent") //兼容H5 UserAgent
+ if userAgent == "" {
+ userAgent = c.Request.UserAgent()
+ }
+ if userAgent != "" {
+ // "BuildID=com.abc.Butterfly;SysType=h5;DevID=B9A00938-FE68-4686-9461-A365C7E5110D;Ver=0.0.1;DevType=iPhone;"
+ decodeUa, err := url.QueryUnescape(userAgent)
+ if err != nil {
+ log.Warn("URLDecode user-agent error", log.Any("ua", userAgent))
+ uastr = userAgent
+ } else {
+ uastr = decodeUa
+ }
+ uas := strings.Split(uastr, ";")
+ for _, v := range uas {
+ vss := strings.Split(strings.TrimSpace(v), "=")
+ if len(vss) < 2 {
+ c.Set(consts.CtxUAH5, vss[0])
+ // log.Warn("user-agent miss", log.Any("ua", userAgent), log.Any("v", v))
+ continue
+ }
+ switch strings.TrimSpace(vss[0]) {
+ case "DevID":
+ ua.DevID = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxDevId, ua.DevID)
+ case "DevType":
+ ua.DevType = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxDevType, ua.DevType)
+ case "SysType":
+ IntputSysType := strings.TrimSpace(vss[1])
+ if strings.Contains(IntputSysType, "h5_android") {
+ ua.SysType = consts.H5Android
+ } else if strings.Contains(IntputSysType, "h5_ios") {
+ ua.SysType = consts.H5IOS
+ } else if strings.Contains(IntputSysType, "h5_pc") {
+ ua.SysType = consts.H5PC
+ } else if strings.Contains(IntputSysType, "android") {
+ ua.SysType = consts.Android
+ } else if strings.Contains(IntputSysType, "ios") {
+ ua.SysType = consts.IOS
+ } else if strings.Contains(IntputSysType, "h5") {
+ ua.SysType = consts.H5
+ } else if strings.Contains(IntputSysType, "pc") {
+ ua.SysType = consts.PC
+ } else if strings.Contains(IntputSysType, "light") {
+ ua.SysType = consts.Light
+ }
+ c.Set(consts.CtxSysType, ua.SysType)
+ case "Ver":
+ ua.Ver = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxVersion, ua.Ver)
+ case "BuildID":
+ ua.BuildID = strings.TrimSpace(vss[1])
+ c.Set(consts.CtxBuildId, ua.BuildID)
+ default:
+ log.Warn("user-agent unknown", log.Any("ua", userAgent))
+ }
+ }
+ } else {
+ log.Warn("user-agent empty")
+ }
+ c.Set(consts.CtxUA, ua)
+}
\ No newline at end of file
diff --git a/pub/common/utils/array.go b/pub/common/utils/array.go
new file mode 100644
index 0000000..e4b06d4
--- /dev/null
+++ b/pub/common/utils/array.go
@@ -0,0 +1,292 @@
+package utils
+
+import (
+ "math/rand"
+ "reflect"
+)
+
+//数组求交集,差集,并集
+/*求并集**/
+func Union(slice1, slice2 []string) []string {
+ m := make(map[string]int)
+ for _, v := range slice1 {
+ m[v]++
+ }
+
+ for _, v := range slice2 {
+ times, _ := m[v]
+ if times == 0 {
+ slice1 = append(slice1, v)
+ }
+ }
+ return slice1
+}
+
+// 求交集
+func Intersect(slice1, slice2 []string) []string {
+ m := make(map[string]int)
+ nn := make([]string, 0)
+ for _, v := range slice1 {
+ m[v]++
+ }
+
+ for _, v := range slice2 {
+ times, _ := m[v]
+ if times == 1 {
+ nn = append(nn, v)
+ }
+ }
+ return nn
+}
+
+// 求交集
+func IntersectInt64(slice1, slice2 []int64) []int64 {
+ m := make(map[int64]int)
+ nn := make([]int64, 0)
+ for _, v := range slice1 {
+ m[v]++
+ }
+
+ for _, v := range slice2 {
+ times, _ := m[v]
+ if times == 1 {
+ nn = append(nn, v)
+ }
+ }
+ return nn
+}
+
+// 求差集 slice1-并集
+func Difference(slice1, slice2 []string) []string {
+ m := make(map[string]int)
+ nn := make([]string, 0)
+ inter := Intersect(slice1, slice2)
+ for _, v := range inter {
+ m[v]++
+ }
+
+ for _, value := range slice1 {
+ times, _ := m[value]
+ if times == 0 {
+ nn = append(nn, value)
+ }
+ }
+ return nn
+}
+
+// 求差集 slice1-并集
+func DifferenceInt64(slice1, slice2 []int64) []int64 {
+ m := make(map[int64]int)
+ nn := make([]int64, 0)
+ inter := IntersectInt64(slice1, slice2)
+ for _, v := range inter {
+ m[v]++
+ }
+
+ for _, value := range slice1 {
+ times, _ := m[value]
+ if times == 0 {
+ nn = append(nn, value)
+ }
+ }
+ return nn
+}
+
+// 去重
+func RemoveRepeat(slc []int64) []int64 {
+ result := []int64{} // 存放结果
+ for i := range slc {
+ flag := true
+ for j := range result {
+ if slc[i] == result[j] {
+ flag = false // 存在重复元素,标识为false
+ break
+ }
+ }
+ if flag { // 标识为false,不添加进结果
+ result = append(result, slc[i])
+ }
+ }
+ return result
+}
+
+func IncludeInt32(slc []int32, n int32) bool {
+ for _, v := range slc {
+ if v == n {
+ return true
+ }
+ }
+ return false
+}
+
+func IncludeInt64(slc []int64, n int64) bool {
+ for _, v := range slc {
+ if v == n {
+ return true
+ }
+ }
+ return false
+}
+
+func RemoveInt64(slc []int64, n int64) []int64 {
+ for i := 0; i < len(slc); {
+ if slc[i] == n {
+ slc = append(slc[:i], slc[i+1:]...)
+ } else {
+ i++
+ }
+ }
+ return slc
+}
+
+func RemoveString(slc []string, n string) []string {
+ for i := 0; i < len(slc); {
+ if slc[i] == n {
+ slc = append(slc[:i], slc[i+1:]...)
+ } else {
+ i++
+ }
+ }
+ return slc
+}
+
+// RemoveRepeatString 去重string
+func RemoveRepeatString(slc []string) []string {
+ result := []string{} // 存放结果
+ for i := range slc {
+ flag := true
+ for j := range result {
+ if slc[i] == result[j] {
+ flag = false // 存在重复元素,标识为false
+ break
+ }
+ }
+ if flag { // 标识为false,不添加进结果
+ result = append(result, slc[i])
+ }
+ }
+ return result
+}
+
+func IncludeString(slc []string, s string) bool {
+ for _, v := range slc {
+ if v == s {
+ return true
+ }
+ }
+ return false
+}
+
+func EqualInt64s(slice1, slice2 []int64) bool {
+ if len(slice1) != len(slice2) {
+ return false
+ }
+ for _, v1 := range slice1 {
+ find := false
+ for _, v2 := range slice2 {
+ if v1 == v2 {
+ find = true
+ break
+ }
+ }
+ if !find {
+ return false
+ }
+ }
+ return true
+}
+
+func EqualInt64WithOrder(slice1, slice2 []int64) bool {
+ if len(slice1) != len(slice2) {
+ return false
+ }
+ for k, v1 := range slice1 {
+ if slice2[k] != v1 {
+ return false
+ }
+ }
+ return true
+}
+
+func EqualString(slice1, slice2 []string) bool {
+ if len(slice1) != len(slice2) {
+ return false
+ }
+ for _, v1 := range slice1 {
+ find := false
+ for _, v2 := range slice2 {
+ if v1 == v2 {
+ find = true
+ break
+ }
+ }
+ if find == false {
+ return false
+ }
+ }
+ return true
+}
+
+// 切片乱序
+func ShuffleSlice(slice interface{}) {
+ rv := reflect.ValueOf(slice)
+ if rv.Type().Kind() != reflect.Slice {
+ return
+ }
+
+ length := rv.Len()
+ if length < 2 {
+ return
+ }
+ swap := reflect.Swapper(slice)
+ //rand.Seed(time.Now().Unix())
+ for i := length - 1; i >= 0; i-- {
+ j := rand.Intn(length)
+ swap(i, j)
+ }
+ return
+}
+
+func RemoveOneInt64(slc []int64, n int64) []int64 {
+ for i := 0; i < len(slc); {
+ if slc[i] == n {
+ slc = append(slc[:i], slc[i+1:]...)
+ break
+ } else {
+ i++
+ }
+ }
+ return slc
+}
+
+// 求最大值
+func SliceMaxInt64(slc []int64) int64 {
+ maxVal := slc[0]
+ for i := 1; i < len(slc); i++ {
+ //从第二个 元素开始循环比较,如果发现有更大的,则交换
+ if maxVal < slc[i] {
+ maxVal = slc[i]
+ }
+ }
+ return maxVal
+}
+
+// 求最大值
+func SliceMinInt64(slc []int64) int64 {
+ minVal := slc[0]
+ for i := 1; i < len(slc); i++ {
+ //从第二个 元素开始循环比较,如果发现有更小的,则交换
+ if minVal > slc[i] {
+ minVal = slc[i]
+ }
+ }
+ return minVal
+}
+
+func ReverseAny(s interface{}) {
+ n := reflect.ValueOf(s).Len()
+ swap := reflect.Swapper(s)
+ for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
+ swap(i, j)
+ }
+}
diff --git a/pub/common/utils/convUtil.go b/pub/common/utils/convUtil.go
new file mode 100644
index 0000000..dda3ea9
--- /dev/null
+++ b/pub/common/utils/convUtil.go
@@ -0,0 +1,184 @@
+package utils
+
+import (
+ "encoding/json"
+ "path/filepath"
+ "strings"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ID = primitive.ObjectID
+
+// ToJsonM 将struct转换为 Json Map
+func ToJsonM(obj interface{}) (map[string]interface{}, error) {
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+// ToBsonM 将struct转换为 Bson Map
+func ToBsonM(s interface{}) (bson.M, error) {
+ data, err := bson.Marshal(s)
+ if err != nil {
+ return nil, err
+ }
+ m := make(bson.M)
+ if err := bson.Unmarshal(data, &m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func MapToJsonString(m map[string]string) (string, error) {
+ if data, err := json.Marshal(m); err != nil {
+ return "", err
+ } else {
+ return string(data), nil
+ }
+}
+
+func StructToJsonString(s interface{}) (string, error) {
+ j, err := json.Marshal(s)
+ if err != nil {
+ return "", err
+ }
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(j, &m); err != nil {
+ return "", err
+ }
+ if data, err := json.Marshal(m); err != nil {
+ return "", err
+ } else {
+ return string(data), nil
+ }
+}
+
+// BMapToStruct s is struct ptr
+func BMapToStruct(s interface{}, m map[string]interface{}) error {
+ bsonBytes, err := bson.Marshal(m)
+ if err != nil {
+ return err
+ }
+ return bson.Unmarshal(bsonBytes, s)
+}
+
+// JMapToStruct s is struct ptr
+func JMapToStruct(s interface{}, m map[string]interface{}) error {
+ bsonBytes, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(bsonBytes, s)
+}
+
+// JSONStruct2Map 将struct转换为Map
+func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) {
+ m := make(map[string]interface{})
+ j, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(j, &m); err != nil {
+ return nil, err
+ }
+ return m, err
+}
+
+// Map2JSONStruct 将map转换为Json struct
+func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
+ j, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(j, v); err != nil {
+ return err
+ }
+ return err
+}
+
+func IDArray(ids []string) ([]ID, error) {
+ var idArray []ID
+ for _, id := range ids {
+ _id, err := primitive.ObjectIDFromHex(id)
+ if err != nil {
+ return nil, err
+ }
+ idArray = append(idArray, _id)
+ }
+
+ return idArray, nil
+}
+
+// 拼接https://
+func BindHttpSch(path string) string {
+ if strings.HasPrefix(path, "http") {
+ return path
+ } else {
+ return "https://" + path
+ }
+}
+
+func BindUrl(hostname string, path ...string) string {
+ if hostname == "" {
+ return strings.TrimLeft(filepath.Join(path...), "/")
+ }
+ if len(path) == 0 {
+ return hostname
+ }
+ su := strings.TrimLeft(filepath.Join(path...), "/")
+ pr := strings.TrimRight(hostname, "/")
+ return pr + "/" + su
+}
+
+func ObjectIDs2String(obj []primitive.ObjectID) []string {
+ arr := []string{}
+ if len(obj) == 0 {
+ return arr
+ }
+ for _, o := range obj {
+ arr = append(arr, o.Hex())
+ }
+ return arr
+}
+
+func String2ObjectID(obj []string) []primitive.ObjectID {
+ arr := []primitive.ObjectID{}
+ for _, o := range obj {
+ oid, err := primitive.ObjectIDFromHex(o)
+ if err != nil {
+ continue
+ }
+ arr = append(arr, oid)
+ }
+ return arr
+}
+
+// PercentOf - calculate what percent [number1] is of [number2].
+// ex. 300 is 12.5% of 2400
+func PercentOf(part int, total int) float64 {
+ return (float64(part) * float64(100)) / float64(total)
+}
+
+func MergeMap(src, dest map[string]interface{}) map[string]interface{} {
+ if len(src) == 0 {
+ return dest
+ }
+ if len(dest) == 0 {
+ return src
+ }
+ if len(src) == 0 && len(dest) == 0 {
+ return make(map[string]interface{})
+ }
+ for k, v := range dest {
+ src[k] = v
+ }
+ return src
+}
diff --git a/pub/common/utils/copy.go b/pub/common/utils/copy.go
new file mode 100644
index 0000000..51e0b2c
--- /dev/null
+++ b/pub/common/utils/copy.go
@@ -0,0 +1,130 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/gob"
+ "reflect"
+ "time"
+)
+
+// DeepCopy 深copy
+func DeepCopy(dst, src interface{}) error {
+ var buf bytes.Buffer
+ if err := gob.NewEncoder(&buf).Encode(src); err != nil {
+ return err
+ }
+ return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
+}
+
+// Interface for delegating copy process to type
+type Interface interface {
+ DeepCopy() interface{}
+}
+
+// Iface is an alias to Copy; this exists for backwards compatibility reasons.
+func Iface(iface interface{}) interface{} {
+ return Copy(iface)
+}
+
+// Copy creates a deep copy of whatever is passed to it and returns the copy
+// in an interface{}. The returned value will need to be asserted to the
+// correct type.
+func Copy(src interface{}) interface{} {
+ if src == nil {
+ return nil
+ }
+
+ // Make the interface a reflect.Value
+ original := reflect.ValueOf(src)
+
+ // Make a copy of the same type as the original.
+ cpy := reflect.New(original.Type()).Elem()
+
+ // Recursively copy the original.
+ copyRecursive(original, cpy)
+
+ // Return the copy as an interface.
+ return cpy.Interface()
+}
+
+// copyRecursive does the actual copying of the interface. It currently has
+// limited support for what it can handle. Add as needed.
+func copyRecursive(original, cpy reflect.Value) {
+ // check for implement deepcopy.Interface
+ if original.CanInterface() {
+ if copier, ok := original.Interface().(Interface); ok {
+ cpy.Set(reflect.ValueOf(copier.DeepCopy()))
+ return
+ }
+ }
+
+ // handle according to original's Kind
+ switch original.Kind() {
+ case reflect.Ptr:
+ // Get the actual value being pointed to.
+ originalValue := original.Elem()
+
+ // if it isn't valid, return.
+ if !originalValue.IsValid() {
+ return
+ }
+ cpy.Set(reflect.New(originalValue.Type()))
+ copyRecursive(originalValue, cpy.Elem())
+
+ case reflect.Interface:
+ // If this is a nil, don't do anything
+ if original.IsNil() {
+ return
+ }
+ // Get the value for the interface, not the pointer.
+ originalValue := original.Elem()
+
+ // Get the value by calling Elem().
+ copyValue := reflect.New(originalValue.Type()).Elem()
+ copyRecursive(originalValue, copyValue)
+ cpy.Set(copyValue)
+
+ case reflect.Struct:
+ t, ok := original.Interface().(time.Time)
+ if ok {
+ cpy.Set(reflect.ValueOf(t))
+ return
+ }
+ // Go through each field of the struct and copy it.
+ for i := 0; i < original.NumField(); i++ {
+ // The Type's StructField for a given field is checked to see if StructField.PkgPath
+ // is set to determine if the field is exported or not because CanSet() returns false
+ // for settable fields. I'm not sure why. -mohae
+ if original.Type().Field(i).PkgPath != "" {
+ continue
+ }
+ copyRecursive(original.Field(i), cpy.Field(i))
+ }
+
+ case reflect.Slice:
+ if original.IsNil() {
+ return
+ }
+ // Make a new slice and copy each element.
+ cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
+ for i := 0; i < original.Len(); i++ {
+ copyRecursive(original.Index(i), cpy.Index(i))
+ }
+
+ case reflect.Map:
+ if original.IsNil() {
+ return
+ }
+ cpy.Set(reflect.MakeMap(original.Type()))
+ for _, key := range original.MapKeys() {
+ originalValue := original.MapIndex(key)
+ copyValue := reflect.New(originalValue.Type()).Elem()
+ copyRecursive(originalValue, copyValue)
+ copyKey := Copy(key.Interface())
+ cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
+ }
+
+ default:
+ cpy.Set(original)
+ }
+}
diff --git a/pub/common/utils/file.go b/pub/common/utils/file.go
new file mode 100644
index 0000000..c595e6f
--- /dev/null
+++ b/pub/common/utils/file.go
@@ -0,0 +1,56 @@
+package utils
+
+import "os"
+
+// CreateEmptyFile 创建一个固定size大小的空文件
+// 如果seek文件失败,则删除文件,并返回错误信息
+func CreateEmptyFile(name string, size int64) (f *os.File, err error) {
+ defer func() {
+ if err != nil && f != nil {
+ f.Close()
+ os.Remove(name)
+ }
+ }()
+ f, err = os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
+ if err != nil {
+ return
+ }
+ if _, err = f.Seek(size-1, 0); err != nil {
+ return
+ }
+ _, err = f.Write([]byte{0})
+ return
+}
+
+func WriteFile(data []byte, filename string) (err error) {
+ defer func() {
+ if err != nil {
+ os.Remove(filename)
+ }
+ }()
+
+ pi := PathForInfo(filename)
+ if pi.Dir != "" {
+ if err = os.MkdirAll(pi.Dir, 0755); err != nil {
+ return
+ }
+ }
+
+ f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
+ if err != nil {
+ return
+ }
+ _, err = f.Write(data)
+ f.Close()
+ return
+}
+func PathExists(path string) (bool, error) {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true, nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
diff --git a/pub/common/utils/hash.go b/pub/common/utils/hash.go
new file mode 100644
index 0000000..568035c
--- /dev/null
+++ b/pub/common/utils/hash.go
@@ -0,0 +1,12 @@
+package utils
+
+// // MakeCrc
+// func MakeCrc(s string) uint32 {
+// return crc32.ChecksumIEEE([]byte(s))
+// }
+
+// func Md5V(str string) string {
+// h := md5.New()
+// h.Write([]byte(str))
+// return hex.EncodeToString(h.Sum(nil))
+// }
\ No newline at end of file
diff --git a/pub/common/utils/http.go b/pub/common/utils/http.go
new file mode 100644
index 0000000..1467d0e
--- /dev/null
+++ b/pub/common/utils/http.go
@@ -0,0 +1,283 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+)
+
+var once sync.Once
+var _client *http.Client
+
+func GetHttpClient() *http.Client {
+ once.Do(func() {
+ _client = &http.Client{
+ Timeout: time.Minute * 1, //设置超时时间
+ Transport: &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).DialContext,
+ TLSHandshakeTimeout: 10 * time.Second, //限制 TLS握手的时间
+ ResponseHeaderTimeout: 60 * time.Second, //限制读取response header的时间,默认 timeout + 5*time.Second
+ ExpectContinueTimeout: 1 * time.Second, //限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待。
+ MaxIdleConns: 100, //所有host的连接池最大连接数量,默认无穷大
+ MaxIdleConnsPerHost: 300, //每个host的连接池最大空闲连接数,默认2
+ MaxConnsPerHost: 100, //每个host的最大连接数量
+ IdleConnTimeout: 3 * time.Minute, //how long an idle connection is kept in the connection pool.
+ },
+ }
+ })
+ return _client
+}
+
+func getBytes(request interface{}) ([]byte, error) {
+ var body []byte
+ switch t := request.(type) {
+ case []byte:
+ body = t
+ case string:
+ body = []byte(t)
+ default:
+ var err error
+ body, err = json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return body, nil
+}
+
+type OnHttpResponse func(res *http.Response) error
+
+func HttpRequest(req *http.Request, onResponse OnHttpResponse) error {
+ client := GetHttpClient()
+ res, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if onResponse != nil {
+ err = onResponse(res)
+ }
+ return err
+}
+
+func HttpMethod(url, method string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return err
+ }
+ if header != nil {
+ req.Header = header
+ }
+ return HttpRequest(req, onResponse)
+}
+
+func HttpGet(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ return HttpMethod(url, http.MethodGet, header, body, onResponse)
+}
+
+func HttpPost(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ return HttpMethod(url, http.MethodPost, header, body, onResponse)
+}
+
+func HttpDelete(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
+ return HttpMethod(url, http.MethodDelete, header, body, onResponse)
+}
+
+func HttpPostRequestJson(url string, request interface{}, resp interface{}) error {
+ header := make(http.Header)
+ header.Set("Content-Type", "application/json")
+ body, err := getBytes(request)
+ if err != nil {
+ return err
+ }
+ return HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, resp)
+ })
+}
+
+func POSTJson(url string, headers map[string]string, data interface{}) (*http.Response, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ body, err := getBytes(data)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest("POST", url, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header = header
+ client := GetHttpClient()
+ return client.Do(req)
+}
+
+func GetWithParam(url string, headers map[string]string, params ...interface{}) (*http.Response, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ for _, p := range params {
+ url = addParams(url, toUrlValues(p))
+ }
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header = header
+ client := GetHttpClient()
+ return client.Do(req)
+}
+
+func GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ for _, p := range params {
+ url = addParams(url, toUrlValues(p))
+ }
+ code := http.StatusBadRequest
+ err := HttpGet(url, header, nil, func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+func POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ body, err := getBytes(params)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ code := http.StatusBadRequest
+ err = HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+func POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ header.Set("Content-Type", "application/json")
+ body, err := getBytes(data)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ code := http.StatusBadRequest
+ err = HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+func DeleteWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
+ header := make(http.Header)
+ for k, v := range headers {
+ header.Set(k, v)
+ }
+ body, err := getBytes(params)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ code := http.StatusBadRequest
+ err = HttpDelete(url, header, bytes.NewReader(body), func(res *http.Response) error {
+ code = res.StatusCode
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("http request (%s) error %s", url, res.Status)
+ }
+ content, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(content, bind)
+ })
+ return code, err
+}
+
+// Add params to a url string.
+func addParams(url_ string, params url.Values) string {
+ if len(params) == 0 {
+ return url_
+ }
+
+ if !strings.Contains(url_, "?") {
+ url_ += "?"
+ }
+
+ if strings.HasSuffix(url_, "?") || strings.HasSuffix(url_, "&") {
+ url_ += params.Encode()
+ } else {
+ url_ += "&" + params.Encode()
+ }
+
+ return url_
+}
+
+func toUrlValues(v interface{}) url.Values {
+ switch t := v.(type) {
+ case url.Values:
+ return t
+ case map[string][]string:
+ return url.Values(t)
+ case map[string]string:
+ rst := make(url.Values)
+ for k, v := range t {
+ rst.Add(k, v)
+ }
+ return rst
+ case nil:
+ return make(url.Values)
+ default:
+ panic("Invalid value")
+ }
+}
diff --git a/pub/common/utils/jwt.go b/pub/common/utils/jwt.go
new file mode 100644
index 0000000..5810a0b
--- /dev/null
+++ b/pub/common/utils/jwt.go
@@ -0,0 +1,127 @@
+package utils
+
+import (
+ "errors"
+ "strings"
+ "time"
+
+ "github.com/dgrijalva/jwt-go"
+)
+
+const (
+ userIDKey = "userId"
+ ipKey = "ip"
+ uaKey = "ua"
+)
+
+type IPClaims struct {
+ Ip string
+ Expire time.Duration
+ UA string
+}
+
+func GenerateIpToken(claims IPClaims, secret string) (string, error) {
+ return generateToken(jwt.MapClaims{
+ ipKey: claims.Ip,
+ uaKey: claims.UA,
+ "exp": Now().Add(claims.Expire).Unix(),
+ "issuer": "com.butterfly",
+ "sub": "asign",
+ "aud": "public",
+ }, []byte(secret))
+}
+
+func ParseIpToken(tokenString string, secret string) (pass bool, ip, ua string, err error) {
+ t, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ })
+ var isExpire *bool
+ if vErr, ok := err.(*jwt.ValidationError); ok {
+ e := vErr.Errors&jwt.ValidationErrorExpired > 0
+ isExpire = &e
+ }
+ if err != nil || isExpire == nil {
+ return
+ }
+ var ok bool
+ var jc jwt.MapClaims
+ jc, ok = t.Claims.(jwt.MapClaims)
+ if !ok {
+ err = errors.New("parse token fail")
+ return
+ }
+ ip, ok = jc[ipKey].(string)
+ if !ok {
+ err = errors.New("parse token fail")
+ return
+ }
+ ua, ok = jc[uaKey].(string)
+ if !ok {
+ err = errors.New("parse token fail")
+ return
+ }
+ pass = true
+ return
+}
+
+type UserClaims struct {
+ UserID int64
+ Expire time.Duration
+}
+
+func generateToken(claims jwt.MapClaims, secret []byte) (string, error) {
+ return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
+}
+
+func GenerateUserToken(claims UserClaims, secret string) (string, error) {
+ return generateToken(jwt.MapClaims{
+ userIDKey: claims.UserID,
+ "exp": Now().Add(claims.Expire).Unix(),
+ "issuer": "com.butterfly",
+ "sub": "asign",
+ "aud": "public",
+ }, []byte(secret))
+}
+
+// ParseToken 解析Token
+func ParseUserToken(token string, secret string) (userId int64, newToken string, err error) {
+ t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
+ return []byte(secret), nil
+ })
+ var isExpire *bool
+ if vErr, ok := err.(jwt.ValidationError); ok {
+ e := vErr.Errors&jwt.ValidationErrorExpired > 0
+ isExpire = &e
+ }
+ if err != nil && isExpire == nil {
+ return
+ }
+
+ jc, ok := t.Claims.(jwt.MapClaims)
+ if !ok {
+ err = errors.New("unknown token error1")
+ return
+ }
+
+ id, ok := jc[userIDKey].(float64)
+ if !ok {
+ err = errors.New("unknown token error1")
+ return
+ }
+ userId = int64(id)
+ if isExpire != nil && *isExpire {
+ newToken, err = GenerateUserToken(UserClaims{UserID: userId, Expire: 30 * 24 * time.Hour}, secret)
+ return
+ }
+ err = nil
+ return
+}
+
+// Strips 'Bearer ' prefix from bearer token string
+func StripBearerPrefixFromTokenString(tok string) string {
+ if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " {
+ return tok[7:]
+ } else {
+ return tok
+ }
+}
\ No newline at end of file
diff --git a/pub/common/utils/num.go b/pub/common/utils/num.go
new file mode 100644
index 0000000..8d0c572
--- /dev/null
+++ b/pub/common/utils/num.go
@@ -0,0 +1,20 @@
+package utils
+
+import (
+ "math/rand"
+ "time"
+)
+
+// CountDigits 有几位数字
+func CountDigits(i int) (count int) {
+ for i != 0 {
+ i /= 10
+ count++
+ }
+ return
+}
+
+func RandInt(min int, max int) int {
+ rand.Seed(time.Now().UnixNano())
+ return min + rand.Intn(max-min)
+}
diff --git a/pub/common/utils/page.go b/pub/common/utils/page.go
new file mode 100644
index 0000000..796db6e
--- /dev/null
+++ b/pub/common/utils/page.go
@@ -0,0 +1,19 @@
+package utils
+
+func GetPageRange(totalLen, pageNum, pageSize int) (start, end int) {
+ if pageNum < 1 {
+ pageNum = 1
+ }
+ if pageSize < 1 {
+ pageSize = 10
+ }
+ start = (pageNum - 1) * pageSize
+ if start > totalLen {
+ start = totalLen
+ }
+ end = start + pageSize
+ if end > totalLen {
+ end = totalLen
+ }
+ return
+}
diff --git a/pub/common/utils/path.go b/pub/common/utils/path.go
new file mode 100644
index 0000000..1b39d4c
--- /dev/null
+++ b/pub/common/utils/path.go
@@ -0,0 +1,72 @@
+package utils
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/PuerkitoBio/purell"
+)
+
+// PathInfo 路径信息
+type PathInfo struct {
+ Dir string // 目录
+ Ext string // 扩展名
+ Filename string // 没有扩展名的文件名名称
+ FullFileName string // 文件全名
+}
+
+// PathForInfo 解析路径
+// 返回文件,路径,文件名等信息
+func PathForInfo(path string) (pi PathInfo) {
+ if path == "" {
+ return
+ }
+ ext := filepath.Ext(path)
+ dir, name := filepath.Split(path)
+
+ pi.Dir = dir
+ pi.Ext = ext
+ pi.Filename = strings.TrimRight(name, ext)
+ pi.FullFileName = name
+ return
+}
+
+// JoinUrlPath 域名与uri拼接(走 purell 标准化,会剥掉尾部斜杠)。
+// 适用于 CDN 文件链接(m3u8 分段、vtt、静态图等)这类"不需要尾部斜杠"的场景。
+// 需要保留尾部斜杠的页面 URL(sitemap/rss/indexnow)请使用 JoinUrlPathKeepSlash。
+func JoinUrlPath(host string, uri string) (urlpath string) {
+ urlpath = joinRawUrlPath(host, uri)
+ if urlpath == "" {
+ return
+ }
+ // 标准化 URL
+ normalized, err := purell.NormalizeURLString(urlpath, purell.FlagsUsuallySafeGreedy)
+ if err != nil {
+ return urlpath
+ }
+ return normalized
+}
+
+// JoinUrlPathKeepSlash 域名与uri拼接,保留 uri 原始的尾部斜杠,不做任何标准化。
+// sitemap/rss/indexnow 的页面 URL 必须与路由注册的 /archives/:id/ 等完全一致,
+// 否则爬虫每次访问都会被 gin 的 RedirectTrailingSlash 301 到带 / 的地址,
+// 浪费抓取预算并造成重定向链。
+func JoinUrlPathKeepSlash(host string, uri string) string {
+ return joinRawUrlPath(host, uri)
+}
+
+// joinRawUrlPath 纯拼接,不做任何标准化。
+func joinRawUrlPath(host string, uri string) (urlpath string) {
+ if uri == "" {
+ return
+ }
+ switch {
+ case strings.HasSuffix(host, "/") && strings.HasPrefix(uri, "/"):
+ urlpath = host[0:len(host)-1] + uri
+ case !strings.HasSuffix(host, "/") && !strings.HasPrefix(uri, "/"):
+ urlpath = host + "/" + uri
+ default:
+ urlpath = host + uri
+ }
+ return
+}
diff --git a/pub/common/utils/redis.go b/pub/common/utils/redis.go
new file mode 100644
index 0000000..1064e73
--- /dev/null
+++ b/pub/common/utils/redis.go
@@ -0,0 +1,56 @@
+package utils
+
+import (
+ "bytes"
+ "time"
+ "videoweb/pub/common/enum/redisconst"
+ sErr "videoweb/pub/common/errors"
+ "videoweb/pub/redis"
+)
+
+const (
+ expireTime = 30 * 24 * time.Hour
+)
+
+type TokenRedis struct {
+}
+
+func NewTokenRedis() *TokenRedis {
+ return &TokenRedis{}
+}
+
+func (tr *TokenRedis) Save(userId int64, token string) (err error) {
+ if err = tr.Delete(userId); err != nil {
+ return
+ }
+ err = redis.Set(redisconst.AppUserTokenKey(userId), token, expireTime)
+ if err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+func (tr *TokenRedis) Exists(userId int64, token string) (has bool, err error) {
+ tokenbytes, err := tr.Get(userId)
+ if err != nil {
+ return
+ }
+ has = bytes.Compare(tokenbytes, []byte(token)) == 0
+ return
+}
+
+func (tr *TokenRedis) Get(userId int64) (token []byte, err error) {
+ token, err = redis.GetBytes(redisconst.AppUserTokenKey(userId))
+ if err != nil {
+ err = sErr.MakeWithErr(sErr.RedisGet, err)
+ }
+ return
+}
+
+func (tr *TokenRedis) Delete(userId int64) (err error) {
+ err = redis.Del(redisconst.AppUserTokenKey(userId))
+ if err != nil {
+ err = sErr.MakeWithErr(sErr.RedisDel, err)
+ }
+ return
+}
diff --git a/pub/common/utils/shellcmd.go b/pub/common/utils/shellcmd.go
new file mode 100644
index 0000000..7e51287
--- /dev/null
+++ b/pub/common/utils/shellcmd.go
@@ -0,0 +1,15 @@
+package utils
+
+import (
+ "bytes"
+ "os/exec"
+)
+
+func ShellCmd(cmdStr string) (string, error) {
+ cmd := exec.Command("/bin/bash", "-c", cmdStr)
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ ec := cmd.Run()
+
+ return out.String(), ec
+}
diff --git a/pub/common/utils/slices.go b/pub/common/utils/slices.go
new file mode 100644
index 0000000..aa5d237
--- /dev/null
+++ b/pub/common/utils/slices.go
@@ -0,0 +1,116 @@
+package utils
+
+import (
+ "golang.org/x/exp/constraints"
+ "sort"
+)
+
+func SliceConstruct[T any](fn func() T) T {
+ return fn()
+}
+
+type SliceWalkCallback func(index int) bool // true 继续
+func SliceWalk(length int, cb SliceWalkCallback) {
+ for i := 0; i < length; i++ {
+ if ok := cb(i); ok {
+ continue
+ } else {
+ break
+ }
+ }
+}
+
+type SliceIncludeFilter func(index int) bool // 返回 true 继续, false 中断
+func SliceInclude(length int, filter SliceIncludeFilter) (int, bool) {
+ for i := 0; i < length; i++ {
+ if ok := filter(i); ok {
+ return i, true
+ }
+ }
+ return -1, false
+}
+
+type SliceConstructNFilter[T any] func(index int) (T, bool) // 返回 是否保留,项值
+func SliceConstructN[T any](length int, filter SliceConstructNFilter[T]) []T {
+ ret := make([]T, 0)
+ for i := 0; i < length; i++ {
+ if item, keep := filter(i); keep {
+ ret = append(ret, item)
+ }
+ }
+ return ret
+}
+
+// SliceUnique 使用泛型实现切片去重
+func SliceUnique[T comparable](s []T) []T {
+ seen := make(map[T]bool)
+ j := 0
+ for _, v := range s {
+ if _, ok := seen[v]; !ok {
+ seen[v] = true
+ s[j] = v
+ j++
+ }
+ }
+ return s[:j]
+}
+
+// SlicesDifference 求两个集合的差集
+func SlicesDifference[T comparable](a, b []T) []T {
+ // 使用 map 存储集合 b 的元素,方便快速判断元素是否存在
+ seen := make(map[T]struct{}, len(b))
+ for _, v := range b {
+ seen[v] = struct{}{}
+ }
+
+ // 遍历集合 a,将不在集合 b 中的元素放入新的切片
+ diff := make([]T, 0)
+ for _, v := range a {
+ if _, ok := seen[v]; !ok {
+ diff = append(diff, v)
+ }
+ }
+
+ return diff
+}
+
+func RemoveSlicesElement[T comparable](slice []T, element T) []T {
+ // 使用两个指针,一个遍历,一个用来记录有效元素的位置
+ j := 0
+ for i := range slice {
+ if slice[i] != element {
+ slice[j] = slice[i]
+ j++
+ }
+ }
+ // 返回截取后的切片
+ return slice[:j]
+}
+
+func SlicesIntersect[T constraints.Ordered | ~string](slice1, slice2 []T) []T {
+ dict := make(map[T]int)
+ ret := make([]T, 0)
+ for _, v1 := range slice1 {
+ dict[v1]++
+ }
+ for _, v2 := range slice2 {
+ if _, exists := dict[v2]; exists {
+ ret = append(ret, v2)
+ }
+ }
+ return ret
+}
+
+func IsSliceEqual[T constraints.Ordered](src []T, dst []T) bool {
+ sort.Slice(src, func(i, j int) bool { return src[i] < src[j] })
+ sort.Slice(dst, func(i, j int) bool { return dst[i] < dst[j] })
+ if len(src) != len(dst) {
+ return false
+ }
+ for i := range src {
+ if src[i] != dst[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/pub/common/utils/strings.go b/pub/common/utils/strings.go
new file mode 100644
index 0000000..768b726
--- /dev/null
+++ b/pub/common/utils/strings.go
@@ -0,0 +1,27 @@
+package utils
+
+// BinarySearch BinarySearch
+func BinarySearch(s []string, t string, n int) (idx int) {
+ idx = -1
+ if n == 0 {
+ return
+ }
+ start, end := 0, n-1
+ for start <= end {
+ mid := (start + end) >> 1
+ if s[mid] < t {
+ start = mid + 1
+ } else if s[mid] > t {
+ end = mid - 1
+ } else {
+ idx = mid
+ break
+ }
+ }
+ return
+}
+
+var YesOrNo = map[bool]string{
+ true: "是",
+ false: "否",
+}
diff --git a/pub/common/utils/structToMap.go b/pub/common/utils/structToMap.go
new file mode 100644
index 0000000..f16d5ce
--- /dev/null
+++ b/pub/common/utils/structToMap.go
@@ -0,0 +1,52 @@
+package utils
+
+import (
+ "fmt"
+ "github.com/mitchellh/mapstructure"
+ "reflect"
+)
+
+func Struct2Map(obj interface{}) map[string]interface{} {
+ t := reflect.TypeOf(obj)
+ v := reflect.ValueOf(obj)
+ if t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ v = reflect.Indirect(v)
+ }
+ var data = make(map[string]interface{})
+ //func (v Value) NumField() int, 返回v持有的结构体类型值的字段数,如果v的Kind不是Struct会panic
+ for i := 0; i < t.NumField(); i++ {
+ n := t.Field(i).Name
+ vv := v.Field(i).Interface()
+ data[Capitalize(n)] = vv
+ }
+ return data
+}
+
+func Map2Struct(obj interface{}, mapInfo map[string]interface{}) interface{} {
+ err := mapstructure.Decode(mapInfo, obj)
+ if err != nil {
+ fmt.Println(err)
+ }
+ return obj
+}
+
+// Capitalize 字符首字母大写
+func Capitalize(str string) string {
+ var upperStr string
+ vv := []rune(str) // 后文有介绍
+ for i := 0; i < len(vv); i++ {
+ if i == 0 {
+ if vv[i] >= 65 && vv[i] <= 96 { // 后文有介绍
+ vv[i] += 32 // string的码表相差32位
+ upperStr += string(vv[i])
+ } else {
+ fmt.Println("Not begins with uppercase letter,")
+ return str
+ }
+ } else {
+ upperStr += string(vv[i])
+ }
+ }
+ return upperStr
+}
diff --git a/pub/common/utils/time.go b/pub/common/utils/time.go
new file mode 100644
index 0000000..94d2f32
--- /dev/null
+++ b/pub/common/utils/time.go
@@ -0,0 +1,276 @@
+package utils
+
+import (
+ "fmt"
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+var (
+ LocBJ, _ = time.LoadLocation("Asia/Shanghai")
+)
+
+func init() {
+ time.Local = LocBJ
+}
+
+func Now() time.Time {
+ return time.Now().Local()
+}
+
+func NowMillisecond() int64 {
+ return Now().UnixNano() / int64(time.Millisecond)
+}
+
+func BeginOfToday() time.Time {
+ t := Now()
+ return BeginOfTime(t)
+}
+
+func BeginOfTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func GetNextMonthTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month+1, day, 0, 0, 0, 0, t.Location())
+}
+func GetNextQuarterTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month+3, day, 0, 0, 0, 0, t.Location())
+}
+
+func GetNextYearTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year+1, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func GetForeverTime() time.Time {
+ t := time.Now()
+ year, month, day := t.Date()
+ return time.Date(year+999, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func IsoWeek() string {
+ year, week := time.Now().ISOWeek()
+ return fmt.Sprintf("%d%02d", year, week)
+}
+
+// Deprecated
+func EndOfToday() time.Time {
+ t := Now()
+ return EndOfTime(t)
+}
+
+func EndOfTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 23, 59, 59, 0, t.Location())
+}
+
+func GetDateString(t time.Time) string {
+ return t.In(LocBJ).Format(pubdef.DateISO)
+}
+
+func GetTodayDate(now time.Time) string {
+ return GetDateString(now.In(time.Local))
+}
+
+func GetYesterdayDate(now time.Time) string {
+ return GetDateString(now.Local().AddDate(0, 0, -1))
+}
+
+func GetDayBeforeYesterdayDate(now time.Time) string {
+ return GetDateString(now.Local().AddDate(0, 0, -2))
+}
+
+func GetDayRange(t time.Time) (dayStart time.Time, dayEnd time.Time) {
+ dayStart = BeginOfTime(t)
+ dayEnd = EndOfTime(t)
+ return
+}
+
+// GetDayRangeByDate 返回一天的开始和结束时间, 00:00:00 - 23:59:59
+func GetDayRangeByDate(date string) (dayStart time.Time, dayEnd time.Time, err error) {
+ var d time.Time
+ d, err = time.ParseInLocation(pubdef.DateISO, date, LocBJ)
+ if err != nil {
+ return
+ }
+ dayStart, dayEnd = GetDayRange(d)
+ return
+}
+
+func GetDayEnd(date string) time.Time {
+ d, _ := time.ParseInLocation(pubdef.DateISO, date, LocBJ)
+ return EndOfTime(d)
+}
+
+func GetDayBegin(date string) time.Time {
+ d, _ := time.ParseInLocation(pubdef.DateISO, date, LocBJ)
+ return BeginOfTime(d)
+}
+
+func BeginWithThisMonth() time.Time {
+ t := Now()
+ year, month, _ := t.Date()
+ return time.Date(year, month, 1, 0, 0, 0, 0, t.Location())
+}
+
+func BeginOfMonth(t time.Time) time.Time {
+ year, month, _ := t.In(time.Local).Date()
+ return time.Date(year, month, 1, 0, 0, 0, 0, t.Location())
+}
+
+func EndOfMonth(t time.Time) time.Time {
+ return BeginOfMonth(t).AddDate(0, 1, 0).Add(-time.Nanosecond)
+}
+
+func EndOfLastMonth(t time.Time) time.Time {
+ return BeginOfMonth(t).Add(-time.Nanosecond)
+}
+
+func EndOfLastMonthDate(t time.Time) string {
+ return GetDateString(EndOfLastMonth(t))
+}
+
+func EndOfLastWeek(t time.Time) time.Time {
+ return BeginOfWeekByTime(t).Add(-time.Nanosecond)
+}
+
+func EndOfLastWeekDate(t time.Time) string {
+ return GetDateString(EndOfLastWeek(t))
+}
+
+// Deprecated
+func BeginOfWeek() time.Time {
+ return BeginOfWeekByTime(Now())
+}
+
+func WeekDateRange(t time.Time) (string, string) {
+ b := BeginOfWeekByTime(t)
+ e := EndOfWeekByTime(t)
+ return GetDateString(b), GetDateString(e)
+}
+
+func MonthDateRange(t time.Time) (string, string) {
+ b := BeginOfMonth(t)
+ e := EndOfMonth(t)
+ return GetDateString(b), GetDateString(e)
+}
+
+func BeginOfWeekByTime(t time.Time) time.Time {
+ bt := BeginOfTime(t)
+ weekday := int(t.Weekday())
+ weekStartDayInt := int(time.Monday)
+ if weekday < weekStartDayInt {
+ weekday = weekday + 7 - weekStartDayInt
+ } else {
+ weekday = weekday - weekStartDayInt
+ }
+ return bt.AddDate(0, 0, -weekday)
+}
+
+func EndOfWeekByTime(t time.Time) time.Time {
+ return BeginOfWeekByTime(t).AddDate(0, 0, 7).Add(-time.Nanosecond)
+}
+
+func BeginTodayMillsecond() int64 {
+ return BeginOfToday().UnixNano() / int64(time.Millisecond)
+}
+
+// ServerCurrMillisecond 获得时间毫秒数 13位
+func ServerCurrMillisecond(t time.Time) int64 {
+ return t.UnixNano() / 1000000
+}
+
+func BeginWithTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
+}
+
+func EndWithTime(t time.Time) time.Time {
+ year, month, day := t.Date()
+ return time.Date(year, month, day, 23, 59, 59, 0, t.Location())
+}
+
+func IsToday(t time.Time) bool {
+ if t.IsZero() {
+ return false
+ }
+ today := Now()
+
+ return t.Year() == today.Year() &&
+ t.Month() == today.Month() &&
+ t.Day() == today.Day()
+}
+
+func IsSameYearAndMonth(t1, t2 time.Time) bool {
+ return t1.Year() == t2.Year() &&
+ t1.Month() == t2.Month()
+}
+
+// 获取月的第一天
+func GetFirstDateOfMonth(d time.Time) time.Time {
+ d = d.AddDate(0, 0, -d.Day()+1)
+ return GetZeroTime(d)
+}
+
+// 获取月的最后一天
+func GetLastDateOfMonth(d time.Time) time.Time {
+ return GetFirstDateOfMonth(d).AddDate(0, 1, -1)
+}
+
+// 获取周的星期一(星期一~星期日)
+func GetMondayOfWeek(d time.Time) time.Time {
+ offset := (int(d.Weekday()) + 6) % 7
+ d = d.AddDate(0, 0, -offset)
+ return GetZeroTime(d)
+}
+
+// 获取周的星期日(星期一~星期日)
+func GetSundayOfWeek(d time.Time) time.Time {
+ offset := 6 - (int(d.Weekday())+6)%7
+ d = d.AddDate(0, 0, offset)
+ return GetZeroTime(d)
+}
+
+// 获取某一天的0点时间
+func GetZeroTime(d time.Time) time.Time {
+ return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location())
+}
+
+// 获取当前整点时间
+func GetWholeTime(d time.Time) time.Time {
+ return time.Date(d.Year(), d.Month(), d.Day(), d.Hour(), 0, 0, 0, d.Location())
+}
+
+func MustParseTime(s string) time.Time {
+ layouts := []string{
+ time.RFC3339,
+ pubdef.DateISO,
+ pubdef.TimeISO,
+ }
+ for _, layout := range layouts {
+ t, err := time.ParseInLocation(layout, s, time.Local)
+ if err != nil {
+ continue
+ }
+ return t
+ }
+ panic(fmt.Errorf("cannot parse %s as time.Time", s))
+}
+
+// 查看两个时间差了多少天,差一秒都算一天
+func TimeSub(t1, t2 time.Time) int64 {
+ t1 = time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, time.Local)
+ t2 = time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, time.Local)
+
+ return int64(t1.Sub(t2).Hours() / 24)
+}
+func RecentMinute(tim time.Time, scaleMinute int64) time.Time {
+ minute := int64(tim.Minute())
+ lave := minute % scaleMinute
+ formatMinute := minute - lave
+ return time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), int(formatMinute), 0, 0, tim.Location())
+}
diff --git a/pub/common/utils/time_test.go b/pub/common/utils/time_test.go
new file mode 100644
index 0000000..dae7c5b
--- /dev/null
+++ b/pub/common/utils/time_test.go
@@ -0,0 +1,130 @@
+package utils
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func Test_GetDayRangeByDate(t *testing.T) {
+ begin, end, _ := GetDayRangeByDate("2022-04-08")
+ fmt.Println(begin, end)
+}
+
+func Test_GetDayRange(t *testing.T) {
+ now := time.Now()
+ fmt.Println(GetDayRange(now))
+}
+
+func Test_GetDateString(t *testing.T) {
+ fmt.Println(GetDateString(time.Now()))
+}
+
+func Test_GetTodayDate(t *testing.T) {
+ fmt.Println(GetTodayDate(time.Now()))
+}
+
+func Test_GetYesterdayDate(t *testing.T) {
+ fmt.Println(GetYesterdayDate(time.Now()))
+}
+
+func Test_GetDayBeforeYesterdayDate(t *testing.T) {
+ fmt.Println(GetDayBeforeYesterdayDate(time.Now()))
+}
+
+func Test_BeginOfWeekByTime(t *testing.T) {
+ {
+ // 如果当前是周日,获取到的结果应该是这周周一而不是下周周一
+ now := time.Date(2022, 4, 3, 1, 0, 0, 0, time.Local)
+ actual := BeginOfWeekByTime(now)
+ expect := time.Date(2022, 3, 28, 0, 0, 0, 0, time.Local)
+ if !reflect.DeepEqual(expect, actual) {
+ t.Fatalf("fail")
+ }
+ }
+ {
+ now := time.Date(2022, 3, 28, 0, 0, 0, 0, time.Local)
+ actual := BeginOfWeekByTime(now)
+ expect := time.Date(2022, 3, 28, 0, 0, 0, 0, time.Local)
+ if !reflect.DeepEqual(expect, actual) {
+ t.Fatalf("fail")
+ }
+ }
+}
+
+func Test_GetDayEnd(t *testing.T) {
+ fmt.Println(GetDayEnd("2020-04-09"))
+}
+
+func Test_GetDayBegin(t *testing.T) {
+ fmt.Println(GetDayBegin("2020-04-09"))
+}
+
+func Test_EndOfWeekByTime(t *testing.T) {
+ now := time.Now()
+ fmt.Println(EndOfWeekByTime(now))
+}
+
+func Test_EndOfMonth(t *testing.T) {
+ now := time.Now()
+ fmt.Println(BeginOfMonth(now))
+ fmt.Println(EndOfMonth(now))
+}
+
+func Test_WeekDateRange(t *testing.T) {
+ fmt.Println(WeekDateRange(time.Now()))
+}
+
+func Test_MonthDateRange(t *testing.T) {
+ fmt.Println(MonthDateRange(time.Now()))
+}
+
+func Test_EndOfLastWeek(t *testing.T) {
+ fmt.Println(EndOfLastWeek(time.Now()))
+}
+
+func Test_EndOfLastMonth(t *testing.T) {
+ fmt.Println(EndOfLastMonth(time.Now()))
+}
+
+func Test_EndOfLastWeekDate(t *testing.T) {
+ fmt.Println(EndOfLastWeekDate(time.Now()))
+}
+
+func Test_EndOfLastMonthDate(t *testing.T) {
+ fmt.Println(EndOfLastMonthDate(time.Now()))
+}
+
+func TestMustParseTime(t *testing.T) {
+ type args struct {
+ s string
+ }
+ tests := []struct {
+ name string
+ args args
+ want time.Time
+ }{
+ {
+ name: "case1",
+ args: args{
+ s: "2022-04-25",
+ },
+ want: time.Date(2022, 4, 25, 0, 0, 0, 0, time.Local),
+ },
+ {
+ name: "case2",
+ args: args{
+ s: "2022-04-25 11:12:13",
+ },
+ want: time.Date(2022, 4, 25, 11, 12, 13, 0, time.Local),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := MustParseTime(tt.args.s); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("MustParseTime() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pub/common/utils/uasplit.go b/pub/common/utils/uasplit.go
new file mode 100644
index 0000000..3d033eb
--- /dev/null
+++ b/pub/common/utils/uasplit.go
@@ -0,0 +1,30 @@
+package utils
+
+import (
+ "strings"
+ "videoweb/pub/common/enum/consts"
+)
+
+func UASplit(useragent string) (phoneModel string, phoneSys string, appVer string) {
+ if useragent == "" {
+ return
+ }
+ infos := strings.Split(useragent, ";")
+ if len(infos) != 3 {
+ return
+ }
+ phoneinfos := strings.Split(strings.Trim(infos[1], " "), " ")
+ if len(phoneinfos) == 2 {
+ phoneModel = strings.Trim(phoneinfos[0], " ")
+ phoneSys = strings.Trim(phoneinfos[1], " ")
+ }
+ appVer = strings.Trim(infos[2], " ")
+ return
+}
+
+func UADeviceType(ua string) string {
+ if strings.Contains(ua, "iPhone") {
+ return consts.IOS
+ }
+ return consts.Android
+}
diff --git a/pub/common/utils/utils.go b/pub/common/utils/utils.go
new file mode 100644
index 0000000..8c64d51
--- /dev/null
+++ b/pub/common/utils/utils.go
@@ -0,0 +1,63 @@
+package utils
+
+import (
+ "fmt"
+ "math/rand"
+ "runtime/debug"
+ "strings"
+ "time"
+ "videoweb/pub/common/log"
+
+ "golang.org/x/exp/constraints"
+)
+
+func RecoverError(onError func(err error)) {
+ if r := recover(); r != nil {
+ err := fmt.Errorf("internal error: %v", r)
+ if onError != nil {
+ onError(err)
+ }
+ log.Error(fmt.Sprintf("[Panic] RecoverError err: %+v\n", r) + string(debug.Stack()))
+ }
+}
+
+func GetRandom(min, max int) int64 {
+ rand.Seed(time.Now().UnixNano())
+ return int64(rand.Intn(max-min+1) + min)
+}
+func EnsureRange[T constraints.Ordered](value, min, max T) T {
+ if value < min {
+ value = min
+ }
+ if value > max {
+ value = max
+ }
+ return value
+}
+
+func InRange[T constraints.Ordered](value, min, max T) bool {
+ if value < min {
+ return false
+ }
+ if value > max {
+ return false
+ }
+ return true
+}
+
+// Shuffle 使用 Fisher-Yates 算法对切片进行随机排序
+func Shuffle[T any](slice []T) {
+ rand.Seed(time.Now().UnixNano())
+ n := len(slice)
+ for i := n - 1; i > 0; i-- {
+ j := rand.Intn(i + 1)
+ slice[i], slice[j] = slice[j], slice[i]
+ }
+}
+func GetRandomMacAddress() string {
+ buf := make([]byte, 6)
+ rand.Read(buf)
+ buf[0] |= 2
+ mac := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5])
+ return strings.ToUpper(mac)
+}
diff --git a/pub/common/utils/verify.go b/pub/common/utils/verify.go
new file mode 100644
index 0000000..fb20ae5
--- /dev/null
+++ b/pub/common/utils/verify.go
@@ -0,0 +1,10 @@
+package utils
+
+import "regexp"
+
+// mobile verify
+func VerifyMobileFormat(mobileNum string) bool {
+ regular := "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"
+ reg := regexp.MustCompile(regular)
+ return reg.MatchString(mobileNum)
+}
diff --git a/pub/common/utils/version.go b/pub/common/utils/version.go
new file mode 100644
index 0000000..e2769d8
--- /dev/null
+++ b/pub/common/utils/version.go
@@ -0,0 +1,43 @@
+package utils
+
+import (
+ "strconv"
+ "strings"
+)
+
+func getSubVersions(ver string) []int {
+ var vers [4]int
+ vv := strings.Split(ver, ".")
+ for i, v := range vv {
+ vers[i], _ = strconv.Atoi(v)
+ }
+ return vers[:]
+}
+
+// VersionCompare
+func VersionCompare(a, b string) int {
+
+ aa := getSubVersions(a)
+ bb := getSubVersions(b)
+
+ for i := 0; i < len(aa) && i < len(bb); i++ {
+
+ if aa[i] == bb[i] {
+ continue
+ }
+
+ if aa[i] < bb[i] {
+ return -1
+ }
+
+ return 1
+ }
+
+ if len(aa) == len(bb) {
+ return 0
+ }
+ if len(aa) < len(bb) {
+ return -1
+ }
+ return 1
+}
diff --git a/pub/common/utils/version_test.go b/pub/common/utils/version_test.go
new file mode 100644
index 0000000..b1adcd7
--- /dev/null
+++ b/pub/common/utils/version_test.go
@@ -0,0 +1,19 @@
+package utils
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestVersionCompare(t *testing.T) {
+
+ fmt.Printf("2.10.9 vs 2.11 = %d\n", VersionCompare("2.10.9", "2.11"))
+ fmt.Printf("2.11.9 vs 2.11 = %d\n", VersionCompare("2.11.9", "2.11"))
+ fmt.Printf("1.10.9 vs 2.11 = %d\n", VersionCompare("1.10.9", "2.11"))
+ fmt.Printf("2.10.9 vs 2.10.1 = %d\n", VersionCompare("2.10.9", "2.11.1"))
+ fmt.Printf("2.10.9 vs 2.10.10 = %d\n", VersionCompare("2.10.9", "2.10.10"))
+ fmt.Printf("2.10.9 vs 2.10.9.0 = %d\n", VersionCompare("2.10.9", "2.10.9.0"))
+ fmt.Printf("1 vs 1.0.0.0 = %d\n", VersionCompare("2.10.9", "2.10.9.0"))
+ fmt.Printf("2.10.9 vs 2.10.9.-1 = %d\n", VersionCompare("2.10.9", "2.10.9.-1"))
+
+}
diff --git a/pub/common/v10/upgradev10.go b/pub/common/v10/upgradev10.go
new file mode 100644
index 0000000..8908c46
--- /dev/null
+++ b/pub/common/v10/upgradev10.go
@@ -0,0 +1,55 @@
+package v10
+
+import (
+ "github.com/go-playground/validator/v10"
+ "reflect"
+ "sync"
+
+ "github.com/gin-gonic/gin/binding"
+)
+
+type DefaultValidator struct {
+ once sync.Once
+ validate *validator.Validate
+}
+
+var _ binding.StructValidator = &DefaultValidator{}
+
+func (v *DefaultValidator) ValidateStruct(obj interface{}) error {
+
+ if kindOfData(obj) == reflect.Struct {
+
+ v.lazyinit()
+
+ if err := v.validate.Struct(obj); err != nil {
+ return error(err)
+ }
+ }
+
+ return nil
+}
+
+func (v *DefaultValidator) Engine() interface{} {
+ v.lazyinit()
+ return v.validate
+}
+
+func (v *DefaultValidator) lazyinit() {
+ v.once.Do(func() {
+ v.validate = validator.New()
+ v.validate.SetTagName("binding")
+
+ // add any custom validations etc. here
+ })
+}
+
+func kindOfData(data interface{}) reflect.Kind {
+
+ value := reflect.ValueOf(data)
+ valueType := value.Kind()
+
+ if valueType == reflect.Ptr {
+ valueType = value.Elem().Kind()
+ }
+ return valueType
+}
diff --git a/pub/common/v10/validate.go b/pub/common/v10/validate.go
new file mode 100644
index 0000000..ef5ca53
--- /dev/null
+++ b/pub/common/v10/validate.go
@@ -0,0 +1,171 @@
+package v10
+
+import (
+ "github.com/nyaruka/phonenumbers"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// IsPureChar 匹配中文、字母、数字
+func IsPureChar(src string) bool {
+ return regexp.MustCompile("^[a-z0-9A-Z\u4e00-\u9fa5]+$").MatchString(src)
+}
+
+// IsPureChineseChar 匹配中文
+func IsPureChineseChar(src string) bool {
+ return regexp.MustCompile("[\u4e00-\u9fa5]").MatchString(src)
+}
+
+// 提取文字 包括数字 英文 不包含符号
+func ExtractPureEngChar(src string) string {
+ pureArray := regexp.MustCompile("[a-z0-9A-Z\u005f]").FindAllString(src, -1)
+ return strings.Join(pureArray, "")
+}
+
+// 提取文字 包括中文 数字 英文 不包含符号
+func ExtractPureChar(src string) string {
+ src = strings.TrimSpace(src)
+ pureArray := regexp.MustCompile("[a-z0-9A-Z\u4e00-\u9fa5]+").FindAllString(src, -1)
+ return strings.Join(pureArray, "")
+}
+
+// IsPhoneNumber 匹配所有国际化的手机号码(手机卡 + 上网卡)
+func IsGlobalizationPhoneNumber(phoneNumber string) bool {
+ num, _ := phonenumbers.Parse(phoneNumber, "CN")
+ return phonenumbers.IsValidNumber(num)
+}
+
+// IsEn 是否是英文的正则表达式验证
+func IsEn(data string) bool {
+ return regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(data)
+}
+
+// IsEnZh 是否是中英文的正则表达式验证
+func IsEnZh(data string) bool {
+ return regexp.MustCompile("^[a-zA-Z|\u4e00-\u9fa5]+$").MatchString(data)
+}
+
+// IsEnNum 是否是英文数字的正则表达式验证
+func IsEnNum(data string) bool {
+ return regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(data)
+}
+
+// IsEnNumSpe 是否是英文数字特殊符号的验证
+func IsEnNumSpe(data string) bool {
+ return regexp.MustCompile(`^[a-zA-Z0-9|~!@#$%^&*()/\|,.<>?"'();:_+-=\[\]{}]+$`).MatchString(data)
+}
+
+// IsObjectID 是否objectID的验证
+func IsObjectID(data string) bool {
+ _, err := primitive.ObjectIDFromHex(data)
+ if err != nil {
+ return false
+ }
+ return true
+}
+
+// IsLengthBetween 判断data字符串长度是否在范围内
+func IsLengthBetween(data string, min int, max int) bool {
+ return len(data) >= min && len(data) <= max
+}
+
+// IsNil nil判断
+func IsNil(i interface{}) bool {
+ vi := reflect.ValueOf(i)
+ if vi.Kind() == reflect.Ptr {
+ return vi.IsNil()
+ }
+ return false
+}
+
+// Phone 验证手机号码
+func Phone(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ if data, ok := field.Interface().(string); ok {
+ if len(data) <= 10 || len(data) >= 15 {
+ return false
+ }
+ if !IsGlobalizationPhoneNumber(data) {
+ return false
+ }
+ return true
+ }
+ return false
+}
+
+// Time_Utc 转换成UTC时间
+func Time_Utc(fl validator.FieldLevel) bool {
+ timeParse, err := time.Parse(time.RFC3339, fl.Field().String())
+ if err != nil {
+ return false
+ }
+ timevalue := reflect.ValueOf(timeParse)
+ fl.Field().Set(timevalue)
+ return true
+}
+
+// StrLength 验证长度
+func StrLength(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ num, err := strconv.Atoi(param)
+ if err != nil {
+ return false
+ }
+ if data, ok := field.Interface().(string); ok {
+ if len(data) == num {
+ return true
+ }
+ }
+ return false
+}
+
+// NeedType 需要类型
+func NeedType(
+ v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
+ field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
+) bool {
+ if data, ok := field.Interface().(string); ok {
+ switch param {
+ case "en": // 英文
+ return IsEn(data)
+ case "enZh": // 中英文
+ return IsEnZh(data)
+ case "enNum": // 英文数字
+ return IsEnNum(data)
+ case "enNumSpe": // 英文数字特殊符号
+ return IsEnNumSpe(data)
+ case "objectID": // 是否为mongo的id
+ return IsObjectID(data)
+ }
+ }
+ return false
+}
+
+// RegisterValidator 注解自定义参数验证
+func RegisterValidator() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
+ //v.RegisterValidation("phone", Phone)
+ // v.RegisterValidation("len", StrLength)
+ // v.RegisterValidation("min", validator.IsGte)
+ // v.RegisterValidation("max", validator.IsLte)
+ // v.RegisterValidation("eq", validator.IsEq)
+ // v.RegisterValidation("type", NeedType)
+ v.RegisterValidation("time_utc", Time_Utc)
+ }
+ c.Next()
+ }
+}
diff --git a/pub/common/wordsfilter/filter.go b/pub/common/wordsfilter/filter.go
new file mode 100644
index 0000000..fa03ce9
--- /dev/null
+++ b/pub/common/wordsfilter/filter.go
@@ -0,0 +1,113 @@
+package wordsfilter
+
+import (
+ "bufio"
+ "io"
+ "net/http"
+ "os"
+ "regexp"
+ "time"
+)
+
+// Filter 敏感词过滤器
+type Filter struct {
+ trie *Trie
+ noise *regexp.Regexp
+}
+
+// New 返回一个敏感词过滤器
+func New() *Filter {
+ return &Filter{
+ trie: NewTrie(),
+ noise: regexp.MustCompile(`[\|\s&%$@*]+`),
+ }
+}
+
+// UpdateNoisePattern 更新去噪模式
+func (filter *Filter) UpdateNoisePattern(pattern string) {
+ filter.noise = regexp.MustCompile(pattern)
+}
+
+// LoadWordDict 加载敏感词字典
+func (filter *Filter) LoadWordDict(path string) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ return filter.Load(f)
+}
+
+// LoadNetWordDict 加载网络敏感词字典
+func (filter *Filter) LoadNetWordDict(url string) error {
+ c := http.Client{
+ Timeout: 5 * time.Second,
+ }
+ rsp, err := c.Get(url)
+ if err != nil {
+ return err
+ }
+ defer rsp.Body.Close()
+
+ return filter.Load(rsp.Body)
+}
+
+// Load common method to add words
+func (filter *Filter) Load(rd io.Reader) error {
+ buf := bufio.NewReader(rd)
+ for {
+ line, _, err := buf.ReadLine()
+ if err != nil {
+ if err != io.EOF {
+ return err
+ }
+ break
+ }
+ filter.trie.Add(string(line))
+ }
+
+ return nil
+}
+
+// AddWord 添加敏感词
+func (filter *Filter) AddWord(words ...string) {
+ filter.trie.Add(words...)
+}
+
+// DelWord 删除敏感词
+func (filter *Filter) DelWord(words ...string) {
+ filter.trie.Del(words...)
+}
+
+// Filter 过滤敏感词
+func (filter *Filter) Filter(text string) string {
+ return filter.trie.Filter(text)
+}
+
+// Replace 和谐敏感词
+func (filter *Filter) Replace(text string, repl rune) string {
+ return filter.trie.Replace(text, repl)
+}
+
+// FindIn 检测敏感词
+func (filter *Filter) FindIn(text string) (bool, string) {
+ text = filter.RemoveNoise(text)
+ return filter.trie.FindIn(text)
+}
+
+// FindAll 找到所有匹配词
+func (filter *Filter) FindAll(text string) []string {
+ return filter.trie.FindAll(text)
+}
+
+// Validate 检测字符串是否合法
+func (filter *Filter) Validate(text string) (bool, string) {
+ text = filter.RemoveNoise(text)
+ return filter.trie.Validate(text)
+}
+
+// RemoveNoise 去除空格等噪音
+func (filter *Filter) RemoveNoise(text string) string {
+ return filter.noise.ReplaceAllString(text, "")
+}
diff --git a/pub/common/wordsfilter/filter_test.go b/pub/common/wordsfilter/filter_test.go
new file mode 100644
index 0000000..49f7662
--- /dev/null
+++ b/pub/common/wordsfilter/filter_test.go
@@ -0,0 +1,396 @@
+package wordsfilter
+
+import (
+ "io"
+ "reflect"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+func TestLoadDict(t *testing.T) {
+ filter := New()
+ err := filter.LoadWordDict("./dict/dict.txt")
+ if err != nil {
+ t.Errorf("fail to load dict %v", err)
+ }
+}
+
+func TestLoadNetWordDict(t *testing.T) {
+ filter := New()
+ err := filter.LoadNetWordDict("https://raw.githubusercontent.com/importcjj/sensitive/master/dict/dict.txt")
+ if err != nil {
+ t.Errorf("fail to load dict %v", err)
+ }
+ if len(filter.trie.Root.Children) == 0 {
+ t.Errorf("load dict empty")
+ }
+}
+
+func TestLoad(t *testing.T) {
+ filter := New()
+ var r io.Reader
+ r = strings.NewReader("read")
+ err := filter.Load(r)
+ if err != nil {
+ t.Errorf("fail to load dict %v", err)
+ }
+ if len(filter.trie.Root.Children) == 0 {
+ t.Errorf("load dict empty")
+ }
+}
+
+func TestSensitiveFilter(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect string
+ DelWords []string
+ ThenExpect string
+ }{
+ {"我有一个东东西", "我有东", []string{"一个"}, "我有一"},
+ {"我有一个东西", "我", []string{"有一个东西"}, "我有"},
+ {"一个东西", "", []string{"一个", "东西"}, ""},
+ {"两个东西", "两西", []string{"个东"}, "两个"},
+ {"一个物体", "物体", []string{"一个"}, "一个物体"},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.Filter(tc.Text); got != tc.Expect {
+ t.Errorf("filter %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+
+ filter.DelWord(tc.DelWords...)
+
+ if got := filter.Filter(tc.Text); got != tc.ThenExpect {
+ t.Errorf("after del, filter %s, got %s, expect %s", tc.Text, got, tc.ThenExpect)
+ }
+
+ filter.AddWord(tc.DelWords...)
+ }
+
+}
+func TestSensitiveValidateSingleword(t *testing.T) {
+ filter := New()
+ filter.AddWord("东")
+
+ testcases := []struct {
+ Text string
+ ExpectPass bool
+ ExpectFirst string
+ }{
+ {"两个东西", false, "东"},
+ }
+
+ for _, tc := range testcases {
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ExpectFirst)
+ }
+ }
+
+}
+
+func TestSensitiveValidate(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+
+ testcases := []struct {
+ Text string
+ ExpectPass bool
+ ExpectFirst string
+ DelWords []string
+ ThenExpectPass bool
+ ThenExpectFirst string
+ }{
+ {"我有一@ |个东东西", false, "一个", []string{"一个"}, false, "个东"},
+ {"我有一个东东西", false, "一个", []string{"一个"}, false, "个东"},
+ {"我有一个东西", false, "有一个东西", []string{"有一个东西", "一个"}, false, "一个东西"},
+ {"一个东西", false, "一个", []string{"个东", "一个"}, false, "一个东西"},
+ {"两个东西", false, "个东", []string{"个东", "东西"}, true, ""},
+ {"一样东西", false, "东西", []string{"东西"}, true, ""},
+ }
+
+ for _, tc := range testcases {
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ExpectFirst)
+ }
+
+ filter.DelWord(tc.DelWords...)
+
+ if pass, first := filter.Validate(tc.Text); pass != tc.ThenExpectPass || first != tc.ThenExpectFirst {
+ t.Errorf("after del, validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ThenExpectPass, tc.ThenExpectFirst)
+ }
+
+ filter.AddWord(tc.DelWords...)
+ }
+
+}
+
+func TestSensitiveReplace(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect string
+ DelWords []string
+ ThenExpect string
+ }{
+ {"我有一个东东西", "我有**东**", []string{"一个"}, "我有一****"},
+ {"我有一个东西", "我*****", []string{"有一个东西"}, "我有****"},
+ {"一个东西", "****", []string{"一个东西", "一个", "东西"}, "一**西"},
+ {"两个东西", "两**西", []string{"个东"}, "两个**"},
+ {"一个物体", "**物体", []string{"一个"}, "一个物体"},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.Replace(tc.Text, '*'); got != tc.Expect {
+ t.Errorf("replace %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+
+ filter.DelWord(tc.DelWords...)
+
+ if got := filter.Replace(tc.Text, '*'); got != tc.ThenExpect {
+ t.Errorf("after del word, replace %s, got %s, expect %s", tc.Text, got, tc.ThenExpect)
+ }
+
+ filter.AddWord(tc.DelWords...)
+ }
+
+}
+
+func TestSensitiveFindAll(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect []string
+ }{
+ {"我有一个东东西", []string{"一个", "个东", "东西"}},
+ {"我有一个东西", []string{"有一个东西", "一个", "一个东西", "个东", "东西"}},
+ {"一个东西", []string{"一个", "一个东西", "个东", "东西"}},
+ {"两个东西", []string{"个东", "东西"}},
+ {"一个物体", []string{"一个"}},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.Expect, got) {
+ t.Errorf("findall %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+ }
+}
+
+func TestSensitiveFindallSingleword(t *testing.T) {
+ filter := New()
+ filter.AddWord("东")
+
+ testcases := []struct {
+ Text string
+ Expect []string
+ }{
+ {"两个东西", []string{"东"}},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.Expect, got) {
+ t.Errorf("findall %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+ }
+
+}
+
+func TestFindAllAfterDeleteSingleWord(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("一个东西")
+ filter.AddWord("一个")
+ filter.AddWord("东西")
+ filter.AddWord("个东")
+
+ testcases := []struct {
+ Text string
+ Expect []string
+ DelWord string
+ ThenExpect []string
+ }{
+ {"我有一个东东西", []string{"一个", "个东", "东西"}, "一个", []string{"个东", "东西"}},
+ {"我有一个东西", []string{"有一个东西", "一个", "一个东西", "个东", "东西"}, "有一个东西", []string{"一个", "一个东西", "个东", "东西"}},
+ {"一个东西", []string{"一个", "一个东西", "个东", "东西"}, "一个东西", []string{"一个", "个东", "东西"}},
+ {"两个东西", []string{"个东", "东西"}, "东西", []string{"个东"}},
+ {"一个物体", []string{"一个"}, "一个", nil},
+ }
+
+ for _, tc := range testcases {
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.Expect, got) {
+ t.Errorf("findall %s, got %s, expect %s", tc.Text, got, tc.Expect)
+ }
+
+ filter.DelWord(tc.DelWord)
+
+ if got := filter.FindAll(tc.Text); !reflect.DeepEqual(tc.ThenExpect, got) {
+ t.Errorf("after del a word, findall %s, got %s, expect %s", tc.Text, got, tc.ThenExpect)
+ }
+
+ filter.AddWord(tc.DelWord)
+ }
+}
+
+func TestDeleteWordNotExists(t *testing.T) {
+ filter := New()
+ filter.DelWord("Any")
+ filter.DelWord("Go", "Bad")
+}
+
+func TestDeleteWord2(t *testing.T) {
+ filter := New()
+ filter.AddWord("有一个东西")
+ filter.AddWord("有一个东")
+ filter.AddWord("有一个")
+
+ testcases := []struct {
+ Text string
+ ExpectPass bool
+ ExpectFirst string
+ DelWord string
+ ThenExpectFirst string
+ }{
+ {"我有一个东西", false, "有一个", "有一个", "有一个东"},
+ }
+
+ for _, tc := range testcases {
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ExpectFirst)
+ }
+
+ filter.DelWord(tc.DelWord)
+
+ if pass, first := filter.Validate(tc.Text); pass != tc.ExpectPass || first != tc.ThenExpectFirst {
+ t.Errorf("validate %s, got %v, %s, expect %v, %s", tc.Text, pass, first, tc.ExpectPass, tc.ThenExpectFirst)
+ }
+
+ filter.AddWord(tc.DelWord)
+ }
+}
+
+func TestFilter_LoadWordDict(t *testing.T) {
+ type fields struct {
+ trie *Trie
+ noise *regexp.Regexp
+ }
+ type args struct {
+ path string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filter := &Filter{
+ trie: tt.fields.trie,
+ noise: tt.fields.noise,
+ }
+ if err := filter.LoadWordDict(tt.args.path); (err != nil) != tt.wantErr {
+ t.Errorf("Filter.LoadWordDict() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestFilter_LoadNetWordDict(t *testing.T) {
+ type fields struct {
+ trie *Trie
+ noise *regexp.Regexp
+ }
+ type args struct {
+ url string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filter := &Filter{
+ trie: tt.fields.trie,
+ noise: tt.fields.noise,
+ }
+ if err := filter.LoadNetWordDict(tt.args.url); (err != nil) != tt.wantErr {
+ t.Errorf("Filter.LoadNetWordDict() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestFilter_Load(t *testing.T) {
+ type fields struct {
+ trie *Trie
+ noise *regexp.Regexp
+ }
+ type args struct {
+ rd io.Reader
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ filter := &Filter{
+ trie: tt.fields.trie,
+ noise: tt.fields.noise,
+ }
+ if err := filter.Load(tt.args.rd); (err != nil) != tt.wantErr {
+ t.Errorf("Filter.Load() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestIssue13(t *testing.T) {
+ filter := New()
+ filter.AddWord("words")
+ filter.AddWord("警察局")
+
+ r := filter.FindAll("words words words kill myself myself suicide警察局kill my self")
+ expected := []string{"words", "警察局"}
+
+ if !reflect.DeepEqual(r, expected) {
+ t.Errorf("%v expected, got %v", expected, r)
+ }
+}
diff --git a/pub/common/wordsfilter/trie_tree.go b/pub/common/wordsfilter/trie_tree.go
new file mode 100644
index 0000000..d06bcc4
--- /dev/null
+++ b/pub/common/wordsfilter/trie_tree.go
@@ -0,0 +1,272 @@
+package wordsfilter
+
+// Trie 短语组成的Trie树.
+type Trie struct {
+ Root *Node
+}
+
+// Node Trie树上的一个节点.
+type Node struct {
+ isRootNode bool
+ isPathEnd bool
+ Character rune
+ Children map[rune]*Node
+}
+
+// NewTrie 新建一棵Trie
+func NewTrie() *Trie {
+ return &Trie{
+ Root: NewRootNode(0),
+ }
+}
+
+// Add 添加若干个词
+func (tree *Trie) Add(words ...string) {
+ for _, word := range words {
+ tree.add(word)
+ }
+}
+
+func (tree *Trie) add(word string) {
+ var current = tree.Root
+ var runes = []rune(word)
+ for position := 0; position < len(runes); position++ {
+ r := runes[position]
+ if next, ok := current.Children[r]; ok {
+ current = next
+ } else {
+ newNode := NewNode(r)
+ current.Children[r] = newNode
+ current = newNode
+ }
+ if position == len(runes)-1 {
+ current.isPathEnd = true
+ }
+ }
+}
+
+func (tree *Trie) Del(words ...string) {
+ for _, word := range words {
+ tree.del(word)
+ }
+}
+
+func (tree *Trie) del(word string) {
+ var current = tree.Root
+ var runes = []rune(word)
+ for position := 0; position < len(runes); position++ {
+ r := runes[position]
+ if next, ok := current.Children[r]; !ok {
+ return
+ } else {
+ current = next
+ }
+
+ if position == len(runes)-1 {
+ current.SoftDel()
+ }
+ }
+}
+
+// Replace 词语替换
+func (tree *Trie) Replace(text string, character rune) string {
+ var (
+ parent = tree.Root
+ current *Node
+ runes = []rune(text)
+ length = len(runes)
+ left = 0
+ found bool
+ )
+
+ for position := 0; position < len(runes); position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found || (!current.IsPathEnd() && position == length-1) {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ // println(string(current.Character), current.IsPathEnd(), left)
+ if current.IsPathEnd() && left <= position {
+ for i := left; i <= position; i++ {
+ runes[i] = character
+ }
+ }
+
+ parent = current
+ }
+
+ return string(runes)
+}
+
+// Filter 直接过滤掉字符串中的敏感词
+func (tree *Trie) Filter(text string) string {
+ var (
+ parent = tree.Root
+ current *Node
+ left = 0
+ found bool
+ runes = []rune(text)
+ length = len(runes)
+ resultRunes = make([]rune, 0, length)
+ )
+
+ for position := 0; position < length; position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found || (!current.IsPathEnd() && position == length-1) {
+ resultRunes = append(resultRunes, runes[left])
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ if current.IsPathEnd() {
+ left = position + 1
+ parent = tree.Root
+ } else {
+ parent = current
+ }
+
+ }
+
+ resultRunes = append(resultRunes, runes[left:]...)
+ return string(resultRunes)
+}
+
+// Validate 验证字符串是否合法,如不合法则返回false和检测到
+// 的第一个敏感词
+func (tree *Trie) Validate(text string) (bool, string) {
+ const (
+ Empty = ""
+ )
+ var (
+ parent = tree.Root
+ current *Node
+ runes = []rune(text)
+ length = len(runes)
+ left = 0
+ found bool
+ )
+
+ for position := 0; position < len(runes); position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found || (!current.IsPathEnd() && position == length-1) {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ if current.IsPathEnd() && left <= position {
+ return false, string(runes[left : position+1])
+ }
+
+ parent = current
+ }
+
+ return true, Empty
+}
+
+// FindIn 判断text中是否含有词库中的词
+func (tree *Trie) FindIn(text string) (bool, string) {
+ validated, first := tree.Validate(text)
+ return !validated, first
+}
+
+// FindAll 找有所有包含在词库中的词
+func (tree *Trie) FindAll(text string) []string {
+ var matches []string
+ var (
+ parent = tree.Root
+ current *Node
+ runes = []rune(text)
+ length = len(runes)
+ left = 0
+ found bool
+ )
+
+ for position := 0; position < length; position++ {
+ current, found = parent.Children[runes[position]]
+
+ if !found {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ if current.IsPathEnd() && left <= position {
+ matches = append(matches, string(runes[left:position+1]))
+ }
+
+ if position == length-1 {
+ parent = tree.Root
+ position = left
+ left++
+ continue
+ }
+
+ parent = current
+ }
+
+ var i = 0
+ if count := len(matches); count > 0 {
+ set := make(map[string]struct{})
+ for i < count {
+ _, ok := set[matches[i]]
+ if !ok {
+ set[matches[i]] = struct{}{}
+ i++
+ continue
+ }
+ count--
+ copy(matches[i:], matches[i+1:])
+ }
+ return matches[:count]
+ }
+
+ return nil
+}
+
+// NewNode 新建子节点
+func NewNode(character rune) *Node {
+ return &Node{
+ Character: character,
+ Children: make(map[rune]*Node, 0),
+ }
+}
+
+// NewRootNode 新建根节点
+func NewRootNode(character rune) *Node {
+ return &Node{
+ isRootNode: true,
+ Character: character,
+ Children: make(map[rune]*Node, 0),
+ }
+}
+
+// IsLeafNode 判断是否叶子节点
+func (node *Node) IsLeafNode() bool {
+ return len(node.Children) == 0
+}
+
+// IsRootNode 判断是否为根节点
+func (node *Node) IsRootNode() bool {
+ return node.isRootNode
+}
+
+// IsPathEnd 判断是否为某个路径的结束
+func (node *Node) IsPathEnd() bool {
+ return node.isPathEnd
+}
+
+// SoftDel 置软删除状态
+func (node *Node) SoftDel() {
+ node.isPathEnd = false
+}
diff --git a/pub/common/wordsfilter/trie_tree_test.go b/pub/common/wordsfilter/trie_tree_test.go
new file mode 100644
index 0000000..46311f3
--- /dev/null
+++ b/pub/common/wordsfilter/trie_tree_test.go
@@ -0,0 +1,14 @@
+package wordsfilter
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestTrieTree(t *testing.T) {
+ tree := NewTrie()
+
+ tree.Add("习近平", "习大大")
+ fmt.Println(tree.Replace("你好吗 我支持习大大, 他的名字叫做习近平", '*'))
+ fmt.Println(tree.Filter("你好吗 我支持习大大, 他的名字叫做习近平"))
+}
diff --git a/pub/esmanager/client.go b/pub/esmanager/client.go
new file mode 100644
index 0000000..6dc05e4
--- /dev/null
+++ b/pub/esmanager/client.go
@@ -0,0 +1,165 @@
+package esmanager
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/elastic/go-elasticsearch/v7/esapi"
+)
+
+const (
+ AnalyzerIkSmart = "ik_smart"
+ AnalyzerIkMaxWord = "ik_max_word"
+ NumberOfShards = 1
+ NumberOfReplicas = 1
+)
+
+func (m *Manager) InitIndices(indexName string, setting M) error {
+ res, err := m.client.Indices.Create(indexName, func(request *esapi.IndicesCreateRequest) {
+ request.Body = marshalSearchM(setting)
+ })
+ defer res.Body.Close()
+ return err
+}
+
+// GetTokens 获取分词
+func (m *Manager) Tokens(index string, bind interface{}, query M, o ...func(*esapi.IndicesAnalyzeRequest)) error {
+ o = append(o,
+ m.client.Indices.Analyze.WithContext(context.Background()),
+ m.client.Indices.Analyze.WithIndex(index),
+ m.client.Indices.Analyze.WithBody(marshalSearchM(query)),
+ m.client.Indices.Analyze.WithPretty(),
+ )
+
+ res, err := m.client.Indices.Analyze(o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ err = unmarshalAnalyzeBody(bind, res)
+ return err
+}
+
+// search 搜索
+func (m *Manager) Search(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) error {
+ marshalSearchM(query)
+ o = append(o,
+ m.client.Search.WithContext(context.Background()),
+ m.client.Search.WithIndex(index),
+ m.client.Search.WithBody(marshalSearchM(query)),
+ m.client.Search.WithTrackTotalHits(true),
+ m.client.Search.WithPretty(),
+ )
+ res, err := m.client.Search(o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ err = unmarshalBody(bind, res)
+ return err
+}
+
+// 批量插入
+func (m *Manager) Bulk(index string, source M, o ...func(*esapi.BulkRequest)) error {
+ o = append(o,
+ m.client.Bulk.WithIndex(index),
+ m.client.Bulk.WithContext(context.Background()),
+ m.client.Bulk.WithRefresh("true"),
+ )
+ res, err := m.client.Bulk(marshalBulkM(source), o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", res))
+ }
+ return nil
+}
+
+// Count 获取数量
+func (m *Manager) Count(index string, query M, o ...func(*esapi.CountRequest)) (int64, error) {
+ o = append(o,
+ m.client.Count.WithIndex(index),
+ m.client.Count.WithBody(marshalSearchM(query)),
+ m.client.Count.WithContext(context.Background()),
+ )
+ res, err := m.client.Count(o...)
+ if err != nil {
+ return 0, err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return 0, errors.New(fmt.Sprintf("%+v", res))
+ }
+ type Bind struct {
+ Count int64 `json:"count"`
+ }
+ var bind = Bind{}
+ err = unmarshalStruct(&bind, res)
+ if err != nil {
+ return 0, err
+ }
+ return bind.Count, err
+}
+
+func (m *Manager) Get(index string, id string, bind interface{}, o ...func(*esapi.GetRequest)) error {
+ o = append(o,
+ m.client.Get.WithContext(context.Background()),
+ m.client.Get.WithPretty(),
+ )
+ res, err := m.client.Get(index, id, o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", res))
+ }
+ return unmarshalBody(bind, res)
+}
+
+func (m *Manager) Add(index string, id string, doc interface{}, o ...func(*esapi.IndexRequest)) error {
+ o = append(o,
+ m.client.Index.WithDocumentID(id),
+ m.client.Index.WithContext(context.Background()),
+ m.client.Index.WithPretty(),
+ m.client.Index.WithRefresh("true"),
+ )
+ res, err := m.client.Index(index, marshalStruct(doc), o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ return nil
+}
+
+func (m *Manager) Del(index string, id string, o ...func(*esapi.DeleteRequest)) error {
+ o = append(o,
+ m.client.Delete.WithContext(context.Background()),
+ m.client.Delete.WithPretty(),
+ m.client.Delete.WithRefresh("true"),
+ )
+ res, err := m.client.Delete(index, id, o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ return nil
+}
+
+func (m *Manager) DelMany(indexs []string, query M, o ...func(*esapi.DeleteByQueryRequest)) error {
+ o = append(o,
+ m.client.DeleteByQuery.WithContext(context.Background()),
+ m.client.DeleteByQuery.WithPretty(),
+ m.client.DeleteByQuery.WithRefresh(true),
+ )
+ res, err := m.client.DeleteByQuery(indexs, marshalSearchM(query), o...)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ return nil
+}
\ No newline at end of file
diff --git a/pub/esmanager/elastic.go b/pub/esmanager/elastic.go
new file mode 100644
index 0000000..ba00bbd
--- /dev/null
+++ b/pub/esmanager/elastic.go
@@ -0,0 +1,143 @@
+package esmanager
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/elastic/go-elasticsearch/v7/esapi"
+)
+
+type M map[string]interface{}
+
+type A []map[string]interface{}
+
+type MGetBody struct {
+ ID string `json:"_id"`
+}
+
+// ElasticResp 响应
+type Response = esapi.Response
+
+type Options struct {
+ Address []string
+ MaxIdleConnPerHost int
+ IdleConnTimeout time.Duration
+}
+
+func unmarshalAggregateBody(bind interface{}, resp *Response) error {
+ var v map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return err
+ }
+ if v != nil && len(v) > 0 {
+ if v, ok := v["aggregations"].(map[string]interface{}); ok {
+ b, _ := json.Marshal(v)
+ _ = json.Unmarshal(b, &bind)
+ }
+ }
+ return nil
+}
+
+func unmarshalAnalyzeBody(bind interface{}, resp *Response) error {
+ var v map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return err
+ }
+ if v != nil && len(v) > 0 {
+ b, _ := json.Marshal(v)
+ _ = json.Unmarshal(b, &bind)
+ }
+ return nil
+}
+
+func unmarshalBody(bind interface{}, resp *Response) error {
+ var data map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&data)
+ if err != nil {
+ return err
+ }
+ if data != nil && len(data) > 0 {
+ if hits1, ok := data["hits"].(map[string]interface{}); ok {
+ b, _ := json.Marshal(hits1)
+ _ = json.Unmarshal(b, &bind)
+ } else if _, ok := data["docs"].([]interface{}); ok {
+ b, _ := json.Marshal(data["docs"])
+ _ = json.Unmarshal(b, &bind)
+ } else if _, ok := data["_source"].(map[string]interface{}); ok {
+ b, _ := json.Marshal(data)
+ _ = json.Unmarshal(b, &bind)
+ }
+ }
+ return nil
+}
+
+func marshalSearchM(query M) io.Reader {
+ var buf bytes.Buffer
+ if query == nil || len(query) == 0 {
+ return nil
+ }
+ err := json.NewEncoder(&buf).Encode(query)
+ if err != nil {
+ return nil
+ }
+ return bytes.NewReader(buf.Bytes())
+}
+
+func marshalBulkM(source M) io.Reader {
+ var buf bytes.Buffer
+ for k, v := range source {
+ var meta = []byte(fmt.Sprintf(`{ "index" : { "_id" : "%s" } }%s`, k, "\n"))
+ var data, _ = json.Marshal(v)
+ data = append(data, "\n"...)
+ buf.Grow(len(meta) + len(data))
+ buf.Write(meta)
+ buf.Write(data)
+ }
+ return bytes.NewReader(buf.Bytes())
+}
+
+func marshalStruct(v interface{}) io.Reader {
+ var buf bytes.Buffer
+ if v == nil {
+ return nil
+ }
+ err := json.NewEncoder(&buf).Encode(v)
+ if err != nil {
+ return nil
+ }
+ return bytes.NewReader(buf.Bytes())
+}
+
+func unmarshalStruct(bind interface{}, resp *Response) error {
+ var v map[string]interface{}
+ defer func() { _ = resp.Body.Close() }()
+ if bind == nil { //传入空表示只执行 defer
+ return nil
+ }
+ err := json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return err
+ }
+ if v != nil && len(v) > 0 {
+ b, _ := json.Marshal(v)
+ _ = json.Unmarshal(b, &bind)
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/esmanager/manager.go b/pub/esmanager/manager.go
new file mode 100644
index 0000000..0f58f4e
--- /dev/null
+++ b/pub/esmanager/manager.go
@@ -0,0 +1,53 @@
+package esmanager
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/elastic/go-elasticsearch/v7"
+)
+
+var Instance *Manager
+
+type Manager struct {
+ client *elasticsearch.Client
+ conf *elasticsearch.Config
+}
+
+func NewManager(url string, user string, pw string) (*Manager, error) {
+ if url == "" {
+ return nil, errors.New(fmt.Sprintf("Elastic NewManager url is empty!"))
+ }
+ cfg := elasticsearch.Config{
+ Addresses: []string{url},
+ Transport: &http.Transport{
+ MaxIdleConnsPerHost: 10,
+ IdleConnTimeout: 30,
+ },
+ Username: user,
+ Password: pw,
+ }
+ return &Manager{conf: &cfg}, nil
+}
+
+func (m *Manager) Client() *elasticsearch.Client {
+ return m.client
+}
+
+func (m *Manager) ClientConnect() error {
+ c, err := elasticsearch.NewClient(*m.conf)
+ if err != nil {
+ return err
+ }
+ resp, err := c.Ping(c.Ping.WithContext(context.Background()))
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return errors.New(fmt.Sprintf("%+v", resp))
+ }
+ m.client = c
+ return nil
+}
\ No newline at end of file
diff --git a/pub/google/googleAuth.go b/pub/google/googleAuth.go
new file mode 100644
index 0000000..1ba566d
--- /dev/null
+++ b/pub/google/googleAuth.go
@@ -0,0 +1,92 @@
+package google
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base32"
+ "encoding/binary"
+ "fmt"
+ "strings"
+ "time"
+)
+
+type GoogleAuth struct {
+}
+
+func NewGoogleAuth() *GoogleAuth {
+ return &GoogleAuth{}
+}
+
+func (ga *GoogleAuth) un() int64 {
+ return time.Now().UnixNano() / 1000 / 30
+}
+
+func (ga *GoogleAuth) hmacSha1(key, data []byte) []byte {
+ h := hmac.New(sha1.New, key)
+ if total := len(data); total > 0 {
+ h.Write(data)
+ }
+ return h.Sum(nil)
+}
+
+func (ga *GoogleAuth) base32encode(src []byte) string {
+ return base32.StdEncoding.EncodeToString(src)
+}
+
+func (ga *GoogleAuth) base32decode(s string) ([]byte, error) {
+ return base32.StdEncoding.DecodeString(s)
+}
+
+func (ga *GoogleAuth) toBytes(value int64) []byte {
+ var result []byte
+ mask := int64(0xFF)
+ shifts := [8]uint16{56, 48, 40, 32, 24, 16, 8, 0}
+ for _, shift := range shifts {
+ result = append(result, byte((value>>shift)&mask))
+ }
+ return result
+}
+
+func (ga *GoogleAuth) toUint32(bts []byte) uint32 {
+ return (uint32(bts[0]) << 24) + (uint32(bts[1]) << 16) +
+ (uint32(bts[2]) << 8) + uint32(bts[3])
+}
+
+func (ga *GoogleAuth) oneTimePassword(key []byte, data []byte) uint32 {
+ hash := ga.hmacSha1(key, data)
+ offset := hash[len(hash)-1] & 0x0F
+ hashParts := hash[offset : offset+4]
+ hashParts[0] = hashParts[0] & 0x7F
+ number := ga.toUint32(hashParts)
+ return number % 1000000
+}
+
+func (ga *GoogleAuth) GetSecret() string {
+ var buf bytes.Buffer
+ binary.Write(&buf, binary.BigEndian, ga.un())
+ return strings.ToUpper(ga.base32encode(ga.hmacSha1(buf.Bytes(), nil)))
+}
+
+func (ga *GoogleAuth) GetCode(secret string) (string, error) {
+ secretUpper := strings.ToUpper(secret)
+ secretKey, err := ga.base32decode(secretUpper)
+ if err != nil {
+ return "", err
+ }
+ number := ga.oneTimePassword(secretKey, ga.toBytes(time.Now().Unix()/30))
+ return fmt.Sprintf("%06d", number), nil
+}
+
+func (ga *GoogleAuth) GetQrcode(user, secret string) string {
+ return fmt.Sprintf("otpauth://totp/%s?secret=%s", user, secret)
+}
+
+func (ga *GoogleAuth) VerifyCode(secret, code string) (bool, error) {
+ _code, err := ga.GetCode(secret)
+ fmt.Println(_code, code, err)
+ if err != nil {
+ return false, err
+ }
+ return _code == code, nil
+}
diff --git a/pub/models/analyticsdailystat/analytics.go b/pub/models/analyticsdailystat/analytics.go
new file mode 100644
index 0000000..75d247b
--- /dev/null
+++ b/pub/models/analyticsdailystat/analytics.go
@@ -0,0 +1,182 @@
+package analyticsdailystat
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.AnalyticsDailyInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsDailyInfo.AppId, Value: 1},
+ {Key: FN.AnalyticsDailyInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsDailyInfo.SumDate, Value: 1},
+ {Key: FN.AnalyticsDailyInfo.AppId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsDailyInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(AnalyticsDailyInfo:initAnalyticsDailyInfoIndex) Create AnalyticsDailyInfo Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneAnalyticsDailyInfo 插入一个评论
+func InsertOneAnalyticsDailyInfo(ctx context.Context, AnalyticsDailyInfo *AnalyticsDailyInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, AnalyticsDailyInfo); err != nil {
+ log.Warn("AnalyticsDailyInfo:InsertOneAnalyticsDailyInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneAnalyticsDailyInfo 删除一个评论
+func DelOneAnalyticsDailyInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:DelOneAnalyticsDailyInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyAnalyticsDailyInfo 删除一个评论评论
+func DelManyAnalyticsDailyInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:DelOneAnalyticsDailyInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneAnalyticsDailyInfo 查找一个评论
+func FindOneAnalyticsDailyInfo(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *AnalyticsDailyInfo, *srvdef.SrvErr) {
+ var AnalyticsDailyInfo AnalyticsDailyInfo
+ if err := db.FindOneEx(
+ ctx, &AnalyticsDailyInfo, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("AnalyticsDailyInfo:FindOneAnalyticsDailyInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &AnalyticsDailyInfo, nil
+}
+
+// CountAnalyticsDailyInfos 统计评论视频评论数量
+func CountAnalyticsDailyInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:CountAnalyticsDailyInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneAnalyticsDailyInfo 更新一个评论
+func UpdateOneAnalyticsDailyInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:UpdateOneAnalyticsDailyInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("AnalyticsDailyInfo:UpdateOneAnalyticsDailyInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyAnalyticsDailyInfo 更新一批评论
+func UpdateManyAnalyticsDailyInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsDailyInfo:UpdateManyAnalyticsDailyInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("AnalyticsDailyInfo:UpdateManyAnalyticsDailyInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrAnalyticsDailyInfoNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyAnalyticsDailyInfo 查找一批评论
+func FindManyAnalyticsDailyInfo(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (AnalyticsDailyInfoList, *srvdef.SrvErr) {
+ data := make(AnalyticsDailyInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("AnalyticsDailyInfo:FindManyAnalyticsDailyInfo db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyAnalyticsDailyInfo 插入一批用户
+func InsertManyAnalyticsDailyInfo(ctx context.Context, histrory *AnalyticsDailyInfoList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("AnalyticsDailyInfo:InsertManyAnalyticsDailyInfo db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateAnalyticsDailyInfo 聚合查找一批数据
+func AggregateAnalyticsDailyInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsDailyInfo:AggregateAnalyticsDailyInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateAnalyticsDailyInfo 聚合查找一批数据
+func ObjectAggregateAnalyticsDailyInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsDailyInfo:AggregateAnalyticsDailyInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/analyticsdailystat/tag.go b/pub/models/analyticsdailystat/tag.go
new file mode 100755
index 0000000..d98856c
--- /dev/null
+++ b/pub/models/analyticsdailystat/tag.go
@@ -0,0 +1,42 @@
+package analyticsdailystat
+
+type TagAnalyticsDailyInfo struct {
+ AppId string
+ AppName string
+ CreatedAt string
+ Id string
+ LeadAmount string
+ LeadNewCount string
+ SumDate string
+ UpdatedAt string
+ YestodayActiveUsers string
+ YestodayNewUsers string
+ YestodayPV string
+ YestodaySessions string
+ YestodayUV string
+}
+
+var FN struct {
+ AnalyticsDailyInfo TagAnalyticsDailyInfo
+}
+
+var TN struct {
+ AnalyticsDailyInfo string
+}
+
+func init() {
+ FN.AnalyticsDailyInfo.AppId = "appId"
+ FN.AnalyticsDailyInfo.AppName = "appName"
+ FN.AnalyticsDailyInfo.CreatedAt = "createdAt"
+ FN.AnalyticsDailyInfo.Id = "_id"
+ FN.AnalyticsDailyInfo.LeadAmount = "leadAmount"
+ FN.AnalyticsDailyInfo.LeadNewCount = "leadNewCount"
+ FN.AnalyticsDailyInfo.SumDate = "sumDate"
+ FN.AnalyticsDailyInfo.UpdatedAt = "updatedAt"
+ FN.AnalyticsDailyInfo.YestodayActiveUsers = "yestodayActiveUsers"
+ FN.AnalyticsDailyInfo.YestodayNewUsers = "yestodayNewUsers"
+ FN.AnalyticsDailyInfo.YestodayPV = "yestodayPV"
+ FN.AnalyticsDailyInfo.YestodaySessions = "yestodaySessions"
+ FN.AnalyticsDailyInfo.YestodayUV = "yestodayUV"
+ TN.AnalyticsDailyInfo = "analytics_daily_info"
+}
diff --git a/pub/models/analyticsdailystat/type.go b/pub/models/analyticsdailystat/type.go
new file mode 100644
index 0000000..943e3ba
--- /dev/null
+++ b/pub/models/analyticsdailystat/type.go
@@ -0,0 +1,27 @@
+package analyticsdailystat
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 数据统计
+// @table:analytics_daily_info
+type AnalyticsDailyInfo struct {
+ Id primitive.ObjectID `bson:"_id,omitempty"`
+ SumDate time.Time `json:"sumDate" bson:"sumDate"` // 时间半小时
+ AppId int64 `json:"appId" bson:"appId"` // appId
+ AppName string `json:"appName" bson:"appName"` // APP名称
+ YestodayPV int64 `json:"yestodayPV" bson:"yestodayPV"` // 页面浏览量 (PV)
+ YestodayUV int64 `json:"yestodayUV" bson:"yestodayUV"` // 独立用户数 (UV)
+ YestodayActiveUsers int64 `json:"yestodayActiveUsers" bson:"yestodayActiveUsers"` // 活跃用户数
+ YestodaySessions int64 `json:"yestodaySessions" bson:"yestodaySessions"` // 会话数
+ YestodayNewUsers int64 `json:"yestodayNewUsers" bson:"yestodayNewUsers"` // 新增数量
+ LeadNewCount float64 `json:"leadNewCount" bson:"leadNewCount"` // 日导量新增
+ LeadAmount float64 `json:"leadAmount" bson:"leadAmount"` // 日导量充值
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type AnalyticsDailyInfoList []*AnalyticsDailyInfo
diff --git a/pub/models/analyticsstat/analytics.go b/pub/models/analyticsstat/analytics.go
new file mode 100644
index 0000000..d4c55ce
--- /dev/null
+++ b/pub/models/analyticsstat/analytics.go
@@ -0,0 +1,182 @@
+package analyticsstat
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.AnalyticsInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsInfo.AppId, Value: 1},
+ {Key: FN.AnalyticsInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsInfo.SumDate, Value: 1},
+ {Key: FN.AnalyticsInfo.AppId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.AnalyticsInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(AnalyticsInfo:initAnalyticsInfoIndex) Create AnalyticsInfo Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneAnalyticsInfo 插入一个评论
+func InsertOneAnalyticsInfo(ctx context.Context, AnalyticsInfo *AnalyticsInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, AnalyticsInfo); err != nil {
+ log.Warn("AnalyticsInfo:InsertOneAnalyticsInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneAnalyticsInfo 删除一个评论
+func DelOneAnalyticsInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsInfo:DelOneAnalyticsInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyAnalyticsInfo 删除一个评论评论
+func DelManyAnalyticsInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsInfo:DelOneAnalyticsInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneAnalyticsInfo 查找一个评论
+func FindOneAnalyticsInfo(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *AnalyticsInfo, *srvdef.SrvErr) {
+ var AnalyticsInfo AnalyticsInfo
+ if err := db.FindOneEx(
+ ctx, &AnalyticsInfo, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("AnalyticsInfo:FindOneAnalyticsInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &AnalyticsInfo, nil
+}
+
+// CountAnalyticsInfos 统计评论视频评论数量
+func CountAnalyticsInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("AnalyticsInfo:CountAnalyticsInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneAnalyticsInfo 更新一个评论
+func UpdateOneAnalyticsInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsInfo:UpdateOneAnalyticsInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("AnalyticsInfo:UpdateOneAnalyticsInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyAnalyticsInfo 更新一批评论
+func UpdateManyAnalyticsInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("AnalyticsInfo:UpdateManyAnalyticsInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("AnalyticsInfo:UpdateManyAnalyticsInfo db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrAnalyticsInfoNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyAnalyticsInfo 查找一批评论
+func FindManyAnalyticsInfo(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (AnalyticsInfoList, *srvdef.SrvErr) {
+ data := make(AnalyticsInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("AnalyticsInfo:FindManyAnalyticsInfo db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyAnalyticsInfo 插入一批用户
+func InsertManyAnalyticsInfo(ctx context.Context, histrory *AnalyticsInfoList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("AnalyticsInfo:InsertManyAnalyticsInfo db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateAnalyticsInfo 聚合查找一批数据
+func AggregateAnalyticsInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsInfo:AggregateAnalyticsInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateAnalyticsInfo 聚合查找一批数据
+func ObjectAggregateAnalyticsInfo(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("AnalyticsInfo:AggregateAnalyticsInfo db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/analyticsstat/tag.go b/pub/models/analyticsstat/tag.go
new file mode 100755
index 0000000..b7418b6
--- /dev/null
+++ b/pub/models/analyticsstat/tag.go
@@ -0,0 +1,42 @@
+package analyticsstat
+
+type TagAnalyticsInfo struct {
+ ActiveUsers string
+ AppId string
+ AppName string
+ CreatedAt string
+ DaySumDate string
+ Id string
+ SumDate string
+ UpdatedAt string
+ YestodayActiveUsers string
+ YestodayNewUsers string
+ YestodayPV string
+ YestodaySessions string
+ YestodayUV string
+}
+
+var FN struct {
+ AnalyticsInfo TagAnalyticsInfo
+}
+
+var TN struct {
+ AnalyticsInfo string
+}
+
+func init() {
+ FN.AnalyticsInfo.ActiveUsers = "activeUsers"
+ FN.AnalyticsInfo.AppId = "appId"
+ FN.AnalyticsInfo.AppName = "appName"
+ FN.AnalyticsInfo.CreatedAt = "createdAt"
+ FN.AnalyticsInfo.DaySumDate = "daySumDate"
+ FN.AnalyticsInfo.Id = "_id"
+ FN.AnalyticsInfo.SumDate = "sumDate"
+ FN.AnalyticsInfo.UpdatedAt = "updatedAt"
+ FN.AnalyticsInfo.YestodayActiveUsers = "yestodayActiveUsers"
+ FN.AnalyticsInfo.YestodayNewUsers = "yestodayNewUsers"
+ FN.AnalyticsInfo.YestodayPV = "yestodayPV"
+ FN.AnalyticsInfo.YestodaySessions = "yestodaySessions"
+ FN.AnalyticsInfo.YestodayUV = "yestodayUV"
+ TN.AnalyticsInfo = "analytics_info"
+}
diff --git a/pub/models/analyticsstat/type.go b/pub/models/analyticsstat/type.go
new file mode 100644
index 0000000..e455267
--- /dev/null
+++ b/pub/models/analyticsstat/type.go
@@ -0,0 +1,27 @@
+package analyticsstat
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 数据统计
+// @table:analytics_info
+type AnalyticsInfo struct {
+ Id primitive.ObjectID `bson:"_id,omitempty"`
+ SumDate time.Time `json:"sumDate" bson:"sumDate"` // 时间半小时
+ DaySumDate time.Time `json:"daySumDate" bson:"daySumDate"` // 当天
+ AppId int64 `json:"appId" bson:"appId"` // appId
+ AppName string `json:"appName" bson:"appName"` // APP名称
+ ActiveUsers int64 `json:"activeUsers" bson:"activeUsers"` // 最近半小时活跃数
+ YestodayPV int64 `json:"yestodayPV" bson:"yestodayPV"` // 页面浏览量 (PV)
+ YestodayUV int64 `json:"yestodayUV" bson:"yestodayUV"` // 独立用户数 (UV)
+ YestodayActiveUsers int64 `json:"yestodayActiveUsers" bson:"yestodayActiveUsers"` // 活跃用户数
+ YestodaySessions int64 `json:"yestodaySessions" bson:"yestodaySessions"` // 会话数
+ YestodayNewUsers int64 `json:"yestodayNewUsers" bson:"yestodayNewUsers"` // 新增数量
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+
+type AnalyticsInfoList []*AnalyticsInfo
diff --git a/pub/models/cfg/dbhelper.go b/pub/models/cfg/dbhelper.go
new file mode 100644
index 0000000..6e8c6d5
--- /dev/null
+++ b/pub/models/cfg/dbhelper.go
@@ -0,0 +1,9 @@
+package cfg
+
+import (
+ "videoweb/pub/mongos"
+)
+
+var (
+ sysCfgColl *mongos.Collection
+)
diff --git a/pub/models/cfg/dbop.go b/pub/models/cfg/dbop.go
new file mode 100644
index 0000000..04ab8b5
--- /dev/null
+++ b/pub/models/cfg/dbop.go
@@ -0,0 +1,55 @@
+package cfg
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ sysCfgColl = mongos.Shop.Coll(TN.SysCfg)
+}
+
+// FindOneSysCfg 查找一个系统配置
+func FindOneSysCfg(ctx context.Context, filter bson.M) (
+ *SysCfg, *srvdef.SrvErr) {
+ var sysCfg SysCfg
+ if err := sysCfgColl.FindOneEx(
+ ctx, &sysCfg, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("cfg:FindOneSysCfg db.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &sysCfg, nil
+}
+
+// UpdateOneSysCfg 更新一个系统配置
+func UpdateOneSysCfg(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := sysCfgColl.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("cfg:UpdateOneSysCfg db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+func DeleteOneSysCfg(ctx context.Context, filter bson.M) *srvdef.SrvErr {
+ _, err := sysCfgColl.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("cfg:DeleteOneSysCfg db.DeleteOne failed", log.Any("filter", filter), log.E(err))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/cfg/tag.go b/pub/models/cfg/tag.go
new file mode 100755
index 0000000..b6efffc
--- /dev/null
+++ b/pub/models/cfg/tag.go
@@ -0,0 +1,68 @@
+package cfg
+
+type TagSysCfg struct {
+ AppAddr string
+ ForeverUrlAddr string
+ GitAddr string
+ GmailAddr string
+ Id string
+ ImgAppCdn string
+ ImgWebCdn string
+ MediaAppCdn string
+ MediaWebCdn string
+ NewUrlAddr string
+ NewUrlAddr1 string
+ NewUrlAddr2 string
+ QQ string
+ Telegram string
+ TelegramAdd string
+ TgGroup string
+ TranslationAddr string
+ TwitterAddr string
+ WatermarkImage string
+ WatermarkMedia string
+ WatermarkMediaTop string
+ WeiChart string
+ DHNewUrlAddr1 string
+ DHNewUrlAddr2 string
+ DHNewUrlAddr3 string
+ DHForeverUrlAddr string
+}
+
+var FN struct {
+ SysCfg TagSysCfg
+}
+
+var TN struct {
+ SysCfg string
+}
+
+func init() {
+ FN.SysCfg.AppAddr = "appAddr"
+ FN.SysCfg.ForeverUrlAddr = "foreverUrlAddr"
+ FN.SysCfg.GitAddr = "gitAddr"
+ FN.SysCfg.GmailAddr = "gmailAddr"
+ FN.SysCfg.Id = "_id"
+ FN.SysCfg.ImgAppCdn = "imgAppCdn"
+ FN.SysCfg.ImgWebCdn = "imgWebCdn"
+ FN.SysCfg.MediaAppCdn = "mediaAppCdn"
+ FN.SysCfg.MediaWebCdn = "mediaWebCdn"
+ FN.SysCfg.NewUrlAddr = "newUrlAddr"
+ FN.SysCfg.NewUrlAddr1 = "newUrlAddr1"
+ FN.SysCfg.NewUrlAddr2 = "newUrlAddr2"
+ FN.SysCfg.QQ = "qq"
+ FN.SysCfg.Telegram = "telegram"
+ FN.SysCfg.TelegramAdd = "telegramAdd"
+ FN.SysCfg.TgGroup = "tgGroup"
+ FN.SysCfg.TranslationAddr = "translationAddr"
+ FN.SysCfg.TwitterAddr = "twitterAddr"
+ FN.SysCfg.WatermarkImage = "watermarkImage"
+ FN.SysCfg.WatermarkMedia = "watermarkMedia"
+ FN.SysCfg.WatermarkMediaTop = "watermarkMediaTop"
+ FN.SysCfg.WeiChart = "weiChart"
+ FN.SysCfg.DHNewUrlAddr1 = "dHNewUrlAddr1"
+ FN.SysCfg.DHNewUrlAddr2 = "dHNewUrlAddr2"
+ FN.SysCfg.DHNewUrlAddr3 = "dHNewUrlAddr3"
+ FN.SysCfg.DHForeverUrlAddr = "dHForeverUrlAddr"
+ TN.SysCfg = "sys_cfg"
+}
\ No newline at end of file
diff --git a/pub/models/cfg/type.go b/pub/models/cfg/type.go
new file mode 100644
index 0000000..bda8f9d
--- /dev/null
+++ b/pub/models/cfg/type.go
@@ -0,0 +1,42 @@
+package cfg
+
+// @table:sys_cfg
+type SysCfg struct {
+ Id int64 `json:"id" bson:"_id"`
+ UnencryptedCdn string `json:"unencryptedCdn" bson:"unencryptedCdn"` // 不加密资源使用的cdn
+ SitemapDefaultImages []string `json:"sitemapDefaultImages" bson:"sitemapDefaultImages"` // Sitemap里面默认封面图
+ ImgAppCdn string `json:"imgAppCdn" bson:"imgAppCdn"` // 图片CDN(app使用)
+ ImgWebCdn string `json:"imgWebCdn" bson:"imgWebCdn"` // 图片CDN(web使用)
+ MediaWebCdn string `json:"mediaWebCdn" bson:"mediaWebCdn"` // 视频CDN(web使用)
+ MediaAppCdn string `json:"mediaAppCdn" bson:"mediaAppCdn"` // 视频CDN(app使用)
+ WatermarkImage string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
+ WatermarkMedia string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件(底部)
+ WatermarkMediaTop string `json:"watermarkMediaTop" bson:"watermarkMediaTop"` // 视频水印文件(顶部)
+ Mp4Addr string `json:"mp4Addr" bson:"mp4Addr"` // mp4地址(web使用)
+ PdfAddr string `json:"pdfAddr" bson:"pdfAddr"` // 回家的路PDF
+ //网站管理
+ NewUrlAddr string `json:"newUrlAddr" bson:"newUrlAddr"` //最新地址
+ NewUrlAddr1 string `json:"newUrlAddr1" bson:"newUrlAddr1"` //最新地址(备用地址1)
+ NewUrlAddr2 string `json:"newUrlAddr2" bson:"newUrlAddr2"` //最新地址(备用地址2)
+ ForeverUrlAddr string `json:"foreverUrlAddr" bson:"foreverUrlAddr"` //永久地址
+ GmailAddr string `json:"gmailAddr" bson:"gmailAddr"` //邮箱地址
+ AppAddr string `json:"appAddr" bson:"appAddr"` //APP下载地址(落地页)
+ WeiChart string `json:"weiChart" bson:"weiChart"` //微信群
+ QQ string `json:"qq" bson:"qq"` //qq群
+ TgGroup string `json:"tgGroup" bson:"tgGroup"` //tg群
+ PotaoGroup string `json:"potaoGroup" bson:"potaoGroup"` //土豆群
+ GitAddr string `json:"gitAddr" bson:"gitAddr"` //git地址
+ TwitterAddr string `json:"twitterAddr" bson:"twitterAddr"` //推特地址
+ Telegram string `json:"telegram" bson:"telegram"` //商务tg
+ TelegramAdd string `json:"telegramAdd" bson:"telegramAdd"` //加入我们tg
+ TranslationAddr string `json:"translationAddr" bson:"translationAddr"` //应用导航
+ //SEO
+ WebTitle string `json:"webTitle" bson:"webTitle"` // 网站SEO标题
+ WebKeyworld []string `json:"webKeyworld" bson:"webKeyworld"` // 网站SEO关键字
+ WebDescription string `json:"webDescription" bson:"webDescription"` // 网站SEO描述
+ WebSiteName string `json:"webSiteName" bson:"webSiteName"` // 网站SEO网站名称
+ WebAddr string `json:"webAddr" bson:"webAddr"` // 网站SEO网站地址
+ DHNewUrlAddr1 string `json:"dhNewUrlAddr1" bson:"dhNewUrlAddr1"` // 导航页-线路1
+ DHNewUrlAddr2 string `json:"dhNewUrlAddr2" bson:"dhNewUrlAddr2"` // 导航页-线路2
+ DHNewUrlAddr3 string `json:"dhNewUrlAddr3" bson:"dhNewUrlAddr3"` // 导航页-线路3
+}
diff --git a/pub/models/commentmod/comment.go b/pub/models/commentmod/comment.go
new file mode 100644
index 0000000..c64c6c3
--- /dev/null
+++ b/pub/models/commentmod/comment.go
@@ -0,0 +1,206 @@
+package commentmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.Comment)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.Id, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.Level, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ObjectId, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ {Key: FN.Comment.Level, Value: 1},
+ {Key: FN.Comment.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.ParentsId, Value: 1},
+ {Key: FN.Comment.ReviewStatus, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.Comment.TopRank, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(comment:initCommentIndex) Create Comment Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneComment 插入一个评论
+func InsertOneComment(ctx context.Context, Comment *CommentModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, Comment); err != nil {
+ log.Warn("comment:InsertOneComment db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneComment 删除一个评论
+func DelOneComment(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("comment:DelOneComment db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// DelManyComment 删除一个评论评论
+func DelManyComment(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("comment:DelOneComment db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneComment 查找一个评论
+func FindOneComment(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (
+ *CommentModel, *srvdef.SrvErr) {
+ var Comment CommentModel
+ if err := db.FindOneEx(
+ ctx, &Comment, filter, opts...); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("comment:FindOneComment db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &Comment, nil
+}
+
+// CountComments 统计评论视频评论数量
+func CountComments(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("comment:CountComments db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneComment 更新一个评论
+func UpdateOneComment(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("comment:UpdateOneComment db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("comment:UpdateOneComment db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpdateManyComment 更新一批评论
+func UpdateManyComment(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("comment:UpdateManyComment db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("comment:UpdateManyComment db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrCommentNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyComment 查找一批评论
+func FindManyComment(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (CommentModelList, *srvdef.SrvErr) {
+ data := make(CommentModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("comment:FindManyComment db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// InsertManyComment 插入一批用户
+func InsertManyComment(ctx context.Context, histrory *CommentModelList) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, histrory); err != nil {
+ log.Warn("comment:InsertManyComment db.InsertMany failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// AggregateComment 聚合查找一批数据
+func AggregateComment(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("comment:AggregateComment db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateComment 聚合查找一批数据
+func ObjectAggregateComment(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("comment:AggregateComment db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/commentmod/tag.go b/pub/models/commentmod/tag.go
new file mode 100755
index 0000000..6554013
--- /dev/null
+++ b/pub/models/commentmod/tag.go
@@ -0,0 +1,44 @@
+package commentmod
+
+type TagComment struct {
+ CreatedAt string
+ Id string
+ Level string
+ ObjectId string
+ ParentsId string
+ ReplyUserName string
+ Text string
+ TopRank string
+ UpdatedAt string
+ UserName string
+ ReviewStatus string
+ CommentCount string
+ Ip string
+ ObjectName string
+}
+
+var FN struct {
+ Comment TagComment
+}
+
+var TN struct {
+ Comment string
+}
+
+func init() {
+ FN.Comment.CreatedAt = "createdAt"
+ FN.Comment.Id = "_id"
+ FN.Comment.Level = "level"
+ FN.Comment.ObjectId = "objectId"
+ FN.Comment.ParentsId = "parentsId"
+ FN.Comment.ReplyUserName = "replyUserName"
+ FN.Comment.Text = "text"
+ FN.Comment.TopRank = "topRank"
+ FN.Comment.UpdatedAt = "updatedAt"
+ FN.Comment.UserName = "userName"
+ FN.Comment.ReviewStatus = "reviewStatus"
+ FN.Comment.Ip = "ip"
+ FN.Comment.CommentCount = "commentCount"
+ FN.Comment.ObjectName = "objectName"
+ TN.Comment = "_comment"
+}
\ No newline at end of file
diff --git a/pub/models/commentmod/type.go b/pub/models/commentmod/type.go
new file mode 100644
index 0000000..cac1c55
--- /dev/null
+++ b/pub/models/commentmod/type.go
@@ -0,0 +1,26 @@
+package commentmod
+
+import (
+ "time"
+)
+
+// 评论管理
+// @table:comment
+type CommentModel struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // 评论ID
+ Ip string `json:"ip" bson:"ip"` // 评论IP
+ ObjectId int64 `json:"objectId" bson:"objectId"` // 对象ID
+ ObjectName string `json:"objectName" bson:"objectName"` // 对象名
+ UserName string `json:"userName" bson:"userName"` // 用户名称
+ Level int64 `json:"level" bson:"level"` // 评论级别
+ ParentsId int64 `json:"parentsId" bson:"parentsId"` // 上级评论ID
+ ReplyUserName string `json:"replyUserName" bson:"replyUserName"` // 回复名称
+ Text string `json:"text" bson:"text"` // 评论内容
+ CommentCount int64 `json:"commentCount" bson:"commentCount"` // 该评论回复数量
+ ReviewStatus int `json:"reviewStatus" bson:"reviewStatus"` // 审核状态 0 未审核 1审核成功 2 审核失败
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt"` // 创建时间
+ TopRank int64 `json:"topRank" bson:"topRank"` // 置顶排序
+}
+
+type CommentModelList []*CommentModel
\ No newline at end of file
diff --git a/pub/models/esmod/eshelper.go b/pub/models/esmod/eshelper.go
new file mode 100644
index 0000000..bd9885c
--- /dev/null
+++ b/pub/models/esmod/eshelper.go
@@ -0,0 +1,54 @@
+package esmod
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+const (
+ FiveMinuteScale = 5 //对齐时间(分钟)
+)
+
+var (
+ videoIndex = "webmdgw_video_index_v3"
+)
+
+func GetVideoIndex() string {
+ return videoIndex
+}
+
+func GetRecordByESIndex(ctx context.Context, esIndex string) (
+ *ESTimestamp, *srvdef.SrvErr) {
+
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ esRecord, serr := FindOneESRecord(ctx, filter)
+ if serr != nil {
+ if serr.Code != srvdef.ErrNoData {
+ return nil, serr
+ }
+ return nil, nil
+ }
+ return &esRecord.Timestamp, nil
+}
+
+func SetOneEsRecord(ctx context.Context, esIndex string,
+ timestamp *ESTimestamp) *srvdef.SrvErr {
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ upsertDoc := bson.M{
+ "$set": bson.M{
+ FN.ESRecord.Timestamp.T: timestamp.T,
+ FN.ESRecord.Timestamp.I: timestamp.I,
+ FN.ESRecord.UpdatedAt: time.Now(),
+ },
+ "$setOnInsert": bson.M{
+ FN.ESRecord.Id: primitive.NewObjectID(),
+ FN.ESRecord.CreatedAt: time.Now(),
+ },
+ }
+ serr := UpsertOneESRecord(ctx, filter, upsertDoc)
+ return serr
+}
\ No newline at end of file
diff --git a/pub/models/esmod/esop.go b/pub/models/esmod/esop.go
new file mode 100644
index 0000000..cfe0385
--- /dev/null
+++ b/pub/models/esmod/esop.go
@@ -0,0 +1,124 @@
+package esmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ initDBIndex(ctx)
+ initIndex()
+ return
+}
+
+// initIndex 初始化索引
+func initIndex() {
+ if esmanager.Instance == nil {
+ return
+ }
+ initVideoIndex(videoIndex)
+}
+
+// initDBIndex 初始化数据库索引
+func initDBIndex(ctx context.Context) {
+ //获取数据库表
+ db = mongos.Shop.Coll(TN.ESRecord)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.ESRecord.EsIndex, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(esmod:initDBIndex) Create es_record Index failed! err:%+v:", err))
+ }
+}
+
+func initVideoIndex(esIndex string) {
+ var setting = esmanager.M{
+ "settings": esmanager.M{
+ "number_of_shards": esmanager.NumberOfShards,
+ "number_of_replicas": esmanager.NumberOfReplicas,
+ "analysis": esmanager.M{
+ "analyzer": esmanager.M{
+ "ik": esmanager.M{
+ "tokenizer": esmanager.AnalyzerIkSmart,
+ },
+ },
+ },
+ },
+ "mappings": esmanager.M{
+ "properties": esmanager.M{
+ FN.ESVideo.Title: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkMaxWord,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.MediaType: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.Remark: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Desc: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Keyworld: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.CreatorName: esmanager.M{
+ "type": "text",
+ "analyzer": esmanager.AnalyzerIkSmart,
+ "search_analyzer": esmanager.AnalyzerIkSmart,
+ },
+ FN.ESVideo.Status: esmanager.M{
+ "type": "integer",
+ },
+ //FN.ESVideo.IsHot: esmanager.M{
+ // "type": "boolean",
+ //},
+ //FN.ESVideo.Sort: esmanager.M{
+ // "type": "integer",
+ //},
+ FN.ESVideo.WatchTimes: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.ShowTimes: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.Comments: esmanager.M{
+ "type": "integer",
+ },
+ FN.ESVideo.AddedTime: esmanager.M{
+ "type": "date",
+ "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ },
+ //FN.ESVideo.CreatedAt: esmanager.M{
+ // "type": "date",
+ // "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss",
+ //},
+ },
+ },
+ }
+ if err := esmanager.Instance.InitIndices(esIndex, setting); err != nil {
+ panic(fmt.Sprintf("ElasticSearch Index %s initMediaIndex error", esIndex))
+ }
+}
diff --git a/pub/models/esmod/recordop.go b/pub/models/esmod/recordop.go
new file mode 100644
index 0000000..bb01d84
--- /dev/null
+++ b/pub/models/esmod/recordop.go
@@ -0,0 +1,144 @@
+package esmod
+
+import (
+ "context"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// InsertOneESRecord 插入ES操作记录
+func InsertOneESRecord(ctx context.Context, esRecord *ESRecord) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, esRecord); err != nil {
+ log.Warn("esmod:InsertOneESRecord db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneESRecord 删除一个ES操作记录
+func DelOneESRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("esmod:DelOnAppTag db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneESRecord 查找一个ES操作记录
+func FindOneESRecord(ctx context.Context, filter bson.M) (
+ *ESRecord, *srvdef.SrvErr) {
+ var esRecord ESRecord
+ if err := db.FindOneEx(
+ ctx, &esRecord, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Info("esmod:FindOnESRecord db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &esRecord, nil
+}
+
+// CountESRecords 统计ES操作记录
+func CountESRecords(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("esmod:CountMediaCats db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneESRecord 更新ES操作记录
+func UpdateOneESRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("esmod:UpdateOnESRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("esmod:UpdateOnESRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrMediaRecordNotExsit, "", "")
+ }
+ return nil
+}
+
+// UpsertOneESRecord 插入或更新一条ES操作记录
+func UpsertOneESRecord(ctx context.Context,
+ filter bson.M, update bson.M) *srvdef.SrvErr {
+ if _, err := db.UpsertOne(ctx, filter, update); err != nil {
+ log.Info("esmod:UpsertOneESRecord db.InsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// UpdateManyESRecord 更新一批ES操作记录
+func UpdateManyESRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("esmod:UpdateManyMediaCat db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount <= 0 {
+ log.Warn("esmod:UpdateManyMediaCat db.UpdateMMany failed",
+ log.E(err), log.Any("filter", filter))
+ //return srvdef.New(srvdef.ErrMediaTagNotExsit, "", "")
+ }
+ return nil
+}
+
+// FindManyESRecord 查找一批ES操作记录
+func FindManyESRecord(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (ESRecordList, *srvdef.SrvErr) {
+ data := make(ESRecordList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("esmod:FindManyESRecord db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+func GetRecordTimeByESIndex(ctx context.Context, esIndex string) (*ESRecord, *srvdef.SrvErr) {
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ esRecord, serr := FindOneESRecord(ctx, filter)
+ if serr != nil {
+ if serr.Code != srvdef.ErrNoData {
+ return nil, serr
+ }
+ return nil, nil
+ }
+ return esRecord, nil
+}
+func SetOneEsRecordTime(ctx context.Context, esIndex string, recordTime time.Time) *srvdef.SrvErr {
+ filter := bson.M{FN.ESRecord.EsIndex: esIndex}
+ upsertDoc := bson.M{
+ "$set": bson.M{
+ FN.ESRecord.RecordTime: recordTime,
+ FN.ESRecord.UpdatedAt: time.Now(),
+ },
+ "$setOnInsert": bson.M{
+ FN.ESRecord.Id: primitive.NewObjectID(),
+ FN.ESRecord.CreatedAt: time.Now(),
+ },
+ }
+ serr := UpsertOneESRecord(ctx, filter, upsertDoc)
+ return serr
+}
\ No newline at end of file
diff --git a/pub/models/esmod/tag.go b/pub/models/esmod/tag.go
new file mode 100755
index 0000000..b85923a
--- /dev/null
+++ b/pub/models/esmod/tag.go
@@ -0,0 +1,90 @@
+package esmod
+
+type TagESRecord struct {
+ CreatedAt string
+ EsIndex string
+ Id string
+ RecordTime string
+ Timestamp struct {
+ I string
+ T string
+ }
+ UpdatedAt string
+}
+type TagESVideo struct {
+ Id string
+ Title string
+ MediaType string
+ AddedTime string
+ CreatorName string
+ Keyworlds string
+ Desc string
+ Remark string
+ Status string
+ IsHot string
+ Sort string
+ WatchTimes string
+ ShowTimes string
+ Comments string
+ UpdatedAt string
+ CreatedAt string
+
+ Keyworld string
+ CreatorId string
+ FirstTag string
+ SecondTags string
+ CoverImg string
+ Nodes string
+ Video string
+ CategoryIds string
+ Tags string
+ Real string
+}
+
+var FN struct {
+ ESRecord TagESRecord
+ ESVideo TagESVideo
+}
+
+var TN struct {
+ ESRecord string
+ ESVideo string
+}
+
+func init() {
+ FN.ESRecord = TagESRecord{
+ CreatedAt: "createdAt",
+ EsIndex: "esIndex",
+ Id: "_id",
+ RecordTime: "recordTime",
+ Timestamp: struct {
+ I string
+ T string
+ }{
+ I: "i",
+ T: "t",
+ },
+ UpdatedAt: "updatedAt",
+ }
+ FN.ESVideo = TagESVideo{
+ Id: "_id",
+ Title: "title",
+ Desc: "desc",
+ Status: "status",
+ AddedTime: "addedTime",
+ MediaType: "mediaType",
+ CreatorName: "creatorName",
+ Keyworlds: "keyworlds",
+ Remark: "remark",
+ IsHot: "isHot",
+ Sort: "sort",
+ UpdatedAt: "updatedAt",
+ CreatedAt: "createdAt",
+ WatchTimes: "watchTimes",
+ ShowTimes: "showTimes",
+ Comments: "comments",
+ }
+
+ TN.ESRecord = "es_record"
+ TN.ESVideo = "es_video"
+}
\ No newline at end of file
diff --git a/pub/models/esmod/type.go b/pub/models/esmod/type.go
new file mode 100644
index 0000000..be5e13f
--- /dev/null
+++ b/pub/models/esmod/type.go
@@ -0,0 +1,85 @@
+//go:generate gen_bson_tag -m esmod
+//esmod essearch数据结构定义
+
+package esmod
+
+import (
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ESTimestamp struct {
+ T uint32 `json:"t" bson:"t"`
+ I uint32 `json:"i" bson:"i"`
+}
+
+func (v *ESTimestamp) IsZero() bool {
+ if v == nil {
+ return true
+ }
+ return v.T == 0 && v.I == 0
+}
+
+// ESOperator es操作记录
+// @table:es_record
+type ESRecord struct {
+ Id primitive.ObjectID `json:"id" bson:"_id"`
+ EsIndex string `json:"esIndex" bson:"esIndex"` //es索引名称
+ Timestamp ESTimestamp `json:"timestamp" bson:"timestamp"` //操作时间
+ RecordTime time.Time `json:"recordTime" bson:"recordTime"` // 操作记录时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` //更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` //创建时间
+}
+
+type ESRecordList []*ESRecord
+
+// ESVideo es中存储的media数据
+// @table:es_video
+type ESVideo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ MediaType videoinformationmod.MediaType `json:"mediaType" bson:"mediaType"` // 资源类型
+ AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 帖子上架时间
+ CreatorName string `json:"creatorName" bson:"creatorName"` // 作者名称
+ Keyworlds string `bson:"keyworlds,omitempty" json:"keyworlds"` // 关键字
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Status videoinformationmod.InformationSatus `json:"status" bson:"status"` // 帖子上架状态
+ IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Comments int64 `json:"comments" bson:"comments"` // 评论条数
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+ CreatorId int64 `json:"creatorId" bson:"creatorId"` // 作者Id
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ Nodes []videoinformationmod.Node `json:"nodes" bson:"nodes"` // 节点
+ Video videoinformationmod.VideoInfo `json:"video" bson:"video"` // 视频
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类Id
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ Real videoinformationmod.Flow `json:"real" bson:"real"` // 真实流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+}
+
+type ESVideoList []*ESVideo
+type ESTotal struct {
+ Value int64 `json:"value"`
+}
+
+type ESVideoSource struct {
+ ID string `json:"_id"`
+ Score float32 `json:"_score"`
+ Source *ESVideo `json:"_source"`
+}
+
+// ESVideoHits search media hits
+type ESVideoHits struct {
+ Total ESTotal `json:"total"`
+ Hits []*ESVideoSource `json:"hits"`
+}
\ No newline at end of file
diff --git a/pub/models/esmod/video.go b/pub/models/esmod/video.go
new file mode 100644
index 0000000..53b19bc
--- /dev/null
+++ b/pub/models/esmod/video.go
@@ -0,0 +1,135 @@
+package esmod
+
+import (
+ "fmt"
+ "videoweb/app/api/proto"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/esmanager"
+ "videoweb/pub/models/videoinformationmod"
+)
+
+// SearchVideo 根据关键字搜索媒体
+func SearchVideo(content string, from int64, size int64, sort string) (int64, []*videoinformationmod.VideoInformation, *srvdef.SrvErr) {
+ fields := []string{FN.ESVideo.Title, FN.ESVideo.CreatorName, FN.ESVideo.Keyworld, FN.ESVideo.Desc, FN.ESVideo.Remark}
+ must := esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields},
+ },
+ {"term": esmanager.M{FN.ESVideo.Status: videoinformationmod.InformationSatusVerifyOK}},
+ },
+ },
+ }
+ switch sort {
+ case string(proto.SearchSortTypeHot):
+ must = esmanager.M{
+ "function_score": esmanager.M{
+ "query": esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields,
+ }},
+ {"term": esmanager.M{FN.ESVideo.Status: videoinformationmod.InformationSatusVerifyOK}},
+ },
+ },
+ },
+ "functions": esmanager.A{
+ esmanager.M{
+ "field_value_factor": esmanager.M{
+ "field": FN.ESVideo.WatchTimes,
+ "factor": 1.0,
+ "modifier": "none",
+ },
+ },
+ },
+ "score_mode": "sum",
+ "boost_mode": "multiply",
+ },
+ }
+ case string(proto.SearchSortTypeNew):
+ must = esmanager.M{
+ "bool": esmanager.M{
+ "must": esmanager.A{
+ {"multi_match": esmanager.M{
+ "query": content,
+ "type": "best_fields",
+ "fields": fields},
+ },
+ {"term": esmanager.M{FN.ESVideo.Status: videoinformationmod.InformationSatusVerifyOK}},
+ },
+ "should": esmanager.A{
+ {"range": esmanager.M{FN.ESVideo.AddedTime: esmanager.M{"gte": "now-30d/d", "boost": 2.0}}}, //7天内提升4倍权重
+ {"range": esmanager.M{FN.ESVideo.AddedTime: esmanager.M{"gte": "now-180d/d", "boost": 1.5}}}, // 30天内提升3倍权重
+ {"range": esmanager.M{FN.ESVideo.AddedTime: esmanager.M{"gte": "now-365d/d", "boost": 1.1}}}, // 90天内提升1.5倍权重
+ },
+ "minimum_should_match": 0, // 允许不匹配任何should条件
+ },
+ }
+ }
+
+ query := esmanager.M{
+ "query": must,
+ "sort": esmanager.A{
+ esmanager.M{"_score": esmanager.M{"order": "desc"}},
+ },
+ "from": from,
+ "size": size,
+ }
+
+ videos := &ESVideoHits{}
+ err := esmanager.Instance.Search(videoIndex, videos, query)
+ if err != nil {
+ info := fmt.Sprintf(
+ "(esmod:SearchVideo) Search failed! err:%+v:", err)
+ log.Warn(info,
+ log.Any("keywords", content),
+ log.Any("from", from),
+ log.Any("size", size))
+ return 0, nil, srvdef.New(srvdef.ErrElsOperError, "", "")
+ }
+ total := int64(0)
+ rslData := make([]*videoinformationmod.VideoInformation, 0)
+ if videos != nil && videos.Hits != nil && len(videos.Hits) > 0 {
+ total = videos.Total.Value
+ for _, video := range videos.Hits {
+ rslData = append(rslData, EsVideoInfoToData(video.Source))
+ }
+ }
+ return total, rslData, nil
+}
+
+func EsVideoInfoToData(esInfo *ESVideo) *videoinformationmod.VideoInformation {
+ info := &videoinformationmod.VideoInformation{
+ Id: esInfo.Id,
+ Title: esInfo.Title,
+ MediaType: esInfo.MediaType,
+ AddedTime: esInfo.AddedTime,
+ CreatorId: esInfo.CreatorId,
+ CreatorName: esInfo.CreatorName,
+ Keyworld: esInfo.Keyworld,
+ FirstTag: esInfo.FirstTag,
+ SecondTags: esInfo.SecondTags,
+ CoverImg: esInfo.CoverImg,
+ Desc: esInfo.Desc,
+ Remark: esInfo.Remark,
+ Nodes: esInfo.Nodes,
+ Status: esInfo.Status,
+ Video: esInfo.Video,
+ CategoryIds: esInfo.CategoryIds,
+ Tags: esInfo.Tags,
+ IsHot: esInfo.IsHot,
+ Sort: esInfo.Sort,
+ Real: esInfo.Real,
+ VideoCount: esInfo.VideoCount,
+ UpdatedAt: esInfo.UpdatedAt,
+ CreatedAt: esInfo.CreatedAt,
+ }
+ return info
+}
\ No newline at end of file
diff --git a/pub/models/ipblackmod/ipblack.go b/pub/models/ipblackmod/ipblack.go
new file mode 100644
index 0000000..c60960d
--- /dev/null
+++ b/pub/models/ipblackmod/ipblack.go
@@ -0,0 +1,109 @@
+package ipblackmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.IPBlackModel)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.IPBlackModel.Ip, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(IPBlackModel:InitIPBlackModel) Create IPBlackModel Index failed! err:%+v:", err))
+ }
+}
+
+// CountIPBlackModel 统计活动数量
+func CountIPBlackModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("IPBlackModelmod:CountIPBlackModel db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// InsertOneIPBlackModel 插入一条活动
+func InsertOneIPBlackModel(ctx context.Context, IPBlackModel *IPBlackModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, IPBlackModel); err != nil {
+ log.Info("IPBlackModelmod:InsertOneIPBlackModel db.InsertOne failed",
+ log.E(err), log.Any("IPBlackModel", IPBlackModel))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneIPBlackModel 删除一个活动
+func DelOneIPBlackModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("IPBlackModelmod:DelOneIPBlackModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneIPBlackModel 更新一个IPBlackModel
+func UpdateOneIPBlackModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("IPBlackModelmod:UpdateOneIPBlackModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("IPBlackModelmod:UpdateOneIPBlackModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ObjectNotExist, "", "")
+ }
+ return nil
+}
+
+// FindManyIPBlackModel 查找一批活动数据
+func FindManyIPBlackModel(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (IPBlackModelList, *srvdef.SrvErr) {
+ data := make(IPBlackModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("IPBlackModelmod:FindManyAcivities db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindOneIPBlackModel 查找一个媒体
+func FindOneIPBlackModel(ctx context.Context, filter bson.M) (
+ *IPBlackModel, *srvdef.SrvErr) {
+ var IPBlackModel IPBlackModel
+ if err := db.FindOneEx(ctx, &IPBlackModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("IPBlackModel:FindOneIPBlackModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ObjectDayStatDoesNotExist, "", "")
+ }
+ return &IPBlackModel, nil
+}
\ No newline at end of file
diff --git a/pub/models/ipblackmod/tag.go b/pub/models/ipblackmod/tag.go
new file mode 100755
index 0000000..ff235ed
--- /dev/null
+++ b/pub/models/ipblackmod/tag.go
@@ -0,0 +1,26 @@
+package ipblackmod
+
+type TagIPBlackModel struct {
+ CreatedAt string
+ ID string
+ Ip string
+ Desc string
+ UpdatedAt string
+}
+
+var FN struct {
+ IPBlackModel TagIPBlackModel
+}
+
+var TN struct {
+ IPBlackModel string
+}
+
+func init() {
+ FN.IPBlackModel.CreatedAt = "createdAt"
+ FN.IPBlackModel.ID = "_id"
+ FN.IPBlackModel.Ip = "ip"
+ FN.IPBlackModel.UpdatedAt = "updatedAt"
+ FN.IPBlackModel.Desc = "desc"
+ TN.IPBlackModel = "ip_black"
+}
\ No newline at end of file
diff --git a/pub/models/ipblackmod/type.go b/pub/models/ipblackmod/type.go
new file mode 100644
index 0000000..be9f72f
--- /dev/null
+++ b/pub/models/ipblackmod/type.go
@@ -0,0 +1,18 @@
+package ipblackmod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 资源同步
+// @table:ip_black
+type IPBlackModel struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Ip string `json:"ip" bson:"ip"`
+ Desc string `json:"desc" bson:"desc"` // 备注
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+type IPBlackModelList []*IPBlackModel
\ No newline at end of file
diff --git a/pub/models/models.go b/pub/models/models.go
new file mode 100644
index 0000000..7f88cb3
--- /dev/null
+++ b/pub/models/models.go
@@ -0,0 +1,53 @@
+package models
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/models/analyticsdailystat"
+ "videoweb/pub/models/analyticsstat"
+ "videoweb/pub/models/cfg"
+ "videoweb/pub/models/commentmod"
+ "videoweb/pub/models/esmod"
+ "videoweb/pub/models/ipblackmod"
+ "videoweb/pub/models/mwacc"
+ "videoweb/pub/models/oplog"
+ "videoweb/pub/models/richtextcontentmod"
+ "videoweb/pub/models/syncfilemod"
+ "videoweb/pub/models/translationcategorymod"
+ "videoweb/pub/models/translationmod"
+ "videoweb/pub/models/videocategorymod"
+ "videoweb/pub/models/videocollectionmediamod"
+ "videoweb/pub/models/videocreatermod"
+
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/models/videotagmod"
+)
+
+func Init() {
+ log.Info("models init start..")
+ ctx := context.Background()
+ cfg.Init(ctx)
+ mwacc.Init(ctx)
+ oplog.Init(ctx)
+
+ videocategorymod.Init(ctx)
+ videoinformationmod.Init(ctx)
+ videotagmod.Init(ctx)
+
+ translationcategorymod.Init(ctx)
+ translationmod.Init(ctx)
+ videocategorymod.Init(ctx)
+ videocreatermod.Init(ctx)
+ videotagmod.Init(ctx)
+ videoinformationmod.Init(ctx)
+ videocreatermod.Init(ctx)
+ syncfilemod.Init(ctx)
+ richtextcontentmod.Init(ctx)
+ commentmod.Init(ctx)
+ videocollectionmediamod.Init(ctx)
+ ipblackmod.Init(ctx)
+ esmod.Init(ctx)
+ analyticsstat.Init(ctx)
+ analyticsdailystat.Init(ctx)
+ log.Info("models init end")
+}
\ No newline at end of file
diff --git a/pub/models/mwacc/acc.go b/pub/models/mwacc/acc.go
new file mode 100644
index 0000000..0832450
--- /dev/null
+++ b/pub/models/mwacc/acc.go
@@ -0,0 +1,170 @@
+package mwacc
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/errors"
+ sErr "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 过滤查询数量
+func AccCountByFilter(ctx context.Context, filters primitive.M, opts ...*options.CountOptions) (c int64, err error) {
+ c, err = mwAccColl.Count(ctx, filters)
+ return
+}
+
+// 过滤查询账号
+func FindOneAccByFilter(ctx context.Context, filters primitive.M, opts ...*options.FindOneOptions) (*MwAcc, error) {
+ var res MwAcc
+ err := mwAccColl.FindOne(ctx, &res, filters, opts...)
+ if err != nil {
+ log.Error("mwacc FindOneAccByFilter err", log.Any("filters", filters), log.E(err))
+ }
+ return &res, err
+}
+
+// 过滤更新
+func UpOneAccByFilter(ctx context.Context, query primitive.M, update primitive.M) (err error) {
+ _, err = mwAccColl.UpdateOne(ctx, query, primitive.M{"$set": update})
+ return
+}
+
+// 过滤删除角色
+func DelAccByFilter(ctx context.Context, filters primitive.M) (err error) {
+ _, err = mwAccColl.DeleteOne(ctx, filters)
+ if err != nil {
+ log.Error("mwacc DelAccByFilter err", log.Any("filters", filters), log.E(err))
+ }
+ return
+}
+
+// // 更新最后登录时间
+// func UpAccLastTimeById(ctx context.Context, id int64) (*mongo.UpdateResult, error) {
+// ret, err := mwAccColl.UpdateOne(ctx, primitive.M{FN.MwAcc.Id: id}, primitive.M{"$set": primitive.M{FN.MwAcc.LastTime: time.Now()}})
+// if err != nil {
+// log.Error("mvacc UpLastTimeById err", log.Any(FN.MwAcc.Id, id), log.E(err))
+// }
+// return ret, err
+// }
+
+// 绑定二维码
+func BindAccQurl(ctx context.Context, acc string) (ok bool, err error) {
+
+ res, dataErr := FindOneAccByFilter(ctx, primitive.M{
+ FN.MwAcc.Acc: acc,
+ })
+
+ if dataErr != nil || res.Id == 0 {
+ err = sErr.MakeWithMsg(sErr.Logo, "用户不存在!")
+ return false, err
+ }
+
+ if res.IsEnb == false {
+ err = sErr.MakeWithMsg(sErr.UserPermission, "用户被禁用!")
+ return false, err
+ }
+ err = UpOneAccByFilter(ctx, primitive.M{
+ FN.MwAcc.Acc: acc,
+ }, primitive.M{
+ FN.MwAcc.IsBase32: true,
+ })
+
+ if err != nil {
+ return false, err
+ }
+ return true, err
+}
+
+// 插入角色
+func AccAdd(ctx context.Context, insertReq *AccInsertDoc) (id int64, ok bool, err error) {
+
+ insertReq.Id = int64(mongos.Lid.MwAcc.Get())
+ insertReq.CreatedAt = time.Now()
+
+ _, err = mwAccColl.InsertOne(ctx, insertReq)
+ if err != nil {
+ log.ZapLog.Error(fmt.Sprintf("InsertRole mongo error %v", err))
+ err = errors.MakeWithErr(errors.DBInsert, err)
+ return 0, false, err
+ }
+ return insertReq.Id, true, nil
+}
+
+// 获取角色列表
+func GetAccList(ctx context.Context, search primitive.M, sort []primitive.M) (
+ total int64, data []*AccGetListRec, err error) {
+
+ var countPipe []primitive.M
+ countPipe = append(countPipe, search, bson.M{"$count": "total"})
+ var aggCount AggStatCount
+ err = mwAccColl.ObjectAggregate(ctx, &aggCount, countPipe)
+ if err != nil {
+ return 0, nil, err
+ }
+ total = aggCount.Total
+
+ var pip []primitive.M
+ pip = append(pip, search)
+ pip = append(pip, sort...)
+ pip = append(pip,
+ primitive.M{
+ "$lookup": primitive.M{
+ "localField": FN.MwAcc.RoleId,
+ "from": TN.MwRole,
+ "foreignField": FN.MwRole.Id,
+ "as": "roles",
+ },
+ },
+ primitive.M{"$unwind": primitive.M{
+ "path": "$roles",
+ "preserveNullAndEmptyArrays": true,
+ },
+ },
+ primitive.M{
+ "$project": primitive.M{
+ FN.MwAcc.Id: 1,
+ FN.MwAcc.UserName: 1,
+ FN.MwAcc.Acc: 1,
+ FN.MwAcc.Pwd: 1,
+ FN.MwAcc.Email: 1,
+ FN.MwAcc.RoleId: 1,
+ FN.MwAcc.RoleName: "$roles.name",
+ FN.MwAcc.Base32: 1,
+ FN.MwAcc.IsBase32: 1,
+ // mwacc.FN.MwAcc.LastTime: 1,
+ FN.MwAcc.IsEnb: 1,
+ FN.MwAcc.Feedback: 1,
+ FN.MwAcc.CreatedAt: 1,
+ FN.MwAcc.UpdatedAt: 1,
+ FN.MwAcc.LimitIP: 1,
+ },
+ })
+
+ err = mwAccColl.Aggregate(ctx, &data, pip)
+ if err != nil {
+ err = errors.MakeWithErr(errors.DBQuery, err)
+ return
+ }
+ return
+}
+
+// 过滤查询账号
+func FindManyAccByFilter(ctx context.Context, filters primitive.M, opts ...*options.FindOptions) ([]*MwAcc, *srvdef.SrvErr) {
+ var res []*MwAcc
+ err := mwAccColl.Find(ctx, &res, filters, opts...)
+ if err != nil {
+ log.Error("mwacc FindOneAccByFilter err", log.Any("filters", filters), log.E(err))
+ return res, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ log.Info("success")
+
+ return res, nil
+}
diff --git a/pub/models/mwacc/dbhelper.go b/pub/models/mwacc/dbhelper.go
new file mode 100644
index 0000000..48d7d4a
--- /dev/null
+++ b/pub/models/mwacc/dbhelper.go
@@ -0,0 +1,13 @@
+package mwacc
+
+import "videoweb/pub/mongos"
+
+var (
+ mwAccColl *mongos.Collection = nil
+ mwRoleColl *mongos.Collection = nil
+ mwAccTokenColl *mongos.Collection = nil
+)
+
+type AggStatCount struct {
+ Total int64 `bson:"total"`
+}
diff --git a/pub/models/mwacc/drop.go b/pub/models/mwacc/drop.go
new file mode 100644
index 0000000..352a218
--- /dev/null
+++ b/pub/models/mwacc/drop.go
@@ -0,0 +1,66 @@
+package mwacc
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ mwAccColl = mongos.Shop.Coll(TN.MwAcc)
+ mwRoleColl = mongos.Shop.Coll(TN.MwRole)
+ mwAccTokenColl = mongos.Shop.Coll(TN.MwAccToken)
+ initIndex(ctx)
+ return
+}
+
+// initIndex 初始化索引
+func initIndex(ctx context.Context) {
+ initAccIndex(ctx)
+ initRoleIndex(ctx)
+ initAccTokenIndex(ctx)
+}
+
+func initAccIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.MwAcc.Id, 1},
+ },
+ },
+ }
+ if _, err := mwAccColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(mwacc:initAccIndex) Create mwacc Index failed! err:%+v:", err))
+ }
+}
+
+func initRoleIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.MwRole.Id, 1},
+ },
+ // Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := mwRoleColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(mwacc:initRoleIndex) Create mwacc Index failed! err:%+v:", err))
+ }
+}
+func initAccTokenIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.MwAccToken.Id, 1},
+ },
+ // Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := mwAccTokenColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(mwacc:initAccTokenIndex) Create mwacc Index failed! err:%+v:", err))
+ }
+}
diff --git a/pub/models/mwacc/fun.go b/pub/models/mwacc/fun.go
new file mode 100644
index 0000000..5b415da
--- /dev/null
+++ b/pub/models/mwacc/fun.go
@@ -0,0 +1,61 @@
+package mwacc
+
+import (
+ "strconv"
+ "videoweb/pub/pubdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+func MakeSearch(keyWords string, status int64, page pubdef.Page) (searchs bson.M) {
+ var search []bson.M
+ if len(keyWords) > 0 {
+ userId, _ := strconv.ParseInt(keyWords, 10, 64)
+ search = append(search, bson.M{FN.MwAcc.Id: userId})
+ search = append(search, bson.M{FN.MwAcc.UserName: bson.M{"$regex": keyWords}})
+ search = append(search, bson.M{FN.MwAcc.Acc: bson.M{"$regex": keyWords}})
+ search = append(search, bson.M{FN.MwAcc.Email: bson.M{"$regex": keyWords}})
+ }
+
+ match := bson.M{}
+ if len(search) > 0 {
+ match["$or"] = search
+ }
+
+ if status > 0 {
+ if status == 1 {
+ match[FN.MwAcc.IsEnb] = true
+ } else {
+ match[FN.MwAcc.IsEnb] = false
+ }
+
+ }
+
+ searchs = bson.M{}
+ searchs["$match"] = match
+ return
+}
+
+func MakeSort(pageNum int64, pageSize int64) (sort []bson.M) {
+ sort = append(sort, bson.M{
+ "$sort": bson.M{
+ FN.MwAcc.CreatedAt: -1,
+ },
+ })
+ if pageNum <= 0 {
+ pageNum = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 10
+ }
+ skip := (pageNum - 1) * pageSize
+ sort = append(sort, bson.M{
+ "$skip": skip,
+ })
+
+ sort = append(sort, bson.M{
+ "$limit": pageSize,
+ })
+
+ return
+}
diff --git a/pub/models/mwacc/mwacc_gen.go b/pub/models/mwacc/mwacc_gen.go
new file mode 100644
index 0000000..1c4987b
--- /dev/null
+++ b/pub/models/mwacc/mwacc_gen.go
@@ -0,0 +1,130 @@
+// Code generated by gen_bson_tag. DO NOT EDIT.
+package mwacc
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// CreateOneMwAcc 创建一个MwAcc
+func CreateOneMwAcc(ctx context.Context, data *MwAcc) *srvdef.SrvErr {
+ _, err := mwAccColl.InsertOne(ctx, data)
+ if err != nil {
+ log.Error("mwacc:CreateOneMwAcc db.InsertOne failed",
+ log.Any("doc", data),
+ log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneMwAcc 查找一个MwAcc
+func FindOneMwAcc(ctx context.Context, filter bson.M) (
+ *MwAcc, *srvdef.SrvErr) {
+ var data *MwAcc
+ if err := mwAccColl.FindOneEx(ctx, &data, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("mwacc:FindOneMwAcc mwAccColl.FindOneEx failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return data, nil
+}
+
+// DelOneOneMwAcc 删除一个MwAcc
+func DelOneOneMwAcc(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := mwAccColl.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("mwacc:DelOneOneMwAcc mwAccColl.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneMwAcc 更新一个MwAcc
+func UpdateOneMwAcc(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := mwAccColl.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("mwacc:UpdateOneMwAcc mwAccColl.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// CountMwAccs 统计MwAcc数量
+func CountMwAccs(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := mwAccColl.Count(ctx, filter)
+ if err != nil {
+ log.Warn("mwacc:CountMwAccs mwAccColl.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// FindManyMwAccs 查找一批MwAcc
+func FindManyMwAccs(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (MwAccList, *srvdef.SrvErr) {
+ data := make(MwAccList, 0)
+ if err := mwAccColl.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("mwacc:FindManyMwAccs mwAccColl.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManyMwAccsWithBind 用自己定义结构查找一批MwAcc数据
+func FindManyMwAccsWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := mwAccColl.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("mwacc:FindManyMwAccsWithBind mwAccColl.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateMwAccs 聚合查找一批MwAcc
+func AggregateMwAccs(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := mwAccColl.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("mwacc:AggregateMwAccs mwAccColl.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateMwAccs 聚合查找一批MwAcc数据
+func ObjectAggregateMwAccs(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := mwAccColl.ObjectAggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("mwacc:AggregateMwAccs mwAccColl.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// ObjectAggregateMwAccs 批量写入或者更新MwAcc数据
+func BulkWriteMwAccs(ctx context.Context,
+ docs []mongo.WriteModel, opts ...*options.BulkWriteOptions) *srvdef.SrvErr {
+ _, err := mwAccColl.BulkWrite(ctx, docs, opts...)
+ if err != nil {
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/mwacc/role.go b/pub/models/mwacc/role.go
new file mode 100644
index 0000000..92cd8a7
--- /dev/null
+++ b/pub/models/mwacc/role.go
@@ -0,0 +1,158 @@
+package mwacc
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/pageopt"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 角色聚集处理
+func RoleAggregate(ctx context.Context, filters []primitive.M, opts ...*options.AggregateOptions) (roles []*MwRole, err error) {
+ roles = make([]*MwRole, 0)
+ err = mwRoleColl.Aggregate(ctx, &roles, filters)
+ return
+}
+
+// 过滤查询数量
+func RoleCountByFilter(ctx context.Context, filters primitive.M, opts ...*options.CountOptions) (c int64, err error) {
+ c, err = mwRoleColl.Count(ctx, filters)
+ return
+}
+
+// 过滤查询角色
+func FindOneRoleByFilter(ctx context.Context, filters primitive.M, opts ...*options.FindOneOptions) (*MwRole, *srvdef.SrvErr) {
+ var res MwRole
+ err := mwRoleColl.FindOne(ctx, &res, filters, opts...)
+ if err != nil {
+ log.Error("mwacc FindOneRoleByFilter err", log.Any("filters", filters), log.E(err))
+ return &res, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return &res, nil
+}
+
+// 过滤更新
+func UpOneRoleByFilter(ctx context.Context, query primitive.M, update primitive.M) *srvdef.SrvErr {
+ _, err := mwRoleColl.UpdateOne(ctx, query, update)
+ if err != nil {
+ log.Error("mwacc UpOneRoleByFilter: db.UpdateOne err", log.Any("update", update), log.E(err))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// 过滤删除角色
+func DelRoleByFilter(ctx context.Context, filters primitive.M) *srvdef.SrvErr {
+ _, err := mwRoleColl.DeleteOne(ctx, filters)
+ if err != nil {
+ log.Error("mwacc DelRoleByFilter err", log.Any("filters", filters), log.E(err))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return nil
+}
+
+// 更新角色权限
+func UpRoleAuth(ctx context.Context, id int64, keys []string, dataRouter []*Router) (ok bool, err error) {
+
+ _, err = mwRoleColl.UpdateOne(ctx, primitive.M{
+ FN.MwRole.Id: id,
+ }, primitive.M{
+ "$set": primitive.M{
+ FN.MwRole.ApiMenus: keys,
+ FN.MwRole.Router: dataRouter,
+ },
+ })
+
+ if err != nil {
+ log.Error("mvacc UpRoleAuth err", log.Any(FN.MwRole.Id, id), log.E(err))
+ return false, err
+ }
+ return true, nil
+}
+
+// 获取角色列表
+func GetRoleList(ctx context.Context, skip, limit int64, matchs ...pageopt.Matcher) (data []*RoleGetListRec, err error) {
+ data = make([]*RoleGetListRec, 0)
+
+ err = mwRoleColl.Aggregate(ctx, &data,
+ []primitive.M{
+ {"$match": pageopt.MergeM(matchs)},
+ {"$sort": primitive.M{FN.MwRole.Id: -1}},
+ {"$skip": skip},
+ {"$limit": limit},
+ {"$lookup": primitive.M{
+ "localField": FN.MwRole.Id,
+ "from": TN.MwAcc,
+ "foreignField": FN.MwAcc.RoleId,
+ "as": "onlyRole",
+ }},
+ {"$project": primitive.M{
+ FN.MwRole.Id: 1,
+ FN.MwRole.Name: 1,
+ FN.MwRole.Desc: 1,
+ FN.MwRole.IsEnb: 1,
+ FN.MwRole.ApiMenus: 1,
+ FN.MwRole.CreatedAt: 1,
+ FN.MwRole.UpdatedAt: 1,
+ "account": primitive.M{
+ "$size": "$onlyRole",
+ },
+ }},
+ })
+ if err != nil && err.Error() != errors.NotFind {
+ log.ZapLog.Error(fmt.Sprintf("mwacc GetRoleList error %v", err))
+ err = errors.MakeWithErr(errors.DBQuery, err)
+ return
+ }
+ return
+}
+
+// 下拉列表
+func RoleDrop(ctx context.Context) (data []*RoleDropDoc, err error) {
+
+ query := primitive.M{FN.MwRole.IsEnb: true}
+
+ opt := options.Find().SetProjection(primitive.M{
+ FN.MwRole.Id: 1,
+ FN.MwRole.Name: 1,
+ })
+
+ err = mwRoleColl.Find(ctx, &data, query, opt)
+ if err != nil {
+ err = errors.MakeWithErr(errors.DBQuery, err)
+ return
+ }
+ return
+}
+
+// 插入角色
+func RoleAdd(ctx context.Context, name string, desc string, isEnb bool) (ok bool, err error) {
+ c, err := mwRoleColl.Count(ctx, primitive.M{FN.MwRole.Name: name})
+ if c > 0 {
+ err = errors.MakeWithMsg(errors.DBInsert, "角色名称")
+ return false, err
+ }
+
+ _, err = mwRoleColl.InsertOne(ctx, primitive.M{
+ FN.MwRole.Id: mongos.Lid.MwRole.Get(),
+ FN.MwRole.Name: name,
+ FN.MwRole.Desc: desc,
+ FN.MwRole.IsEnb: isEnb,
+ FN.MwRole.CreatedAt: time.Now(),
+ })
+
+ if err != nil {
+ log.ZapLog.Error(fmt.Sprintf("InsertRole mongo error %v", err))
+ err = errors.MakeWithErr(errors.DBInsert, err)
+ return false, err
+ }
+
+ return true, nil
+}
diff --git a/pub/models/mwacc/tag.go b/pub/models/mwacc/tag.go
new file mode 100755
index 0000000..28b8406
--- /dev/null
+++ b/pub/models/mwacc/tag.go
@@ -0,0 +1,88 @@
+package mwacc
+
+type TagMwRole struct {
+ ApiMenus string
+ CreatedAt string
+ Desc string
+ Id string
+ IsEnb string
+ Name string
+ Router string
+ UpdatedAt string
+}
+type TagMwAcc struct {
+ Acc string
+ Base32 string
+ Code string
+ CreatedAt string
+ Email string
+ Feedback string
+ Id string
+ IsBase32 string
+ IsEnb string
+ LastTime string
+ LimitIP string
+ Pwd string
+ Qurl string
+ RoleId string
+ RoleName string
+ Token string
+ UpdatedAt string
+ UserName string
+}
+type TagMwAccToken struct {
+ AccId string
+ CreatedAt string
+ Id string
+ Token string
+ UpdatedAt string
+}
+
+var FN struct {
+ MwRole TagMwRole
+ MwAcc TagMwAcc
+ MwAccToken TagMwAccToken
+}
+
+var TN struct {
+ MwRole string
+ MwAcc string
+ MwAccToken string
+}
+
+func init() {
+ FN.MwRole.ApiMenus = "apiMenus"
+ FN.MwRole.CreatedAt = "createdAt"
+ FN.MwRole.Desc = "desc"
+ FN.MwRole.Id = "_id"
+ FN.MwRole.IsEnb = "isEnb"
+ FN.MwRole.Name = "name"
+ FN.MwRole.Router = "router"
+ FN.MwRole.UpdatedAt = "updatedAt"
+ FN.MwAcc.Acc = "acc"
+ FN.MwAcc.Base32 = "base32"
+ FN.MwAcc.Code = "code"
+ FN.MwAcc.CreatedAt = "createdAt"
+ FN.MwAcc.Email = "email"
+ FN.MwAcc.Feedback = "feedback"
+ FN.MwAcc.Id = "_id"
+ FN.MwAcc.IsBase32 = "isBase32"
+ FN.MwAcc.IsEnb = "isEnb"
+ FN.MwAcc.LastTime = "lastTime"
+ FN.MwAcc.LimitIP = "limitIP"
+ FN.MwAcc.Pwd = "pwd"
+ FN.MwAcc.Qurl = "qurl"
+ FN.MwAcc.RoleId = "roleId"
+ FN.MwAcc.RoleName = "roleName"
+ FN.MwAcc.Token = "token"
+ FN.MwAcc.UpdatedAt = "updatedAt"
+ FN.MwAcc.UserName = "userName"
+ FN.MwAccToken.AccId = "accId"
+ FN.MwAccToken.CreatedAt = "createdAt"
+ FN.MwAccToken.Id = "_id"
+ FN.MwAccToken.Token = "token"
+ FN.MwAccToken.UpdatedAt = "updatedAt"
+ TN.MwRole = "mw_role"
+ TN.MwAcc = "mw_acc"
+ TN.MwAccToken = "mw_acc_token"
+}
diff --git a/pub/models/mwacc/token.go b/pub/models/mwacc/token.go
new file mode 100644
index 0000000..4bdd821
--- /dev/null
+++ b/pub/models/mwacc/token.go
@@ -0,0 +1,84 @@
+package mwacc
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 过滤更新
+func UpOneTokenByFilter(ctx context.Context, query primitive.M, update primitive.M) (err error) {
+ _, err = mwAccTokenColl.UpdateOne(ctx, query, primitive.M{"$set": update})
+ return
+}
+
+func SaveToken(ctx context.Context, accId int64, token string) (ok bool, err error) {
+ var booToken MwAccToken
+
+ // 查找
+ err = mwAccTokenColl.FindOne(ctx, &booToken, primitive.M{FN.MwAccToken.AccId: accId})
+
+ if booToken.AccId > 0 {
+ // 更新
+ err = UpOneTokenByFilter(ctx, primitive.M{
+ FN.MwAccToken.AccId: accId,
+ }, primitive.M{
+ FN.MwAccToken.Token: token,
+ FN.MwAccToken.UpdatedAt: time.Now(),
+ })
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+ }
+
+ if err != nil || booToken.AccId == 0 {
+ if _, err := mwAccTokenColl.InsertOne(ctx,
+ primitive.M{
+ FN.MwAccToken.Id: mongos.Lid.MwAccToken.Get(),
+ FN.MwAccToken.AccId: accId,
+ FN.MwAccToken.Token: token,
+ FN.MwAccToken.CreatedAt: time.Now(),
+ }); err != nil {
+ return false, err
+ }
+ }
+
+ return true, nil
+}
+
+func CheckToken(ctx context.Context, token string) (ok bool, err error) {
+ var booToken MwAccToken
+ Terr := mwAccTokenColl.FindOne(ctx, &booToken, primitive.M{FN.MwAccToken.Token: token})
+ if Terr != nil || booToken.AccId == 0 {
+ err = errors.MakeWithMsg(errors.NoToken, "token 不存在或已过期")
+ return false, err
+ }
+ return true, nil
+}
+
+// // 检测Token
+// func CheckToken(ctx context.Context, token string) (ok bool, err error) {
+// var booToken WebToken
+// Terr := tokenColl.FindOne(ctx, &booToken, primitive.M{"token": token})
+// if Terr != nil || booToken.Id == 0 {
+// err = errors.MakeWithMsg(errors.NoToken, "token 不存在或已过期")
+// return
+// }
+// return true, nil
+// }
+
+// 删除token
+func DeleteOneToken(ctx context.Context, filter bson.M) *srvdef.SrvErr {
+ if _, err := mwAccTokenColl.DeleteOne(ctx, filter); err != nil {
+ log.Warn("mwacc:DeleteOneToken db.DeleteOne failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/mwacc/type.go b/pub/models/mwacc/type.go
new file mode 100644
index 0000000..1f5e471
--- /dev/null
+++ b/pub/models/mwacc/type.go
@@ -0,0 +1,78 @@
+package mwacc
+
+import (
+ "time"
+)
+
+type Meta struct {
+ Title string `bson:"title"`
+ Icon string `bson:"icon"`
+ NoCache bool `bson:"noCache"`
+}
+
+type Apiurl struct {
+ Url []string `bson:"url"`
+ Desc string `bson:"desc"`
+ Status bool `bson:"status"`
+ Method []string `bson:"method"`
+}
+
+// 编辑返回去的数据
+type Router struct {
+ Path string `bson:"path"`
+ Component string `bson:"component"`
+ Redirect string `bson:"redirect"`
+ Name string `bson:"name"`
+ IsShow bool `bson:"isShow"`
+ Hidden bool `bson:"hidden"`
+ Meta Meta `bson:"meta"`
+ Children []Router `bson:"children"`
+ ApiUrl []Apiurl `bson:"apiUrl"`
+}
+
+// 角色
+// @table:mw_role
+type MwRole struct {
+ Id int64 `bson:"_id"`
+ Name string `bson:"name"` // 角色名称
+ Desc string `bson:"desc"` // 描述
+ IsEnb bool `bson:"isEnb"` // 是否启用 false:禁用 true:启用
+ ApiMenus []string `bson:"apiMenus"` // 已有权限的api
+ Router []Router `bson:"router"`
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+}
+
+// 账号
+// @table:mw_acc
+type MwAcc struct {
+ Id int64 `bson:"_id"` // id
+ UserName string `bson:"userName"` // 用户名
+ Acc string `bson:"acc"` // 账号
+ Pwd string `bson:"pwd"` // 密码
+ Email string `bson:"email"` // 邮箱
+ RoleId int64 `bson:"roleId"` // 角色Id
+ RoleName string `bson:"roleName"` // 角色名
+ Base32 string `bson:"base32"` // 二步签名
+ IsBase32 bool `bson:"isBase32"` // 是否已签名
+ Code string `bson:"code"` // 验证码
+ Qurl []byte `bson:"qurl"` // 二维码链接
+ Token string `bson:"token"` // token
+ LastTime time.Time `bson:"lastTime"` // 登录时间
+ IsEnb bool `bson:"isEnb"` // 使能
+ Feedback string `bson:"feedback"` // 禁用原因
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+ LimitIP []string `bson:"limitIP"` // 限制IP
+}
+type MwAccList []*MwAcc
+
+// 账号Token
+// @table:mw_acc_token
+type MwAccToken struct {
+ Id int64 `bson:"_id"` // id
+ AccId int64 `bson:"accId"` // 账号id
+ Token string `bson:"token"` // token
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+}
diff --git a/pub/models/mwacc/web.go b/pub/models/mwacc/web.go
new file mode 100644
index 0000000..ec5f3fb
--- /dev/null
+++ b/pub/models/mwacc/web.go
@@ -0,0 +1,53 @@
+package mwacc
+
+import (
+ "time"
+)
+
+type AccGetListRec struct {
+ Id int64 `bson:"_id"` // id
+ Username string `bson:"username"` // 用户名
+ Acc string `bson:"acc"` // 用户名
+ Pwd string `bson:"pwd"` // 密码
+ Email string `bson:"email"` // 邮箱
+ RoleId int64 `bson:"roleId"` // 角色Id
+ RoleName string `bson:"roleName"` // 角色名
+ // Base32 string `bson:"base32"` // 二步签名
+ IsBase32 bool `bson:"isBase32"` // 是否已签名
+ IsEnb bool `bson:"isEnb"` // 使能
+ Feedback string `bson:"feedback"` // 禁用原因
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+ LimitIP []string `bson:"limitIP"` // 允许访问的IP
+}
+
+type RoleGetListRec struct {
+ Id int64 `bson:"_id"` // 角色Id
+ Name string `bson:"name"` // 角色名称
+ Desc string `bson:"desc"` // 描述
+ IsEnb bool `bson:"isEnb"` // 是否启用 false:禁用 true:启用
+ ApiMenus []string `bson:"apiMenus"` // 已有权限的api
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+ AccCount int64 `bson:"accCount"` // 账户数量
+}
+
+type AccInsertDoc struct {
+ Id int64 `bson:"_id"` // id
+ UserName string `bson:"userName"` // 用户名
+ Pwd string `bson:"pwd"` // 密码
+ Acc string `bson:"acc"` // 用户名
+ Email string `bson:"email"` // 邮箱
+ RoleId int64 `bson:"roleId"` // 角色Id
+ Base32 string `bson:"base32"` // 二步签名
+ IsBase32 bool `bson:"isBase32"` // 是否已签名
+ IsEnb bool `bson:"isEnb"` // 使能
+ LastTime time.Time `bson:"lastTime"` // 登录时间
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ LimitIP []string `bson:"limitIP"` // 允许访问的IP
+}
+
+type RoleDropDoc struct {
+ Id int64 `bson:"_id"` // 角色Id
+ Name string `bson:"name"` // 角色名称
+}
diff --git a/pub/models/oplog/dbhelper.go b/pub/models/oplog/dbhelper.go
new file mode 100644
index 0000000..d19a38e
--- /dev/null
+++ b/pub/models/oplog/dbhelper.go
@@ -0,0 +1,9 @@
+package oplog
+
+import (
+ "videoweb/pub/mongos"
+)
+
+var (
+ opLogColl *mongos.Collection
+)
diff --git a/pub/models/oplog/oplog.go b/pub/models/oplog/oplog.go
new file mode 100644
index 0000000..ecb33ec
--- /dev/null
+++ b/pub/models/oplog/oplog.go
@@ -0,0 +1,175 @@
+package oplog
+
+import (
+ "context"
+ "fmt"
+ "time"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/common/utils"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ opLogColl = mongos.ShopLog.Coll(TN.OpLog)
+ initIndex(ctx)
+ return
+}
+
+func initIndex(ctx context.Context) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {FN.OpLog.ActId, 1},
+ {FN.OpLog.Action, 1},
+ {FN.OpLog.Path, 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {FN.OpLog.CreatedAt, -1},
+ },
+ },
+ }
+ if _, err := opLogColl.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(oplog:initIndex) Create adLog Index failed! err:%+v:", err))
+ }
+}
+
+// 添加日志
+func AddLog(ctx context.Context, path string, desc string, actId int64, rawData, ip string) (err error) {
+ _, err = opLogColl.InsertOne(ctx, primitive.M{
+ FN.OpLog.Id: mongos.Lid.MwOpLog.Get(),
+ FN.OpLog.Action: desc,
+ FN.OpLog.Path: path,
+ FN.OpLog.ActId: actId,
+ FN.OpLog.RawData: rawData,
+ FN.OpLog.CreatedAt: utils.Now(),
+ FN.OpLog.IP: ip,
+ })
+ return
+}
+
+type LogReq struct {
+ PageNum int64 `json:"pageNum"` // 页码
+ PageSize int64 `json:"pageSize"` //
+ Path *string `json:"path"` // 请求接口
+ Action *string `json:"action"` // 操作目的
+ ActId *int64 `json:"actId"` // 操作人Id
+ StartTime *time.Time `json:"startTime"` // 开始时间
+ EndTime *time.Time `json:"endTime"` // 开始时间
+}
+
+// 操作日志信息
+type OpLogInfo struct {
+ Path string `json:"path" bson:"path"` // 请求接口
+ ActId int64 `json:"actId" bson:"actId"` // 操作人Id
+ Action string `json:"action" bson:"action"` // 操作目的
+ Param string `json:"param" bson:"rawData"` // 操作内容
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 操作时间
+ ActName string `json:"actName" bson:"actName"` // 操作人
+}
+
+type OpPageRes struct {
+ Total int64 `json:"total" bson:"total"`
+ List []*OpLogInfo `json:"list" bson:"list"`
+}
+
+// 获取日志列表
+func PageLogList(ctx context.Context, req *LogReq) (res *OpPageRes, err error) {
+ filter := bson.M{}
+ if req.Path != nil && *req.Path != "" {
+ filter[FN.OpLog.Path] = bson.M{"$regex": req.Path}
+ }
+ if req.Action != nil && *req.Action != "" {
+ filter[FN.OpLog.Action] = req.Action
+ }
+ if req.ActId != nil && *req.ActId > 0 {
+ filter[FN.OpLog.ActId] = req.ActId
+ }
+ if req.StartTime != nil && req.EndTime != nil {
+ filter[FN.OpLog.CreatedAt] = bson.M{"$gte": req.StartTime, "$lt": req.EndTime}
+ }
+ res = new(OpPageRes)
+ res.Total, err = mongos.ShopLog.Coll(TN.OpLog).Count(ctx, filter)
+ if err != nil {
+ return
+ }
+
+ sort := bson.M{FN.OpLog.CreatedAt: -1}
+ opt := &options.FindOptions{}
+ opt.SetSort(sort)
+ skip := (req.PageNum - 1) * req.PageSize
+ opt.SetSkip(skip)
+ opt.SetLimit(req.PageSize)
+ list := make(OpLogList, 0)
+ err = mongos.ShopLog.Coll(TN.OpLog).Find(ctx, &list, filter, opt)
+ if err != nil {
+ return
+ }
+ actIds := make([]int64, 0)
+ for _, v := range list {
+ actIds = append(actIds, v.ActId)
+ }
+ // err = opLogColl.Aggregate(ctx, &res.List, []bson.M{
+ // {"$match": conf},
+ // {"$sort": bson.M{FN.OpLog.CreatedAt: -1}},
+ // {"$skip": (req.PageNum - 1) * req.PageSize},
+ // {"$limit": req.PageSize},
+ // {"$lookup": bson.M{
+ // "localField": FN.OpLog.ActId,
+ // "from": mwacc.TN.MwAcc,
+ // "foreignField": mwacc.FN.MwAcc.Id,
+ // "as": "admins",
+ // }},
+ // {"$unwind": "$admins"},
+ // {"$project": bson.M{
+ // FN.OpLog.Path: 1,
+ // FN.OpLog.Action: 1,
+ // FN.OpLog.ActId: 1,
+ // FN.OpLog.RawData: 1,
+ // FN.OpLog.CreatedAt: 1,
+ // "actName": "$admins.userName",
+ // }},
+ // })
+ return
+}
+
+// CountOpLogs 计算某个查询的数量
+func CountOpLogs(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ total, err := mongos.ShopLog.Coll(TN.OpLog).Count(ctx, filter)
+ if err != nil {
+ log.Info("oplog:CountOpLogs db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return total, nil
+}
+
+// InsertOneUser 插入一个用户
+func InsertOneUser(ctx context.Context, opLog *OpLog) *srvdef.SrvErr {
+ if _, err := mongos.ShopLog.Coll(TN.OpLog).InsertOne(ctx, opLog); err != nil {
+ log.Warn("oplog:InsertOneUser db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindManyOpLogs 查找一批数据
+func FindManyOpLogs(ctx context.Context,
+ filter bson.M, opts ...*options.FindOptions) (
+ OpLogList, *srvdef.SrvErr) {
+ data := make(OpLogList, 0)
+ if err := mongos.ShopLog.Coll(TN.OpLog).Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("oplog:FindManyOpLogs db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
diff --git a/pub/models/oplog/tag.go b/pub/models/oplog/tag.go
new file mode 100755
index 0000000..e2bc190
--- /dev/null
+++ b/pub/models/oplog/tag.go
@@ -0,0 +1,34 @@
+package oplog
+
+type TagOpLog struct {
+ ActId string
+ Action string
+ CreatedAt string
+ IP string
+ Id string
+ IsWhite string
+ OpType string
+ Path string
+ RawData string
+}
+
+var FN struct {
+ OpLog TagOpLog
+}
+
+var TN struct {
+ OpLog string
+}
+
+func init() {
+ FN.OpLog.ActId = "actId"
+ FN.OpLog.Action = "action"
+ FN.OpLog.CreatedAt = "createdAt"
+ FN.OpLog.IP = "ip"
+ FN.OpLog.Id = "_id"
+ FN.OpLog.IsWhite = "isWhite"
+ FN.OpLog.OpType = "opType"
+ FN.OpLog.Path = "path"
+ FN.OpLog.RawData = "rawData"
+ TN.OpLog = "mw_op_log"
+}
diff --git a/pub/models/oplog/type.go b/pub/models/oplog/type.go
new file mode 100644
index 0000000..c3348ef
--- /dev/null
+++ b/pub/models/oplog/type.go
@@ -0,0 +1,22 @@
+package oplog
+
+import (
+ "time"
+ "videoweb/pub/common"
+)
+
+// 操作日志
+// @table:mw_op_log
+type OpLog struct {
+ Id int64 `bson:"_id"` // id
+ Path string `bson:"path"` // 请求接口
+ OpType common.ApiOpType `bson:"opType"` // 操作类型
+ IsWhite bool `bson:"isWhite"` // 是否白名单
+ ActId int64 `bson:"actId"` // 操作人Id
+ Action string `bson:"action"` // 操作目的
+ RawData string `bson:"rawData"` // 原始数据
+ CreatedAt time.Time `bson:"createdAt"` // 操作时间
+ IP string `bson:"ip"` // 操作人IP
+}
+
+type OpLogList []*OpLog
diff --git a/pub/models/richtextcontentmod/richtextcontent.go b/pub/models/richtextcontentmod/richtextcontent.go
new file mode 100644
index 0000000..eceb86e
--- /dev/null
+++ b/pub/models/richtextcontentmod/richtextcontent.go
@@ -0,0 +1,153 @@
+package richtextcontentmod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.RichTextContentModel)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.RichTextContentModel.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.RichTextContentModel.Status, Value: 1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(RichTextContentModelModel:initRichTextContentModelModelIndex) Create RichTextContentModelModel Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneRichTextContentModelModel 插入定时任务
+func InsertOneRichTextContentModelModel(ctx context.Context, RichTextContentModelModel *RichTextContentModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, RichTextContentModelModel); err != nil {
+ log.Warn("RichTextContentModelModel:InsertOneRichTextContentModelModel db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneRichTextContentModelModel 插入定时任务
+func FindOneRichTextContentModelModel(ctx context.Context, filter bson.M) (
+ *RichTextContentModel, *srvdef.SrvErr) {
+ var RichTextContentModelModel RichTextContentModel
+ if err := db.FindOneEx(ctx, &RichTextContentModelModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("RichTextContentModelModel:FindOneRichTextContentModelModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &RichTextContentModelModel, nil
+}
+
+// DelOneRichTextContentModelModel 插入定时任务
+func DelOneRichTextContentModelModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:DelOneRichTextContentModelModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneRichTextContentModelModel 插入定时任务
+func UpdateOneRichTextContentModelModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:UpdateOneRichTextContentModelModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+// UpsertOneRichTextContentModelModel 更新插入一个记录
+func UpsertOneRichTextContentModelModel(ctx context.Context, filter bson.M, upsertDoc bson.M) *srvdef.SrvErr {
+ _, err := db.UpsertOne(ctx, filter, upsertDoc)
+ if err != nil {
+ log.Info("RichTextContentModelModel:UpsertOneRichTextContentModelModel db.UpsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// CountRichTextContentModelModels 统计用户数量
+func CountRichTextContentModelModels(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:CountRichTextContentModelModels db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateManyRichTextContentModelModel 更新满足条件的用户
+func UpdateManyRichTextContentModelModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("RichTextContentModelModel:UpdateManyRichTextContentModelModel db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManyRichTextContentModelModel 查找一批用户数据
+func FindManyRichTextContentModelModel(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (RichTextContentModelList, *srvdef.SrvErr) {
+ data := make(RichTextContentModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("RichTextContentModelModel:FindManyRichTextContentModelModel db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManyRichTextContentModelModelWithBind 用自己定义结构查找一批用户数据
+func FindManyRichTextContentModelModelWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("RichTextContentModelModel:FindManyRichTextContentModelModelWithBind db.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateRichTextContentModelModels 聚合查找一批用户信息
+func AggregateRichTextContentModelModels(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("feedback:AggregateRichTextContentModelModels db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/richtextcontentmod/tag.go b/pub/models/richtextcontentmod/tag.go
new file mode 100755
index 0000000..8598aa1
--- /dev/null
+++ b/pub/models/richtextcontentmod/tag.go
@@ -0,0 +1,40 @@
+package richtextcontentmod
+
+type TagRichTextContentModel struct {
+ Content string
+ CreatedAt string
+ Desc string
+ Id string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ Title string
+ UpdatedAt string
+}
+
+var FN struct {
+ RichTextContentModel TagRichTextContentModel
+}
+
+var TN struct {
+ RichTextContentModel string
+}
+
+func init() {
+ FN.RichTextContentModel.Content = "content"
+ FN.RichTextContentModel.CreatedAt = "createdAt"
+ FN.RichTextContentModel.Desc = "desc"
+ FN.RichTextContentModel.Id = "_id"
+ FN.RichTextContentModel.SeoInfo.Description = "seoInfo.webDescription"
+ FN.RichTextContentModel.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.RichTextContentModel.SeoInfo.Title = "seoInfo.webTitle"
+ FN.RichTextContentModel.Sort = "sort"
+ FN.RichTextContentModel.Status = "status"
+ FN.RichTextContentModel.Title = "title"
+ FN.RichTextContentModel.UpdatedAt = "updatedAt"
+ TN.RichTextContentModel = "rich_text_content"
+}
diff --git a/pub/models/richtextcontentmod/type.go b/pub/models/richtextcontentmod/type.go
new file mode 100644
index 0000000..bdc8223
--- /dev/null
+++ b/pub/models/richtextcontentmod/type.go
@@ -0,0 +1,22 @@
+package richtextcontentmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// RichTextContentModel 富文本内容
+// @table:rich_text_content
+type RichTextContentModel struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 标题(内部)
+ Content string `json:"content" bson:"content"` // 内容
+ Status bool `json:"status" bson:"status"` // 是否启用
+ Sort int64 `json:"sort" bson:"sort"` // 排序值
+ Desc string `json:"desc" bson:"desc"` // 标题(app)
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ CreatedAt time.Time `bson:"createdAt" json:"createdAt" ` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt" ` // 更新时间
+}
+
+type RichTextContentModelList []*RichTextContentModel
diff --git a/pub/models/skdrecord/record.go b/pub/models/skdrecord/record.go
new file mode 100644
index 0000000..b93294a
--- /dev/null
+++ b/pub/models/skdrecord/record.go
@@ -0,0 +1,153 @@
+package skdrecord
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.SkdRecord)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.SkdRecord.Job, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.SkdRecord.CreatedAt, Value: 1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(SkdRecord:initSkdRecordIndex) Create SkdRecord Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneSkdRecord 插入定时任务
+func InsertOneSkdRecord(ctx context.Context, SkdRecord *SkdRecord) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, SkdRecord); err != nil {
+ log.Warn("SkdRecord:InsertOneSkdRecord db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// FindOneSkdRecord 插入定时任务
+func FindOneSkdRecord(ctx context.Context, filter bson.M) (
+ *SkdRecord, *srvdef.SrvErr) {
+ var SkdRecord SkdRecord
+ if err := db.FindOneEx(ctx, &SkdRecord, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("SkdRecord:FindOneSkdRecord db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrSkdRecordNotExsit, "", "")
+ }
+ return &SkdRecord, nil
+}
+
+// DelOneSkdRecord 插入定时任务
+func DelOneSkdRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("SkdRecord:DelOneSkdRecord db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneSkdRecord 插入定时任务
+func UpdateOneSkdRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("SkdRecord:UpdateOneSkdRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ return srvdef.New(srvdef.ErrSkdRecordNotExsit, "", "")
+ }
+ return nil
+}
+
+// UpsertOneSkdRecord 更新插入一个记录
+func UpsertOneSkdRecord(ctx context.Context, filter bson.M, upsertDoc bson.M) *srvdef.SrvErr {
+ _, err := db.UpsertOne(ctx, filter, upsertDoc)
+ if err != nil {
+ log.Info("SkdRecord:UpsertOneSkdRecord db.UpsertOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// CountSkdRecords 统计用户数量
+func CountSkdRecords(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("SkdRecord:CountSkdRecords db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateManySkdRecord 更新满足条件的用户
+func UpdateManySkdRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ _, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("SkdRecord:UpdateManySkdRecord db.UpdateMany failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ return nil
+}
+
+// FindManySkdRecord 查找一批用户数据
+func FindManySkdRecord(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (SkdRecordList, *srvdef.SrvErr) {
+ data := make(SkdRecordList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("SkdRecord:FindManySkdRecord db.Find failed", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindManySkdRecordWithBind 用自己定义结构查找一批用户数据
+func FindManySkdRecordWithBind(ctx context.Context, bind interface{}, filter bson.M,
+ opts ...*options.FindOptions) *srvdef.SrvErr {
+ if err := db.Find(ctx, bind, filter, opts...); err != nil {
+ log.Warn("SkdRecord:FindManySkdRecordWithBind db.Find failed", log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
+
+// AggregateSkdRecords 聚合查找一批用户信息
+func AggregateSkdRecords(ctx context.Context,
+ bind interface{}, pipeline []bson.M) *srvdef.SrvErr {
+ if err := db.Aggregate(ctx, bind, pipeline); err != nil {
+ log.Warn("feedback:AggregateSkdRecords db.Aggregate failed",
+ log.E(err), log.Any("pipeline", pipeline))
+ return srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/skdrecord/tag.go b/pub/models/skdrecord/tag.go
new file mode 100755
index 0000000..0344cef
--- /dev/null
+++ b/pub/models/skdrecord/tag.go
@@ -0,0 +1,30 @@
+package skdrecord
+
+type TagSkdRecord struct {
+ CreatedAt string
+ FirstRecordTime string
+ ID string
+ Item string
+ Job string
+ RecordTime string
+ UpdatedAt string
+}
+
+var FN struct {
+ SkdRecord TagSkdRecord
+}
+
+var TN struct {
+ SkdRecord string
+}
+
+func init() {
+ FN.SkdRecord.CreatedAt = "createdAt"
+ FN.SkdRecord.FirstRecordTime = "firstRecordTime"
+ FN.SkdRecord.ID = "_id"
+ FN.SkdRecord.Item = "item"
+ FN.SkdRecord.Job = "job"
+ FN.SkdRecord.RecordTime = "recordTime"
+ FN.SkdRecord.UpdatedAt = "updatedAt"
+ TN.SkdRecord = "skd_record"
+}
diff --git a/pub/models/skdrecord/type.go b/pub/models/skdrecord/type.go
new file mode 100644
index 0000000..bbd641c
--- /dev/null
+++ b/pub/models/skdrecord/type.go
@@ -0,0 +1,27 @@
+package skdrecord
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 表名
+// SkdRecord 获取充值记录
+// @table:skd_record
+type SkdRecord struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Job string `bson:"job"` // 工作
+ Item string `bson:"item"` // 统计项
+ RecordTime time.Time `bson:"recordTime"` // 提交时间
+ FirstRecordTime time.Time `bson:"firstRecordTime"` // 第一次提交时间
+ CreatedAt time.Time `bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt"` // 更新时间
+}
+
+const (
+ GameRecordJob = "GameRecordJob" // 按天统计访问信息
+ LoufengUpdate = "LoufengUpdate" // 楼风更新信息
+)
+
+type SkdRecordList []*SkdRecord
diff --git a/pub/models/syncfilemod/sync.go b/pub/models/syncfilemod/sync.go
new file mode 100644
index 0000000..0feb28d
--- /dev/null
+++ b/pub/models/syncfilemod/sync.go
@@ -0,0 +1,108 @@
+package syncfilemod
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.SyncRecord)
+ // 初始化索引
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.SyncRecord.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(SyncRecord:InitSyncRecord) Create SyncRecord Index failed! err:%+v:", err))
+ }
+}
+
+// CountSyncRecord 统计活动数量
+func CountSyncRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("SyncRecordmod:CountSyncRecord db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// InsertOneSyncRecord 插入一条活动
+func InsertOneSyncRecord(ctx context.Context, SyncRecord *SyncRecord) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, SyncRecord); err != nil {
+ log.Info("SyncRecordmod:InsertOneSyncRecord db.InsertOne failed",
+ log.E(err), log.Any("SyncRecord", SyncRecord))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneSyncRecord 删除一个活动
+func DelOneSyncRecord(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("SyncRecordmod:DelOneSyncRecord db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneSyncRecord 更新一个SyncRecord
+func UpdateOneSyncRecord(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("SyncRecordmod:UpdateOneSyncRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("SyncRecordmod:UpdateOneSyncRecord db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ObjectNotExist, "", "")
+ }
+ return nil
+}
+
+// FindManySyncRecord 查找一批活动数据
+func FindManySyncRecord(ctx context.Context, filter bson.M,
+ opts ...*options.FindOptions) (SyncRecordList, *srvdef.SrvErr) {
+ data := make(SyncRecordList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); err != nil {
+ log.Warn("SyncRecordmod:FindManyAcivities db.Find failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return data, nil
+}
+
+// FindOneSyncRecord 查找一个媒体
+func FindOneSyncRecord(ctx context.Context, filter bson.M) (
+ *SyncRecord, *srvdef.SrvErr) {
+ var SyncRecord SyncRecord
+ if err := db.FindOneEx(ctx, &SyncRecord, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("SyncRecord:FindOneSyncRecord db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ObjectDayStatDoesNotExist, "", "")
+ }
+ return &SyncRecord, nil
+}
\ No newline at end of file
diff --git a/pub/models/syncfilemod/tag.go b/pub/models/syncfilemod/tag.go
new file mode 100755
index 0000000..b9da6f4
--- /dev/null
+++ b/pub/models/syncfilemod/tag.go
@@ -0,0 +1,30 @@
+package syncfilemod
+
+type TagSyncRecord struct {
+ CreatedAt string
+ ID string
+ Job string
+ RecordId string
+ SuccessAt string
+ UpdatedAt string
+ RecordTime string
+}
+
+var FN struct {
+ SyncRecord TagSyncRecord
+}
+
+var TN struct {
+ SyncRecord string
+}
+
+func init() {
+ FN.SyncRecord.CreatedAt = "createdAt"
+ FN.SyncRecord.ID = "_id"
+ FN.SyncRecord.Job = "job"
+ FN.SyncRecord.RecordId = "recordId"
+ FN.SyncRecord.SuccessAt = "successAt"
+ FN.SyncRecord.UpdatedAt = "updatedAt"
+ FN.SyncRecord.RecordTime = "recordTime"
+ TN.SyncRecord = "sync_record"
+}
\ No newline at end of file
diff --git a/pub/models/syncfilemod/type.go b/pub/models/syncfilemod/type.go
new file mode 100644
index 0000000..75f1141
--- /dev/null
+++ b/pub/models/syncfilemod/type.go
@@ -0,0 +1,32 @@
+package syncfilemod
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 资源同步
+// @table:sync_record
+type SyncRecord struct {
+ ID primitive.ObjectID `bson:"_id,omitempty"`
+ Job string `json:"job" bson:"job"` // 工作
+ RecordId int64 `json:"recordId" bson:"recordId"` // 用户ID
+ RecordTime time.Time `json:"recordTime" bson:"recordTime"` // 记录时间
+ SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+}
+type SyncRecordList []*SyncRecord
+
+const (
+ NovelJob = "novelJob" // 小说记录同步
+ ComicsJob = "comicsJob" // 漫画记录同步
+ PostJob = "postJob" // 帖子记录同步
+ VideoJob = "videoJob" // 视频同步
+)
+
+var SyncJobDescribe = map[string]string{
+ NovelJob: "小说记录同步",
+ ComicsJob: "漫画记录同步",
+}
\ No newline at end of file
diff --git a/pub/models/translationcategorymod/category.go b/pub/models/translationcategorymod/category.go
new file mode 100644
index 0000000..3e6895c
--- /dev/null
+++ b/pub/models/translationcategorymod/category.go
@@ -0,0 +1,112 @@
+package translationcategorymod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.TranslationCategory)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationCategory.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationCategory.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initTranslationCategoryIndex) Create TranslationCategory Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneTranslationCategory 插入一个导航分类
+func InsertOneTranslationCategory(ctx context.Context, qa *TranslationCategory) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("TranslationCategorymod:InsertOneTranslationCategory db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneTranslationCategory 删除一个导航分类
+func DelOneTranslationCategory(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("TranslationCategorymod:DelOneTranslationCategory db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneTranslationCategory 查找一个导航分类
+func FindOneTranslationCategory(ctx context.Context, filter bson.M) (
+ *TranslationCategory, *srvdef.SrvErr) {
+ var TranslationCategory TranslationCategory
+ if err := db.FindOneEx(ctx, &TranslationCategory, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("TranslationCategorymod:FindOneTranslationCategory db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &TranslationCategory, nil
+}
+
+func FindManyTranslationCategorys(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (TranslationCategoryList, *srvdef.SrvErr) {
+ data := make(TranslationCategoryList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyTranslationCategorys", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountTranslationCategorys 统计导航分类数量
+func CountTranslationCategorys(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountTranslationCategorys db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneTranslationCategory 更新一个导航分类
+func UpdateOneTranslationCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneTranslationCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneTranslationCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/translationcategorymod/tag.go b/pub/models/translationcategorymod/tag.go
new file mode 100755
index 0000000..7ef84a1
--- /dev/null
+++ b/pub/models/translationcategorymod/tag.go
@@ -0,0 +1,30 @@
+package translationcategorymod
+
+type TagTranslationCategory struct {
+ CreatedAt string
+ Id string
+ Name string
+ Rank string
+ Status string
+ UpdatedAt string
+ UrlValue string
+}
+
+var FN struct {
+ TranslationCategory TagTranslationCategory
+}
+
+var TN struct {
+ TranslationCategory string
+}
+
+func init() {
+ FN.TranslationCategory.CreatedAt = "createdAt"
+ FN.TranslationCategory.Id = "_id"
+ FN.TranslationCategory.Name = "name"
+ FN.TranslationCategory.Rank = "rank"
+ FN.TranslationCategory.Status = "status"
+ FN.TranslationCategory.UpdatedAt = "updatedAt"
+ FN.TranslationCategory.UrlValue = "urlValue"
+ TN.TranslationCategory = "translation_category"
+}
diff --git a/pub/models/translationcategorymod/type.go b/pub/models/translationcategorymod/type.go
new file mode 100644
index 0000000..9e88045
--- /dev/null
+++ b/pub/models/translationcategorymod/type.go
@@ -0,0 +1,17 @@
+package translationcategorymod
+
+import (
+ "time"
+)
+
+// @table:translation_category
+type TranslationCategory struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ Status bool `json:"status" bson:"status"` // 上架状态
+ VideoCategoryId int64 `json:"videoCategoryId" bson:"videoCategoryId"` // 视频分类
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type TranslationCategoryList []*TranslationCategory
diff --git a/pub/models/translationmod/tag.go b/pub/models/translationmod/tag.go
new file mode 100755
index 0000000..7aefb73
--- /dev/null
+++ b/pub/models/translationmod/tag.go
@@ -0,0 +1,36 @@
+package translationmod
+
+type TagTranslationInfo struct {
+ CreatedAt string
+ HtmlId string
+ Id string
+ Link string
+ Name string
+ Rank string
+ Status string
+ TransCategoryId string
+ UpdatedAt string
+ VideoCategoryId string
+}
+
+var FN struct {
+ TranslationInfo TagTranslationInfo
+}
+
+var TN struct {
+ TranslationInfo string
+}
+
+func init() {
+ FN.TranslationInfo.CreatedAt = "createdAt"
+ FN.TranslationInfo.HtmlId = "htmlId"
+ FN.TranslationInfo.Id = "_id"
+ FN.TranslationInfo.Link = "link"
+ FN.TranslationInfo.Name = "name"
+ FN.TranslationInfo.Rank = "rank"
+ FN.TranslationInfo.Status = "status"
+ FN.TranslationInfo.TransCategoryId = "transCategoryId"
+ FN.TranslationInfo.UpdatedAt = "updatedAt"
+ FN.TranslationInfo.VideoCategoryId = "videoCategoryId"
+ TN.TranslationInfo = "translation_info"
+}
diff --git a/pub/models/translationmod/translation.go b/pub/models/translationmod/translation.go
new file mode 100644
index 0000000..807c0c7
--- /dev/null
+++ b/pub/models/translationmod/translation.go
@@ -0,0 +1,112 @@
+package translationmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.TranslationInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationInfo.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.TranslationInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initTranslationInfoIndex) Create TranslationInfo Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneTranslationInfo 插入一个渠道统计
+func InsertOneTranslationInfo(ctx context.Context, qa *TranslationInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("TranslationInfomod:InsertOneTranslationInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneTranslationInfo 删除一个渠道统计
+func DelOneTranslationInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("TranslationInfomod:DelOneTranslationInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneTranslationInfo 查找一个渠道统计
+func FindOneTranslationInfo(ctx context.Context, filter bson.M) (
+ *TranslationInfo, *srvdef.SrvErr) {
+ var TranslationInfo TranslationInfo
+ if err := db.FindOneEx(ctx, &TranslationInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("TranslationInfomod:FindOneTranslationInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &TranslationInfo, nil
+}
+
+func FindManyTranslationInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (TranslationInfoList, *srvdef.SrvErr) {
+ data := make(TranslationInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyTranslationInfos", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountTranslationInfos 统计渠道统计数量
+func CountTranslationInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountTranslationInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneTranslationInfo 更新一个渠道统计
+func UpdateOneTranslationInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneTranslationInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneTranslationInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
diff --git a/pub/models/translationmod/type.go b/pub/models/translationmod/type.go
new file mode 100644
index 0000000..34c50ef
--- /dev/null
+++ b/pub/models/translationmod/type.go
@@ -0,0 +1,20 @@
+package translationmod
+
+import (
+ "time"
+)
+
+// @table:translation_info
+type TranslationInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ TransCategoryId int64 `json:"transCategoryId" bson:"transCategoryId"` // 导航分类
+ VideoCategoryId int64 `json:"videoCategoryId" bson:"videoCategoryId"` // 视频分类
+ HtmlId int64 `json:"htmlId" bson:"htmlId"` // 富文本ID
+ Link string `json:"link" bson:"link"` // 外部跳转
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ Status bool `json:"status" bson:"status"` // 上架状态
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type TranslationInfoList []*TranslationInfo
diff --git a/pub/models/utils/dbhelper.go b/pub/models/utils/dbhelper.go
new file mode 100644
index 0000000..d4b585b
--- /dev/null
+++ b/pub/models/utils/dbhelper.go
@@ -0,0 +1 @@
+package utils
diff --git a/pub/models/utils/dbop.go b/pub/models/utils/dbop.go
new file mode 100644
index 0000000..05c75d3
--- /dev/null
+++ b/pub/models/utils/dbop.go
@@ -0,0 +1,11 @@
+package utils
+
+import (
+ "context"
+)
+
+// Init 初始化数据
+func Init(ctx context.Context) {
+ //initIndex(ctx)
+ return
+}
diff --git a/pub/models/utils/tag.go b/pub/models/utils/tag.go
new file mode 100755
index 0000000..bee5413
--- /dev/null
+++ b/pub/models/utils/tag.go
@@ -0,0 +1,30 @@
+package utils
+
+type TagGenerateId struct {
+ BusinessId string
+ CreateAt string
+ Desc string
+ Id string
+ MaxId string
+ Step string
+ UpdateAt string
+}
+
+var FN struct {
+ GenerateId TagGenerateId
+}
+
+var TN struct {
+ GenerateId string
+}
+
+func init() {
+ FN.GenerateId.BusinessId = "businessId"
+ FN.GenerateId.CreateAt = "createAt"
+ FN.GenerateId.Desc = "desc"
+ FN.GenerateId.Id = "_id"
+ FN.GenerateId.MaxId = "maxId"
+ FN.GenerateId.Step = "step"
+ FN.GenerateId.UpdateAt = "updateAt"
+ TN.GenerateId = "generate_id"
+}
diff --git a/pub/models/utils/type.go b/pub/models/utils/type.go
new file mode 100644
index 0000000..394afbc
--- /dev/null
+++ b/pub/models/utils/type.go
@@ -0,0 +1,18 @@
+package utils
+
+import (
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// @table:generate_id
+type GenerateId struct {
+ Id primitive.ObjectID `bson:"_id"`
+ UpdateAt time.Time `bson:"updateAt"`
+ BusinessId string `bson:"businessId"`
+ MaxId int64 `bson:"maxId"`
+ Step int64 `bson:"step"`
+ Desc string `bson:"desc"`
+ CreateAt time.Time `bson:"createAt"`
+}
diff --git a/pub/models/videocategorymod/category.go b/pub/models/videocategorymod/category.go
new file mode 100644
index 0000000..a35d2cb
--- /dev/null
+++ b/pub/models/videocategorymod/category.go
@@ -0,0 +1,132 @@
+package videocategorymod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoCategory)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCategory.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCategory.ContentType, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCategory.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoCategoryIndex) Create VideoCategory Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoCategory 插入一个渠道统计
+func InsertOneVideoCategory(ctx context.Context, qa *VideoCategory) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoCategorymod:InsertOneVideoCategory db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoCategory 删除一个渠道统计
+func DelOneVideoCategory(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCategorymod:DelOneVideoCategory db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoCategory 查找一个渠道统计
+func FindOneVideoCategory(ctx context.Context, filter bson.M) (
+ *VideoCategory, *srvdef.SrvErr) {
+ var VideoCategory VideoCategory
+ if err := db.FindOneEx(ctx, &VideoCategory, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoCategorymod:FindOneVideoCategory db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoCategory, nil
+}
+
+func FindManyVideoCategorys(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoCategoryList, *srvdef.SrvErr) {
+ data := make(VideoCategoryList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoCategorys", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoCategorys 统计渠道统计数量
+func CountVideoCategorys(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoCategorys db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoCategory 更新一个渠道统计
+func UpdateOneVideoCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVideoCategory(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoCategory db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/videocategorymod/tag.go b/pub/models/videocategorymod/tag.go
new file mode 100755
index 0000000..cb37ff8
--- /dev/null
+++ b/pub/models/videocategorymod/tag.go
@@ -0,0 +1,46 @@
+package videocategorymod
+
+type TagVideoCategory struct {
+ ContentType string
+ CreatedAt string
+ Desc string
+ Id string
+ IndexNow string
+ Name string
+ Rank string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ SortType string
+ Status string
+ Title string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoCategory TagVideoCategory
+}
+
+var TN struct {
+ VideoCategory string
+}
+
+func init() {
+ FN.VideoCategory.ContentType = "contentType"
+ FN.VideoCategory.CreatedAt = "createdAt"
+ FN.VideoCategory.Desc = "desc"
+ FN.VideoCategory.Id = "_id"
+ FN.VideoCategory.IndexNow = "indexNow"
+ FN.VideoCategory.Name = "name"
+ FN.VideoCategory.Rank = "rank"
+ FN.VideoCategory.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoCategory.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoCategory.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoCategory.SortType = "sortType"
+ FN.VideoCategory.Status = "status"
+ FN.VideoCategory.Title = "title"
+ FN.VideoCategory.UpdatedAt = "updatedAt"
+ TN.VideoCategory = "video_category"
+}
\ No newline at end of file
diff --git a/pub/models/videocategorymod/type.go b/pub/models/videocategorymod/type.go
new file mode 100644
index 0000000..4e9042b
--- /dev/null
+++ b/pub/models/videocategorymod/type.go
@@ -0,0 +1,30 @@
+package videocategorymod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_category
+type VideoCategory struct {
+ Id int64 `json:"id" bson:"_id,omitempty"` // Id
+ Name string `json:"name" bson:"name"` // 分类名称
+ Title string `json:"title" bson:"title"` // APP显示
+ Desc string `json:"desc" bson:"desc"` // 描述
+ Status bool `json:"status" bson:"status"` // 上架状态
+ ContentType int64 `json:"contentType" bson:"contentType"` // 内容:0: 默认 1:首页 2:今日 3:合集
+ SortType int64 `json:"sortType" bson:"sortType"` // 排序方式 0:上线时间 1:观看次数 2排序
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoCategoryList []*VideoCategory
+type VideoSortType int32
+
+const (
+ VideoSortTypeAddTime VideoSortType = 0 // 上线时间
+ VideoSortTypeWatchs VideoSortType = 1 // 观看次数
+ VideoSortTypeRank VideoSortType = 2 // 视频排序
+)
\ No newline at end of file
diff --git a/pub/models/videocollectionmediamod/tag.go b/pub/models/videocollectionmediamod/tag.go
new file mode 100755
index 0000000..4c3f916
--- /dev/null
+++ b/pub/models/videocollectionmediamod/tag.go
@@ -0,0 +1,30 @@
+package videocollectionmediamod
+
+type TagVideoCollectionMediaModel struct {
+ ID string
+ CollectionID string
+ VideoId string
+ Status string
+ Rank string
+ CreatedAt string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoCollectionMediaModel TagVideoCollectionMediaModel
+}
+
+var TN struct {
+ VideoCollectionMediaModel string
+}
+
+func init() {
+ FN.VideoCollectionMediaModel.ID = "_id"
+ FN.VideoCollectionMediaModel.CollectionID = "collectionId"
+ FN.VideoCollectionMediaModel.VideoId = "videoId"
+ FN.VideoCollectionMediaModel.Status = "status"
+ FN.VideoCollectionMediaModel.Rank = "rank"
+ FN.VideoCollectionMediaModel.CreatedAt = "createdAt"
+ FN.VideoCollectionMediaModel.UpdatedAt = "updatedAt"
+ TN.VideoCollectionMediaModel = "video_collection_media"
+}
\ No newline at end of file
diff --git a/pub/models/videocollectionmediamod/type.go b/pub/models/videocollectionmediamod/type.go
new file mode 100644
index 0000000..d966f1b
--- /dev/null
+++ b/pub/models/videocollectionmediamod/type.go
@@ -0,0 +1,21 @@
+package videocollectionmediamod
+
+import (
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+)
+
+// VideoCollectionMediaModel 视频合集资源
+// @table:video_collection_media
+type VideoCollectionMediaModel struct {
+ ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 唯一ID
+ CollectionID int64 `json:"collectionId" bson:"collectionId"` // 合集ID
+ VideoId int64 `json:"videoId" bson:"videoId"` // 视频ID
+ Status videoinformationmod.InformationSatus `json:"status" bson:"status"` // 状态
+ Rank int64 `json:"rank" bson:"rank"` // 排序
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 刷新时间
+
+}
+type VideoCollectionMediaModelList []*VideoCollectionMediaModel
\ No newline at end of file
diff --git a/pub/models/videocollectionmediamod/videocollection.go b/pub/models/videocollectionmediamod/videocollection.go
new file mode 100644
index 0000000..dcbfcb3
--- /dev/null
+++ b/pub/models/videocollectionmediamod/videocollection.go
@@ -0,0 +1,154 @@
+package videocollectionmediamod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoCollectionMediaModel)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CollectionID, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CollectionID, Value: 1},
+ {Key: FN.VideoCollectionMediaModel.Rank, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CollectionID, Value: 1},
+ {Key: FN.VideoCollectionMediaModel.VideoId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.VideoId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCollectionMediaModel.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoCollectionMediaModelIndex) Create VideoCollectionMediaModel Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoCollectionMediaModel 插入一个渠道统计
+func InsertOneVideoCollectionMediaModel(ctx context.Context, qa *VideoCollectionMediaModel) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoCollectionMediaModelmod:InsertOneVideoCollectionMediaModel db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoCollectionMediaModel 删除一个渠道统计
+func DelOneVideoCollectionMediaModel(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCollectionMediaModelmod:DelOneVideoCollectionMediaModel db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+func DelManyVideoCollectionMediaModel(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
+ count, err := db.DeleteMany(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCollectionMediaModelmod:DelManyVideoCollectionMediaModel db.DeleteMany failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoCollectionMediaModel 查找一个渠道统计
+func FindOneVideoCollectionMediaModel(ctx context.Context, filter bson.M) (
+ *VideoCollectionMediaModel, *srvdef.SrvErr) {
+ var VideoCollectionMediaModel VideoCollectionMediaModel
+ if err := db.FindOneEx(ctx, &VideoCollectionMediaModel, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoCollectionMediaModelmod:FindOneVideoCollectionMediaModel db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoCollectionMediaModel, nil
+}
+
+func FindManyVideoCollectionMediaModels(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoCollectionMediaModelList, *srvdef.SrvErr) {
+ data := make(VideoCollectionMediaModelList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoCollectionMediaModels", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoCollectionMediaModels 统计渠道统计数量
+func CountVideoCollectionMediaModels(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoCollectionMediaModels db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoCollectionMediaModel 更新一个渠道统计
+func UpdateOneVideoCollectionMediaModel(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoCollectionMediaModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoCollectionMediaModel db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func InsertManyVideoCollectionMedia(ctx context.Context, list []*VideoCollectionMediaModel) *srvdef.SrvErr {
+ _, err := db.InsertMany(ctx, &list)
+ if err != nil {
+ log.Warn("post:InsertManyVideoCollectionMediaModel db.InsertMany failed",
+ log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/videocreatermod/creater.go b/pub/models/videocreatermod/creater.go
new file mode 100644
index 0000000..922fe93
--- /dev/null
+++ b/pub/models/videocreatermod/creater.go
@@ -0,0 +1,127 @@
+package videocreatermod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoCreaterInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCreaterInfo.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoCreaterInfo.CreatedAt, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoCreaterInfoIndex) Create VideoCreaterInfo Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoCreaterInfo 插入一个渠道统计
+func InsertOneVideoCreaterInfo(ctx context.Context, qa *VideoCreaterInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ log.Warn("VideoCreaterInfomod:InsertOneVideoCreaterInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoCreaterInfo 删除一个渠道统计
+func DelOneVideoCreaterInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoCreaterInfomod:DelOneVideoCreaterInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoCreaterInfo 查找一个渠道统计
+func FindOneVideoCreaterInfo(ctx context.Context, filter bson.M) (
+ *VideoCreaterInfo, *srvdef.SrvErr) {
+ var VideoCreaterInfo VideoCreaterInfo
+ if err := db.FindOneEx(ctx, &VideoCreaterInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoCreaterInfomod:FindOneVideoCreaterInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoCreaterInfo, nil
+}
+
+func FindManyVideoCreaterInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoCreaterInfoList, *srvdef.SrvErr) {
+ data := make(VideoCreaterInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoCreaterInfos", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoCreaterInfos 统计渠道统计数量
+func CountVideoCreaterInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoCreaterInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoCreaterInfo 更新一个渠道统计
+func UpdateOneVideoCreaterInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoCreaterInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoCreaterInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVideoCreaterInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoCreaterInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/videocreatermod/tag.go b/pub/models/videocreatermod/tag.go
new file mode 100755
index 0000000..21762f3
--- /dev/null
+++ b/pub/models/videocreatermod/tag.go
@@ -0,0 +1,38 @@
+package videocreatermod
+
+type TagVideoCreaterInfo struct {
+ CreatedAt string
+ Id string
+ IndexNow string
+ Name string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoCreaterInfo TagVideoCreaterInfo
+}
+
+var TN struct {
+ VideoCreaterInfo string
+}
+
+func init() {
+ FN.VideoCreaterInfo.CreatedAt = "createdAt"
+ FN.VideoCreaterInfo.Id = "_id"
+ FN.VideoCreaterInfo.IndexNow = "indexNow"
+ FN.VideoCreaterInfo.Name = "name"
+ FN.VideoCreaterInfo.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoCreaterInfo.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoCreaterInfo.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoCreaterInfo.Sort = "sort"
+ FN.VideoCreaterInfo.Status = "status"
+ FN.VideoCreaterInfo.UpdatedAt = "updatedAt"
+ TN.VideoCreaterInfo = "video_creater"
+}
\ No newline at end of file
diff --git a/pub/models/videocreatermod/type.go b/pub/models/videocreatermod/type.go
new file mode 100644
index 0000000..951bbed
--- /dev/null
+++ b/pub/models/videocreatermod/type.go
@@ -0,0 +1,19 @@
+package videocreatermod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_creater
+type VideoCreaterInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Name string `json:"name" bson:"name"` // 帖子标题
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoCreaterInfoList []*VideoCreaterInfo
\ No newline at end of file
diff --git a/pub/models/videoinformationmod/information.go b/pub/models/videoinformationmod/information.go
new file mode 100644
index 0000000..2d40e81
--- /dev/null
+++ b/pub/models/videoinformationmod/information.go
@@ -0,0 +1,248 @@
+package videoinformationmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoInformation)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CategoryIds, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.CategoryIds, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Video.FileId, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.CategoryIds, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: "nodes.nodeId", Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.CreatorId, Value: 1},
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.Tags, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.Real.WatchTimes, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Status, Value: 1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ {Key: FN.VideoInformation.IsTop, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.IsTop, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoInformation.Sort, Value: -1},
+ {Key: FN.VideoInformation.AddedTime, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoInformationIndex) Create VideoInformation Index failed! err:%+v:", err))
+ }
+}
+
+// InsertOneVideoInformation 插入一个渠道统计
+func InsertOneVideoInformation(ctx context.Context, qa *VideoInformation) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoInformationmod:InsertOneVideoInformation db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+func InsertManyVideoInformation(ctx context.Context, qa []*VideoInformation) *srvdef.SrvErr {
+ if _, err := db.InsertMany(ctx, &qa); err != nil {
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoInformation 删除一个渠道统计
+func DelOneVideoInformation(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoInformationmod:DelOneVideoInformation db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoInformation 查找一个渠道统计
+func FindOneVideoInformation(ctx context.Context, filter bson.M) (
+ *VideoInformation, *srvdef.SrvErr) {
+ var VideoInformation VideoInformation
+ if err := db.FindOneEx(ctx, &VideoInformation, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoInformationmod:FindOneVideoInformation db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoInformation, nil
+}
+
+func FindManyVideoInformations(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoInformationList, *srvdef.SrvErr) {
+ data := make(VideoInformationList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoInformations", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoInformations 统计渠道统计数量
+func CountVideoInformations(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoInformations db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoInformation 更新一个渠道统计
+func UpdateOneVideoInformation(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoInformation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoInformation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+
+func UpdateManyVideoInformation(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoInformation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/videoinformationmod/tag.go b/pub/models/videoinformationmod/tag.go
new file mode 100755
index 0000000..839afa2
--- /dev/null
+++ b/pub/models/videoinformationmod/tag.go
@@ -0,0 +1,100 @@
+package videoinformationmod
+
+type TagVideoInformation struct {
+ AddedTime string
+ CategoryIds string
+ CoverImg string
+ CreatedAt string
+ CreatorId string
+ CreatorName string
+ Desc string
+ FirstTag string
+ Id string
+ IndexNow string
+ IsHot string
+ IsTop string
+ Keyworld string
+ MediaType string
+ Nodes string
+ Real struct {
+ Comments string
+ MonthWatchs string
+ ShowTimes string
+ TodayWatchs string
+ WatchTimes string
+ WeekWatchs string
+ }
+ Remark string
+ SecondTags string
+ Sort string
+ Status string
+ Tags string
+ Title string
+ UpdatedAt string
+ PageUpdatedAt string
+ Video struct {
+ Cover string
+ FileId string
+ Height string
+ PreMp4FileNameFs string
+ TaskId string
+ Url string
+ VideoTime string
+ Width string
+ UnencryptedPreMp4 string
+ }
+ VideoCount string
+ UnencryptedImage string
+}
+
+var FN struct {
+ VideoInformation TagVideoInformation
+}
+
+var TN struct {
+ VideoInformation string
+}
+
+func init() {
+ FN.VideoInformation.AddedTime = "addedTime"
+ FN.VideoInformation.CategoryIds = "categoryIds"
+ FN.VideoInformation.CoverImg = "coverImg"
+ FN.VideoInformation.CreatedAt = "createdAt"
+ FN.VideoInformation.CreatorId = "creatorId"
+ FN.VideoInformation.CreatorName = "creatorName"
+ FN.VideoInformation.Desc = "desc"
+ FN.VideoInformation.FirstTag = "firstTag"
+ FN.VideoInformation.Id = "_id"
+ FN.VideoInformation.IndexNow = "indexNow"
+ FN.VideoInformation.IsHot = "isHot"
+ FN.VideoInformation.IsTop = "isTop"
+ FN.VideoInformation.Keyworld = "keyworld"
+ FN.VideoInformation.MediaType = "mediaType"
+ FN.VideoInformation.Nodes = "nodes"
+ FN.VideoInformation.Real.Comments = "real.comments"
+ FN.VideoInformation.Real.MonthWatchs = "real.monthWatchs"
+ FN.VideoInformation.Real.ShowTimes = "real.showTimes"
+ FN.VideoInformation.Real.TodayWatchs = "real.todayWatchs"
+ FN.VideoInformation.Real.WatchTimes = "real.watchTimes"
+ FN.VideoInformation.Real.WeekWatchs = "real.weekWatchs"
+ FN.VideoInformation.Remark = "remark"
+ FN.VideoInformation.SecondTags = "secondTags"
+ FN.VideoInformation.Sort = "sort"
+ FN.VideoInformation.Status = "status"
+ FN.VideoInformation.Tags = "tags"
+ FN.VideoInformation.Title = "title"
+ FN.VideoInformation.UpdatedAt = "updatedAt"
+ FN.VideoInformation.PageUpdatedAt = "pageUpdatedAt"
+ FN.VideoInformation.Video.Cover = "video.cover"
+ FN.VideoInformation.Video.FileId = "video.fileId"
+ FN.VideoInformation.Video.Height = "video.height"
+ FN.VideoInformation.Video.PreMp4FileNameFs = "video.preMp4FileNameFs"
+ FN.VideoInformation.Video.TaskId = "video.taskId"
+ FN.VideoInformation.Video.Url = "video.url"
+ FN.VideoInformation.Video.VideoTime = "video.videoTime"
+ FN.VideoInformation.Video.Width = "video.width"
+ FN.VideoInformation.Video.UnencryptedPreMp4 = "video.unencryptedPreMp4"
+ FN.VideoInformation.VideoCount = "videoCount"
+ FN.VideoInformation.UnencryptedImage = "unencryptedImage"
+ TN.VideoInformation = "video_information"
+}
diff --git a/pub/models/videoinformationmod/type.go b/pub/models/videoinformationmod/type.go
new file mode 100644
index 0000000..b340df8
--- /dev/null
+++ b/pub/models/videoinformationmod/type.go
@@ -0,0 +1,85 @@
+package videoinformationmod
+
+import (
+ "time"
+)
+
+// @table:video_information
+type VideoInformation struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ MediaType MediaType `json:"mediaType" bson:"mediaType"` // 资源类型
+ AddedTime time.Time `json:"addedTime" bson:"addedTime"` // 帖子上架时间
+ CreatorId int64 `json:"creatorId" bson:"creatorId"` // 作者Id
+ CreatorName string `json:"creatorName" bson:"creatorName"` // 作者名称
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Nodes []Node `json:"nodes" bson:"nodes"` // 节点
+ Status InformationSatus `json:"status" bson:"status"` // 帖子上架状态
+ Video VideoInfo `json:"video" bson:"video"` // 视频
+ CategoryIds []int64 `json:"categoryIds" bson:"categoryIds"` // 分类Id
+ Tags []int64 `json:"tags" bson:"tags"` // 标签
+ IsHot bool `json:"isHot" bson:"isHot"` // 是否热门
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ Real Flow `json:"real" bson:"real"` // 真实流量
+ VideoCount int64 `json:"videoCount" bson:"videoCount"` // 合集视频数量
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ PageUpdatedAt time.Time `json:"pageUpdatedAt" bson:"pageUpdatedAt"` // 页面展示更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+ IsTop bool `json:"isTop" bson:"isTop"` // 是否置顶
+}
+type VideoInformationList []*VideoInformation
+
+type Node struct {
+ NodeId string `json:"nodeId" bson:"nodeId"` // 节点ID
+ Title string `bson:"title" json:"title"` // 节点标题
+ Videos []VideoInfo `json:"videos" bson:"videos"` // 视频信息列表
+ Images []string `json:"images" bson:"images"` // 图片
+ Url string `json:"url" bson:"url"` // 链接
+ Text string `json:"text" bson:"text"` // 文本
+}
+
+type VideoInfo struct {
+ TaskId string `json:"taskId" bson:"taskId"` // 视频ID(视频才会存在)
+ FileId string `json:"fileId" bson:"fileId"` // 媒资库ID
+ Url string `json:"url" bson:"url"` // 视频URl
+ Cover string `json:"cover" bson:"cover"` // 视频封面
+ Height int64 `json:"height" bson:"height"` // 分辨率高(视频才会存在)
+ Width int64 `json:"width" bson:"width"` // 分辨率宽(视频才会存在)
+ VideoTime int32 `bson:"videoTime" json:"videoTime"` // 影片长度
+ PreMp4FileNameFs string `bson:"preMp4FileNameFs,omitempty" json:"preMp4FileNameFs"` // 预览MP4文件
+ UnencryptedPreMp4 string `bson:"unencryptedPreMp4,omitempty" json:"unencryptedPreMp4"` // 不加密的预览MP4
+ VttFile string `bson:"vttFile" json:"vttFile"` // 雪碧图
+ TotalFrames int64 `bson:"totalFrames,omitempty" json:"totalFrames"` // 总帧数
+}
+
+type InformationSatus int32
+
+const (
+ InformationSatusWaiting InformationSatus = 0 // 初始化
+ InformationSatusVerifyOK InformationSatus = 1 // 上架
+ InformationSatusVerifyCancel InformationSatus = 2 // 下架
+)
+
+type Flow struct {
+ WatchTimes int64 `json:"watchTimes" bson:"watchTimes"` // 观看次数
+ ShowTimes int64 `json:"showTimes" bson:"showTimes"` // 展示次数
+ Comments int64 `json:"comments" bson:"comments"` // 评论条数
+ WeekWatchs int64 `json:"weekWatchs" bson:"weekWatchs"` // 本周观看次数
+ MonthWatchs int64 `json:"monthWatchs" bson:"monthWatchs"` // 本月观看次数
+ TodayWatchs int64 `json:"todayWatchs" bson:"todayWatchs"` // 当日观看次数
+}
+
+type MediaType int64
+
+const (
+ MediaType_Video MediaType = 0 // 视频
+ MediaType_Collection MediaType = 1 // 合集
+ MediaType_Advitise MediaType = 2 // 广告
+)
diff --git a/pub/models/videotagmod/tag.go b/pub/models/videotagmod/tag.go
new file mode 100755
index 0000000..58f2adf
--- /dev/null
+++ b/pub/models/videotagmod/tag.go
@@ -0,0 +1,38 @@
+package videotagmod
+
+type TagVideoTagInfo struct {
+ CreatedAt string
+ Id string
+ IndexNow string
+ SeoInfo struct {
+ Description string
+ Keyworld string
+ Title string
+ }
+ Sort string
+ Status string
+ Title string
+ UpdatedAt string
+}
+
+var FN struct {
+ VideoTagInfo TagVideoTagInfo
+}
+
+var TN struct {
+ VideoTagInfo string
+}
+
+func init() {
+ FN.VideoTagInfo.CreatedAt = "createdAt"
+ FN.VideoTagInfo.Id = "_id"
+ FN.VideoTagInfo.IndexNow = "indexNow"
+ FN.VideoTagInfo.SeoInfo.Description = "seoInfo.webDescription"
+ FN.VideoTagInfo.SeoInfo.Keyworld = "seoInfo.webKeyworld"
+ FN.VideoTagInfo.SeoInfo.Title = "seoInfo.webTitle"
+ FN.VideoTagInfo.Sort = "sort"
+ FN.VideoTagInfo.Status = "status"
+ FN.VideoTagInfo.Title = "title"
+ FN.VideoTagInfo.UpdatedAt = "updatedAt"
+ TN.VideoTagInfo = "video_taginfo"
+}
\ No newline at end of file
diff --git a/pub/models/videotagmod/taginfo.go b/pub/models/videotagmod/taginfo.go
new file mode 100644
index 0000000..910622d
--- /dev/null
+++ b/pub/models/videotagmod/taginfo.go
@@ -0,0 +1,132 @@
+package videotagmod
+
+import (
+ "context"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+ "videoweb/pub/mongos"
+
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 表名
+var db *mongos.Collection
+
+func Init(ctx context.Context) {
+ // 获取数据库表
+ db = mongos.Shop.Coll(TN.VideoTagInfo)
+ // 获取数据库表
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.Status, Value: 1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.CreatedAt, Value: -1},
+ },
+ },
+ {
+ Keys: bson.D{
+ {Key: FN.VideoTagInfo.Status, Value: 1},
+ {Key: FN.VideoTagInfo.Sort, Value: -1},
+ },
+ },
+ }
+ if _, err := db.CreateIndex(ctx, indexes); err != nil {
+ panic(fmt.Sprintf("(channelmod:initVideoTagInfoIndex) Create VideoTagInfo Index failed! err:%+v:", err))
+ }
+
+}
+
+// InsertOneVideoTagInfo 插入一个渠道统计
+func InsertOneVideoTagInfo(ctx context.Context, qa *VideoTagInfo) *srvdef.SrvErr {
+ if _, err := db.InsertOne(ctx, qa); err != nil {
+ // log.Warn("VideoTagInfomod:InsertOneVideoTagInfo db.InsertOne failed", log.E(err))
+ return srvdef.New(srvdef.ErrDbInsertError, "", "")
+ }
+ return nil
+}
+
+// DelOneVideoTagInfo 删除一个渠道统计
+func DelOneVideoTagInfo(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.DeleteOne(ctx, filter)
+ if err != nil {
+ log.Warn("VideoTagInfomod:DelOneVideoTagInfo db.DeleteOne failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
+ }
+ return count, nil
+}
+
+// FindOneVideoTagInfo 查找一个渠道统计
+func FindOneVideoTagInfo(ctx context.Context, filter bson.M) (
+ *VideoTagInfo, *srvdef.SrvErr) {
+ var VideoTagInfo VideoTagInfo
+ if err := db.FindOneEx(ctx, &VideoTagInfo, filter); err != nil {
+ if err != mongo.ErrNoDocuments {
+ log.Warn("VideoTagInfomod:FindOneVideoTagInfo db.FindOne failed",
+ log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return nil, srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return &VideoTagInfo, nil
+}
+
+func FindManyVideoTagInfos(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (VideoTagInfoList, *srvdef.SrvErr) {
+ data := make(VideoTagInfoList, 0)
+ if err := db.Find(ctx, &data, filter, opts...); nil != err {
+ log.Warn("faq:FindManyVideoTagInfos", log.E(err), log.Any("filter", filter))
+ return nil, srvdef.New(srvdef.ErrDbQuery.Code, "", "")
+ }
+ return data, nil
+}
+
+// CountVideoTagInfos 统计渠道统计数量
+func CountVideoTagInfos(ctx context.Context, filter bson.M) (
+ int64, *srvdef.SrvErr) {
+ count, err := db.Count(ctx, filter)
+ if err != nil {
+ log.Warn("faqmod:CountVideoTagInfos db.Count failed",
+ log.E(err), log.Any("filter", filter))
+ return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
+ }
+ return count, nil
+}
+
+// UpdateOneVideoTagInfo 更新一个渠道统计
+func UpdateOneVideoTagInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateOne(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateOneVideoTagInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount != 1 {
+ log.Warn("post:UpdateOneVideoTagInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
+func UpdateManyVideoTagInfo(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
+ result, err := db.UpdateMany(ctx, filter, update)
+ if err != nil {
+ log.Warn("post:UpdateManyVideoTagInfo db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrDbUpdateError, "", "")
+ }
+ if result.MatchedCount < 1 {
+ log.Warn("post:UpdateManyVideoInfomation db.UpdateOne failed",
+ log.E(err), log.Any("filter", filter))
+ return srvdef.New(srvdef.ErrNoData, "", "")
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pub/models/videotagmod/type.go b/pub/models/videotagmod/type.go
new file mode 100644
index 0000000..5685931
--- /dev/null
+++ b/pub/models/videotagmod/type.go
@@ -0,0 +1,19 @@
+package videotagmod
+
+import (
+ "time"
+ "videoweb/pub/pubdef"
+)
+
+// @table:video_taginfo
+type VideoTagInfo struct {
+ Id int64 `json:"id" bson:"_id,omitempty"`
+ Title string `json:"title" bson:"title"` // 帖子标题
+ Status bool `json:"status" bson:"status"` // 上架状态
+ Sort int64 `json:"sort" bson:"sort"` // 排序
+ SeoInfo pubdef.SeoValue `json:"seoInfo" bson:"seoInfo"` // Seo相关信息
+ IndexNow int64 `json:"indexNow" bson:"indexNow"` // 是否需要提交 0:代表提交 1:表示已提交 运营通过接口可以修改
+ UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 更新时间
+ CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
+}
+type VideoTagInfoList []*VideoTagInfo
\ No newline at end of file
diff --git a/pub/mongos/QueryBuilder.go b/pub/mongos/QueryBuilder.go
new file mode 100644
index 0000000..9cdb5df
--- /dev/null
+++ b/pub/mongos/QueryBuilder.go
@@ -0,0 +1,150 @@
+package mongos
+
+import (
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+type QueryBuilder struct {
+ query bson.M
+}
+
+func NewQueryBuilder() *QueryBuilder {
+ return &QueryBuilder{query: bson.M{}}
+}
+
+func (q *QueryBuilder) Build() bson.M {
+ return q.query
+}
+func (q *QueryBuilder) addCondition(condition bool, field string, value interface{}) {
+ if condition {
+ q.query[field] = value
+ }
+}
+
+func (q *QueryBuilder) Eq(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = value
+ }
+ return q
+}
+
+func (q *QueryBuilder) Ne(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$ne": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Or(conditions ...bson.M) *QueryBuilder {
+ if len(conditions) > 0 {
+ q.query["$or"] = conditions
+ }
+ return q
+}
+
+func (q *QueryBuilder) Set(value bson.M) *QueryBuilder {
+ q.query["$set"] = value
+ return q
+}
+
+func (q *QueryBuilder) Gt(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$gt": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Gte(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$gte": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Lt(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$lt": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Lte(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$lte": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) In(field string, values *[]int64, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$in": *values}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Nin(field string, values []interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$nin": values}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Regex(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"regex": value.(string)}
+ }
+ return q
+}
+
+func (q *QueryBuilder) All(field string, values []interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$all": values}
+ }
+ return q
+}
+
+func (q *QueryBuilder) ElemMatch(field string, value interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$elemMatch": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Size(field string, value int, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$size": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Exists(field string, value bool, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$exists": value}
+ }
+ return q
+}
+
+func (q *QueryBuilder) Mod(field string, divisor, remainder int, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$mod": []int{divisor, remainder}}
+ }
+ return q
+}
+
+func (q *QueryBuilder) GeoWithin(field string, geometry interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$geoWithin": bson.M{
+ "$geometry": geometry,
+ }}
+ }
+ return q
+}
+
+func (q *QueryBuilder) GeoIntersects(field string, geometry interface{}, condition ...bool) *QueryBuilder {
+ if len(condition) == 0 || condition[0] {
+ q.query[field] = bson.M{"$geoIntersects": bson.M{
+ "$geometry": geometry,
+ }}
+ }
+ return q
+}
diff --git a/pub/mongos/base_mod.go b/pub/mongos/base_mod.go
new file mode 100644
index 0000000..5b402c7
--- /dev/null
+++ b/pub/mongos/base_mod.go
@@ -0,0 +1,582 @@
+package mongos
+
+import (
+ "context"
+ "errors"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/utils"
+
+ "reflect"
+ "strings"
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type Collection struct {
+ name string
+ coll *mongo.Collection
+}
+
+func (b *Collection) FindOne(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOneOptions) error {
+ err := b.coll.FindOne(ctx, query, opts...).Decode(bind)
+ if err != nil && err == mongo.ErrNoDocuments {
+ return nil
+ }
+ return err
+}
+
+func (b *Collection) FindOneEx(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOneOptions) error {
+ err := b.coll.FindOne(ctx, query, opts...).Decode(bind)
+ return err
+}
+
+func (b *Collection) FindMOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update interface{}, opts ...*options.FindOneAndUpdateOptions) error {
+ setReturn := false
+ for _, opt := range opts {
+ if opt != nil && opt.ReturnDocument != nil {
+ setReturn = true
+ break
+ }
+ }
+ if !setReturn {
+ after := options.After
+ opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+ }
+ um := utils.Struct2Map(update)
+ // um["update_at"] = time.Now()
+ // set := bson.M{
+ // "$set": um,
+ // }
+ result := b.coll.FindOneAndUpdate(ctx, query, um, opts...)
+ err := result.Decode(bind)
+ return err
+}
+
+func (b *Collection) FindMOneAndUpdateInc(ctx context.Context, bind interface{}, query bson.M, update interface{}, inc interface{}, opts ...*options.FindOneAndUpdateOptions) error {
+ um := utils.Struct2Map(update)
+ um["update_at"] = time.Now()
+ im := utils.Struct2Map(inc)
+ set := bson.M{
+ "$set": um,
+ }
+ if im != nil {
+ set["$inc"] = inc
+ }
+
+ setReturn := false
+ for _, opt := range opts {
+ if opt != nil && opt.ReturnDocument != nil {
+ setReturn = true
+ break
+ }
+ }
+ if !setReturn {
+ after := options.After
+ opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+ }
+
+ result := b.coll.FindOneAndUpdate(ctx, query, set, opts...)
+ err := result.Decode(bind)
+ return err
+}
+
+// func (b *Collection) FindOneAndUpdateTiny(
+// ctx context.Context,
+// query interface{},
+// update bson.M,
+// opts ...*options.FindOneAndUpdateOptions) error {
+// result := b.collection().FindOneAndUpdate(ctx, query, update, opts...)
+// return result.Err()
+// }
+
+// func (b *MongoTool) FindOneAndUpdateReturnTiny(ctx context.Context,
+// bind interface{},
+// query bson.M,
+// update bson.M,
+// afterDoc bool,
+// opts ...*options.FindOneAndUpdateOptions) error {
+
+// if afterDoc {
+// opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.After))
+// } else {
+// opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.Before))
+// }
+
+// result := b.collection().FindOneAndUpdate(ctx, query, update, opts...).Decode(bind)
+// return result
+// }
+
+// func (b *Collection) FindOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
+// setReturn := false
+// for _, opt := range opts {
+// if opt != nil && opt.ReturnDocument != nil {
+// setReturn = true
+// break
+// }
+// }
+// if !setReturn {
+// after := options.After
+// opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+// }
+// b.setUpdatedAt(update, time.Now())
+// result := b.collection().FindOneAndUpdate(ctx, query, update, opts...)
+// err := result.Decode(bind)
+// return err
+// }
+
+func (b *Collection) Find(ctx context.Context, bind interface{}, query bson.M, opt ...*options.FindOptions) error {
+ err := b.validSlice(bind)
+ cur, err := b.coll.Find(ctx, query, opt...)
+ if err != nil && err == mongo.ErrNoDocuments {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ err = cur.All(ctx, bind)
+ return err
+}
+
+// func (b *Collection) FindByPage(ctx context.Context, bind interface{}, query bson.M, pageAndSort *PageAndSort, opts ...*options.FindOptions) error {
+// err := b.validSlice(bind)
+
+// if err != nil {
+// return err
+// }
+
+// opt := &options.FindOptions{}
+// if pageAndSort == nil {
+// return errors.New("page must not nil")
+// }
+
+// if pageAndSort.Page < 0 || pageAndSort.PageSize == 0 {
+// return errors.New("page and PageSize must not nil")
+// }
+
+// skip := pageAndSort.Page * pageAndSort.PageSize
+// opt.Skip = &skip
+// opt.Limit = &pageAndSort.PageSize
+// if pageAndSort.Sort != nil {
+// opt.Sort = pageAndSort.Sort
+// }
+// opts = append(opts, opt)
+// cur, err := b.collection().Find(ctx, query, opts...)
+// if err != nil && err == mongo.ErrNoDocuments {
+// return nil
+// } else if err != nil {
+// return err
+// }
+// err = cur.All(ctx, bind)
+// return err
+
+// }
+
+func (b *Collection) FindOneAndDelete(ctx context.Context, bind interface{}, query bson.M, opts ...*options.FindOneAndDeleteOptions) error {
+ result := b.coll.FindOneAndDelete(ctx, query, opts...)
+ if result.Err() != nil {
+ return result.Err()
+ }
+ return result.Decode(bind)
+}
+
+func (b *Collection) InsertOneTiny(ctx context.Context, doc interface{}) (err error) {
+ _, err = b.coll.InsertOne(ctx, doc)
+ return
+}
+
+func (b *Collection) InsertOne(ctx context.Context, doc interface{}) (int, error) {
+ result, err := b.coll.InsertOne(ctx, doc)
+ if err != nil {
+ // log.Info("Collection:InsertOne failed", log.Any("colName", b.name), log.E(err))
+ return 0, err
+ }
+ if _, ok := result.InsertedID.(primitive.ObjectID); ok {
+ return 1, err
+ }
+ return 0, err
+}
+func (b *Collection) IDInsertOne(ctx context.Context, doc interface{}) (string, error) {
+ result, err := b.coll.InsertOne(ctx, doc)
+ if err != nil {
+ log.Info("Collection:InsertOne failed", log.Any("colName", b.name), log.E(err))
+ return "", err
+ }
+ if id, ok := result.InsertedID.(primitive.ObjectID); ok {
+ return id.Hex(), err
+ }
+ return "", err
+}
+
+func (b *Collection) validSlice(bind interface{}) error {
+ if reflect.TypeOf(bind).Kind() != reflect.Ptr {
+ return errors.New("interface must be a Ptr")
+ }
+ if reflect.TypeOf(bind).Elem().Kind() != reflect.Slice {
+ return errors.New("interface must be a slice")
+ }
+ return nil
+}
+
+func (b *Collection) InsertMany(ctx context.Context, docs interface{}) ([]interface{}, error) {
+ err := b.validSlice(docs)
+ if err != nil {
+ return nil, err
+ }
+ res := b.setTimeForSlice(docs)
+ result, err := b.coll.InsertMany(ctx, res)
+ if err != nil {
+ return nil, err
+ }
+ return result.InsertedIDs, err
+}
+
+// struct方式
+func (b *Collection) UpdateMOne(ctx context.Context, query bson.M, update interface{}) (*mongo.UpdateResult, error) {
+ um := utils.Struct2Map(update)
+ um["update_at"] = time.Now()
+ set := bson.M{
+ "$set": um,
+ }
+ return b.coll.UpdateOne(ctx, query, set)
+}
+
+func (b *Collection) UpdateMOneInc(ctx context.Context, query bson.M, update interface{}, inc interface{}) (*mongo.UpdateResult, error) {
+ um := utils.Struct2Map(update)
+ um["update_at"] = time.Now()
+ set := bson.M{
+ "$set": um,
+ }
+ im := utils.Struct2Map(inc)
+ if im != nil {
+ set["$inc"] = im
+ }
+ return b.coll.UpdateOne(ctx, query, set)
+}
+
+// 原生bson.M 方式
+func (b *Collection) UpdateOne(ctx context.Context, query bson.M, update bson.M, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) {
+ //b.setUpdatedAt(update, time.Now())
+ return b.coll.UpdateOne(ctx, query, update, opts...)
+}
+
+func (b *Collection) UpsertOne(ctx context.Context, query bson.M, update bson.M) (*mongo.UpdateResult, error) {
+ opt := &options.UpdateOptions{}
+ opt.SetUpsert(true)
+ //b.setDeaultTime(update)
+ result, err := b.coll.UpdateOne(ctx, query, update, opt)
+ return result, err
+
+}
+
+func (b *Collection) UpdateMMany(ctx context.Context, query bson.M, update interface{}) (*mongo.UpdateResult, error) {
+ // b.setUpdatedAt(update, time.Now())
+ return b.coll.UpdateMany(ctx, query, update)
+}
+
+func (b *Collection) UpdateMany(ctx context.Context, query bson.M, update bson.M) (*mongo.UpdateResult, error) {
+ //b.setUpdatedAt(update, time.Now())
+ return b.coll.UpdateMany(ctx, query, update)
+}
+
+// func (b *Collection) DeleteById(ctx context.Context, id interface{}) (int64, error) {
+
+// param := bson.M{"_id": id}
+// result, err := b.collection().DeleteOne(ctx, param)
+// if err != nil {
+// return 0, err
+// }
+// return result.DeletedCount, nil
+
+// }
+
+func (b *Collection) DeleteOne(ctx context.Context, query bson.M, opts ...*options.DeleteOptions) (int64, error) {
+ result, err := b.coll.DeleteOne(ctx, query)
+ if err != nil {
+ return 0, err
+ }
+ return result.DeletedCount, nil
+}
+
+func (b *Collection) DeleteMany(ctx context.Context, query bson.M, opts ...*options.DeleteOptions) (int64, error) {
+ result, err := b.coll.DeleteMany(ctx, query, opts...)
+ if err != nil {
+ return 0, err
+ }
+ deletedCount := result.DeletedCount
+ return deletedCount, nil
+
+}
+
+func (b *Collection) Distince(ctx context.Context, filter bson.M, fieldName string) ([]interface{}, error) {
+ return b.coll.Distinct(ctx, fieldName, filter)
+}
+
+func (b *Collection) Aggregate(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error {
+
+ err := b.validSlice(bind)
+ if err != nil {
+ return err
+ }
+ //bs, _ := json.Marshal(pipeline)
+ //log.Debug(string(bs))
+ cur, err := b.coll.Aggregate(ctx, pipeline, opts...)
+ if err != nil {
+ return err
+ }
+ return cur.All(ctx, bind)
+}
+
+func (b *Collection) ObjectAggregate(ctx context.Context, bind interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error {
+ cur, err := b.coll.Aggregate(ctx, pipeline, opts...)
+ if err != nil {
+ return err
+ }
+ if cur.Next(ctx) {
+ return cur.Decode(bind)
+ }
+ return nil
+}
+
+func (b *Collection) QueryOne(ctx context.Context, bind interface{}, query interface{}, opts ...*options.FindOneOptions) error {
+ err := b.coll.FindOne(ctx, query, opts...).Decode(bind)
+ if err != nil && err == mongo.ErrNoDocuments {
+ return nil
+ }
+ return err
+}
+
+func (b *Collection) Count(ctx context.Context, query interface{}, opts ...*options.CountOptions) (int64, error) {
+ return b.coll.CountDocuments(ctx, query, opts...)
+}
+
+func (b *Collection) EstimatedCount(ctx context.Context, opts ...*options.EstimatedDocumentCountOptions) (int64, error) {
+ return b.coll.EstimatedDocumentCount(ctx, opts...)
+}
+
+func (b *Collection) BulkWrite(ctx context.Context, models []mongo.WriteModel, opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, error) {
+ return b.coll.BulkWrite(ctx, models, opts...)
+}
+
+// CreateIndex 创建数据索引.options 在index里面创建
+func (b *Collection) CreateIndex(ctx context.Context, models []mongo.IndexModel) ([]string, error) {
+ data, err := b.coll.Indexes().CreateMany(ctx, models)
+ if err != nil {
+ if strings.Contains(err.Error(), "IndexKeySpecsConflict") {
+ return data, nil
+ }
+ if strings.Contains(err.Error(), "IndexOptionsConflict") {
+ return data, nil
+ }
+ return data, err
+ }
+ return data, nil
+}
+
+// DropIndex 删除数据索引.options 在index里面创建
+// func (b *Collection) DropIndex(ctx context.Context, indexName string) error {
+// _, err := b.coll.Indexes().DropOne(ctx, indexName)
+// if err != nil {
+// return err
+// }
+// return nil
+// }
+
+// func (b *MongoTool) addCreatedAt(val interface{}) {
+// if _, ok := val.(bson.M); ok {
+// return
+// }
+// if _, ok := val.(map[string]interface{}); ok {
+// return
+// }
+// v := reflect.ValueOf(val).Elem().FieldByName("CreatedAt")
+// if v.IsValid() {
+// v.Set(reflect.ValueOf(time.Now()))
+// }
+
+// }
+// func (b *MongoTool) addUpdatedAt(val interface{}) {
+// if _, ok := val.(bson.M); ok {
+// return
+// }
+// if _, ok := val.(map[string]interface{}); ok {
+// return
+// }
+// v := reflect.ValueOf(val).Elem().FieldByName("UpdatedAt")
+// if v.IsValid() {
+// v.Set(reflect.ValueOf(time.Now()))
+// }
+// }
+
+func (b *Collection) setTimeForSlice(docs interface{}) []interface{} {
+ arr := reflect.ValueOf(docs).Elem()
+ result := make([]interface{}, 0)
+ for i := 0; i < arr.Len(); i++ {
+ ele := arr.Index(i).Elem()
+ now := time.Now()
+ if ma := ele.FieldByName("update_at"); ma.IsValid() {
+ ma.Set(reflect.ValueOf(now))
+ }
+ if ca := ele.FieldByName("create_at"); ca.IsValid() {
+ ca.Set(reflect.ValueOf(now))
+ }
+ result = append(result, ele.Interface())
+ }
+ return result
+}
+
+// func (b *Collection) validSlice(bind interface{}) error {
+
+// if reflect.TypeOf(bind).Kind() != reflect.Ptr {
+// return errors.New("interface must be a Ptr")
+// }
+// if reflect.TypeOf(bind).Elem().Kind() != reflect.Slice {
+// return errors.New("interface must be a slice")
+// }
+// return nil
+// }
+
+// func (b *Collection) setUpdatedAt(doc bson.M, now time.Time) {
+// set, exists := doc["$set"]
+// if !exists {
+// doc["$set"] = bson.M{"updatedAt": now}
+// return
+// }
+
+// if res, ok := set.(bson.M); ok {
+// res["updatedAt"] = now
+// }
+// }
+
+// func (b *Collection) setCreatedAt(doc bson.M, now time.Time) {
+// set, exists := doc["$set"]
+// if !exists {
+// doc["$set"] = bson.M{"createdAt": now}
+// return
+// }
+// if res, ok := set.(bson.M); ok {
+// res["createdAt"] = now
+// }
+// }
+
+// func (b *Collection) setDeaultTime(doc bson.M) {
+// now := time.Now()
+// set, exists := doc["$set"]
+// if exists {
+// if res, ok := set.(bson.M); ok {
+// delete(res, "createdAt")
+// res["updatedAt"] = now
+// if d, ok := doc["$setOnInsert"]; ok {
+// if ss, ok := d.(bson.M); ok {
+// if _, ok := ss["createdAt"]; !ok {
+// ss["createdAt"] = now
+// }
+// }
+// } else {
+// doc["$setOnInsert"] = bson.M{"createdAt": now}
+// }
+// return
+// }
+// if res, ok := set.(interface{}); ok {
+// struct2Map := utils.Struct2Map(res)
+// delete(struct2Map, "createdAt")
+// struct2Map["updatedAt"] = now
+// doc["$set"] = struct2Map
+// doc["$setOnInsert"] = bson.M{"createdAt": now}
+// return
+// }
+// }
+// }
+
+// ////////////////////////////////////////////
+func (b *Collection) GetStringIDInsertOne(ctx context.Context, doc interface{}) (string, error) {
+ result, err := b.coll.InsertOne(ctx, doc)
+ if err != nil {
+ return "", err
+ }
+ if id, ok := result.InsertedID.(primitive.ObjectID); ok {
+ return id.Hex(), nil
+ }
+ return "", err
+}
+
+// InsertOne 插入单条信息
+func (b *Collection) GetObjectIDInsertOne(ctx context.Context, document interface{}, opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) {
+ return b.coll.InsertOne(ctx, document, opts...)
+}
+
+// UpdateOneNew 或者修改或者插入一条数据
+func (b *Collection) UpdateOneNew(ctx context.Context, query bson.M, update bson.M) (*mongo.UpdateResult, error) {
+ return b.coll.UpdateOne(ctx, query, update, options.Update().SetUpsert(true))
+}
+
+func (b *Collection) Max(ctx context.Context, fieldName string, filter bson.M) (int64, error) {
+ //b.coll.Aggregate(ctx, )
+ type Temp struct {
+ Max int64 `bson:"max"`
+ }
+ var temp []Temp
+ query := []bson.M{
+ {"$group": bson.M{
+ "_id": nil,
+ "max": bson.M{"$max": "$" + fieldName},
+ },
+ },
+ }
+ if err := b.Aggregate(ctx, &temp, query); err != nil || len(temp) <= 0 {
+ return 0, err
+ }
+
+ return temp[0].Max, nil
+}
+
+// func (b *Collection) setUpdatedAt(doc bson.M, now time.Time) {
+// set, exists := doc["$set"]
+// if !exists {
+// doc["$set"] = bson.M{"updatedAt": now}
+// return
+// }
+
+// if res, ok := set.(bson.M); ok {
+// res["updatedAt"] = now
+// }
+// }
+
+func (b *Collection) FindOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
+ setReturn := false
+ for _, opt := range opts {
+ if opt != nil && opt.ReturnDocument != nil {
+ setReturn = true
+ break
+ }
+ }
+ if !setReturn {
+ after := options.After
+ opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
+ }
+ // b.coll.setUpdatedAt(update, time.Now())
+ result := b.coll.FindOneAndUpdate(ctx, query, update, opts...)
+ err := result.Decode(bind)
+ return err
+}
+
+// //
+// func (b *Collection) IfNotExistThenInsertAndUpdate(ctx context.Context, query bson.M, insert bson.M, update bson.M) error {
+// // func (b *MongoTool) FindOneAndUpdate(ctx context.Context, bind interface{}, query bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
+// err := b.FindOneAndUpdate(ctx, nil, query, update)
+// if err != nil {
+// _, err = b.InsertOne(ctx, insert)
+// }
+// return err
+// }
+
+func (b *Collection) WatchOpLog(ctx context.Context, pipeline []bson.M,
+ opt ...*options.ChangeStreamOptions) (*mongo.ChangeStream, error) {
+ changeStream, err := b.coll.Watch(ctx, pipeline, opt...)
+ if err != nil {
+ return nil, err
+ }
+ return changeStream, nil
+}
\ No newline at end of file
diff --git a/pub/mongos/codec.go b/pub/mongos/codec.go
new file mode 100644
index 0000000..9d9fb30
--- /dev/null
+++ b/pub/mongos/codec.go
@@ -0,0 +1,119 @@
+package mongos
+
+import (
+ "fmt"
+ "github.com/shopspring/decimal"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/bsoncodec"
+ "go.mongodb.org/mongo-driver/bson/bsonrw"
+ "go.mongodb.org/mongo-driver/bson/bsontype"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "reflect"
+ "time"
+)
+
+var Registry = func() *bsoncodec.Registry {
+ builder := bson.NewRegistryBuilder()
+ builder.RegisterDecoder(reflect.TypeOf(time.Time{}), &localTimeDecoder{})
+ builder.RegisterDecoder(reflect.TypeOf(decimal.Decimal{}), &decimalDecoder{})
+ builder.RegisterEncoder(reflect.TypeOf(decimal.Decimal{}), &decimalEncoder{})
+ builder.RegisterDefaultDecoder(reflect.Float32, &floatDecoder{})
+ builder.RegisterDefaultDecoder(reflect.Float64, &floatDecoder{})
+ return builder.Build()
+}()
+
+type floatDecoder struct {
+}
+
+func (dvd *floatDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
+ var f float64
+ var err error
+ switch vr.Type() {
+ case bsontype.Int32:
+ i32, err := vr.ReadInt32()
+ if err != nil {
+ return err
+ }
+ f = float64(i32)
+ case bsontype.Int64:
+ i64, err := vr.ReadInt64()
+ if err != nil {
+ return err
+ }
+ f = float64(i64)
+ case bsontype.Double:
+ f, err = vr.ReadDouble()
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("cannot decode %v into a float32 or float64 type", vr.Type())
+ }
+ val.SetFloat(f)
+ return nil
+}
+
+type localTimeDecoder struct{}
+
+func (*localTimeDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
+ dvd := bsoncodec.DefaultValueDecoders{}
+ err := dvd.TimeDecodeValue(dc, vr, val)
+ if err != nil {
+ return err
+ }
+ t := val.Interface().(time.Time)
+ val.Set(reflect.ValueOf(t.Local()))
+ return nil
+}
+
+type decimalDecoder struct{}
+
+func (*decimalDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
+ if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) {
+ return bsoncodec.ValueDecoderError{Name: "DecimalDecodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val}
+ }
+ var (
+ num bool
+ )
+ if vr.Type() == bson.TypeInt32 || vr.Type() == bson.TypeInt64 {
+ vr.ReadInt32()
+ vr.ReadInt64()
+ num = true
+ }
+ if num {
+ d := decimal.NewFromFloat(0.0)
+ val.Set(reflect.ValueOf(d))
+ return nil
+ }
+
+ mongodecimal, err := vr.ReadDecimal128()
+ if err != nil {
+ return err
+ }
+ d, err := decimal.NewFromString(mongodecimal.String())
+ if err != nil {
+ return err
+ }
+ val.Set(reflect.ValueOf(d))
+ return nil
+}
+
+//
+
+type decimalEncoder struct{}
+
+func (*decimalEncoder) EncodeValue(ctx bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
+ if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) {
+ return bsoncodec.ValueDecoderError{Name: "DecimalEncodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val}
+ }
+
+ if d, ok := val.Interface().(decimal.Decimal); ok {
+ mongodecimal, err := primitive.ParseDecimal128(d.StringFixed(2))
+ if err != nil {
+ return err
+ }
+ val = reflect.ValueOf(mongodecimal)
+ }
+ dve := bsoncodec.DefaultValueEncoders{}
+ return dve.Decimal128EncodeValue(ctx, vw, val)
+}
diff --git a/pub/mongos/generate_id.go b/pub/mongos/generate_id.go
new file mode 100644
index 0000000..19123f9
--- /dev/null
+++ b/pub/mongos/generate_id.go
@@ -0,0 +1,80 @@
+package mongos
+
+import (
+ "context"
+ "fmt"
+ "videoweb/pub/models/utils"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var Lid struct {
+ User *lid
+ MwAcc *lid
+ MwRole *lid
+ MwOpLog *lid
+ MwAccToken *lid
+ SystemMsg *lid
+ AdvitiseInfo *lid
+ AdvitiseBind *lid
+ AdvitiseLocation *lid
+ App *lid
+ VideoTag *lid
+ VideoCategory *lid
+ VideoInfo *lid
+ TransCategory *lid
+ Translation *lid
+ VideoCreator *lid
+ Comment *lid
+ HtmlText *lid
+}
+
+func StartLid() {
+
+ coll := Shop.Coll(utils.TN.GenerateId)
+ initIndex(coll)
+
+ Lid.User = createLid(coll, "user", 10)
+ Lid.MwAcc = createLid(coll, "MwAcc", 1)
+ Lid.MwRole = createLid(coll, "MwRole", 1)
+ Lid.MwOpLog = createLid(coll, "MwOpLog", 10)
+ Lid.MwAccToken = createLid(coll, "MwAccToken", 10)
+ Lid.SystemMsg = createLid(coll, "SystemMsg", 10)
+ Lid.AdvitiseInfo = createLid(coll, "AdvitiseInfo", 10)
+ Lid.AdvitiseBind = createLid(coll, "AdvitiseBind", 10)
+ Lid.AdvitiseLocation = createLid(coll, "AdvitiseLocation", 10)
+ Lid.App = createLid(coll, "App", 10)
+ Lid.VideoTag = createLid(coll, "VideoTag", 10)
+ Lid.VideoCategory = createLid(coll, "VideoCategory", 10)
+ Lid.VideoInfo = createLid(coll, "VideoInfo", 10)
+ Lid.TransCategory = createLid(coll, "TransCategory", 10)
+ Lid.Translation = createLid(coll, "Translation", 10)
+ Lid.VideoCreator = createLid(coll, "VideoCreator", 10)
+ Lid.Comment = createLid(coll, "Comment", 10)
+ Lid.HtmlText = createLid(coll, "HtmlText", 10)
+}
+
+// initIndex 初始化索引
+func initIndex(coll *Collection) {
+ indexes := []mongo.IndexModel{
+ {
+ Keys: bson.D{
+ {Key: utils.FN.GenerateId.BusinessId, Value: 1},
+ },
+ Options: options.Index().SetUnique(true),
+ },
+ }
+ if _, err := coll.CreateIndex(context.Background(), indexes); err != nil {
+ panic(fmt.Sprintf("(user:initIndex) Create Index user failed! err:%+v:", err))
+ }
+}
+
+func createLid(db *Collection, businessId string, len int) *lid {
+ lid, err := newLid(db, businessId, len)
+ if err != nil {
+ panic(err)
+ }
+ return lid
+}
diff --git a/pub/mongos/lid.go b/pub/mongos/lid.go
new file mode 100644
index 0000000..f2dd3da
--- /dev/null
+++ b/pub/mongos/lid.go
@@ -0,0 +1,102 @@
+package mongos
+
+import (
+ "context"
+ "time"
+ "videoweb/pub/models/utils"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+const notFindMsg = "mongo: no documents in result"
+
+type lid struct {
+ db *Collection // 数据库连接
+ businessId string // 业务id
+ ch chan int64 // id缓冲池
+ min, max int64 // id段最小值,最大值
+}
+
+// newLid 创建一个lid,db:数据库连接;businessId:业务id;len:缓冲池大小
+func newLid(db *Collection, businessId string, len int) (*lid, error) {
+ lid := lid{
+ db: db,
+ businessId: businessId,
+ ch: make(chan int64, len),
+ }
+ go lid.productId()
+ return &lid, nil
+}
+
+// Get 获取自增id
+func (l *lid) Get() int64 {
+ return <-l.ch
+}
+
+// productId 生产id
+func (l *lid) productId() {
+ l.reset()
+ for {
+ if l.min >= l.max {
+ l.reset()
+
+ }
+
+ l.min++
+ l.ch <- l.min
+ }
+}
+
+// reset 不断尝试从数据库获取,直到成功
+func (l *lid) reset() {
+ for {
+ err := l.getFromDB()
+ if err == nil {
+ return
+ }
+ time.Sleep(time.Second)
+ continue
+ }
+}
+
+func (l *lid) getFromDB() error {
+ var (
+ max_id int64
+ step int64
+ //idModel IdModel
+ idModel utils.GenerateId
+ )
+ businessIdfilter := bson.M{utils.FN.GenerateId.BusinessId: l.businessId}
+ txErr := Shop.Transaction(context.Background(), func(sctx mongo.SessionContext) (err error) {
+ err = l.db.FindOneEx(sctx, &idModel, businessIdfilter)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ idModel.MaxId = 10000
+ _, err = l.db.InsertOne(sctx,
+ bson.M{
+ utils.FN.GenerateId.BusinessId: l.businessId,
+ utils.FN.GenerateId.MaxId: 0,
+ utils.FN.GenerateId.Step: 10,
+ utils.FN.GenerateId.Desc: "id自增器",
+ utils.FN.GenerateId.CreateAt: time.Now(),
+ utils.FN.GenerateId.UpdateAt: time.Now()})
+ if err != nil {
+ return
+ }
+ } else {
+ return
+ }
+ }
+ max_id = idModel.MaxId
+ step = idModel.Step
+ _, err = l.db.UpdateOne(sctx, businessIdfilter, bson.M{"$set": bson.M{utils.FN.GenerateId.MaxId: max_id + step}})
+ if err != nil {
+ return
+ }
+ l.min = max_id
+ l.max = max_id + step
+ return
+ })
+ return txErr
+}
diff --git a/pub/mongos/manager.go b/pub/mongos/manager.go
new file mode 100644
index 0000000..ddec16e
--- /dev/null
+++ b/pub/mongos/manager.go
@@ -0,0 +1,168 @@
+package mongos
+
+import (
+ "context"
+ "errors"
+ "time"
+ "videoweb/pub/common/log"
+
+ "go.mongodb.org/mongo-driver/mongo/readconcern"
+ "go.mongodb.org/mongo-driver/mongo/readpref"
+ "go.mongodb.org/mongo-driver/mongo/writeconcern"
+ "go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
+
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// var (
+// connPools *mongo.Client
+// dataBase *mongo.Database
+// )
+
+var (
+ //业务库
+ Shop *Manager
+ //日志库
+ ShopLog *Manager
+)
+
+type Manager struct {
+ client *mongo.Client
+ //dataBase *mongo.Database
+ url string
+ db string
+}
+
+func NewManager(url string) *Manager {
+ return &Manager{url: url}
+}
+
+func (m *Manager) Database() *mongo.Database {
+ return m.client.Database(m.db)
+}
+
+func (m *Manager) Coll(name string) *Collection {
+ coll := m.Database().Collection(name)
+ return &Collection{name: name, coll: coll}
+}
+
+func (m *Manager) Init() error {
+ cs, err := connstring.Parse(m.url)
+ if err != nil {
+ log.Error("db connect parse err", log.E(err))
+ return err
+ }
+ if cs.Database == "" {
+ log.Error("db connect database MUST contains")
+ return errors.New("database MUST contains")
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ client, err := mongo.Connect(ctx, options.Client().ApplyURI(m.url))
+ if err != nil {
+ log.Error("[warn] connect mongo db err", log.E(err))
+ return err
+ }
+ if err = client.Ping(ctx, nil); err != nil {
+ log.Error("[warn] ping mongo db err", log.E(err))
+ return err
+ }
+
+ //connPools = client
+ m.client = client
+ m.db = cs.Database
+
+ log.Info("mongos:Init ok!", log.Any("dbName", m.db))
+ return nil
+}
+
+func (m *Manager) DisConnect() {
+ if m.client != nil {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := m.client.Disconnect(ctx); err != nil {
+ log.Error("db disconnect err", log.E(err))
+ }
+
+ m.client = nil
+ }
+}
+
+// Transaction 开启事务处理包裹处理,里面处理的全是利用的事务的ctx 切记里面的context必须要使用同一个,因为里面有重试机制,循环调用该函数
+func (m *Manager) Transaction(ctx context.Context, fn func(sctx mongo.SessionContext) error) error {
+ opts := options.Session().SetDefaultReadPreference(readpref.Primary())
+ handler := func(sctx mongo.SessionContext) error {
+ lHandler := func(ctx mongo.SessionContext) (err error) {
+ tOpts := options.Transaction()
+ tOpts.SetReadConcern(readconcern.Snapshot())
+ tOpts.SetWriteConcern(writeconcern.New(writeconcern.WMajority()))
+ err = sctx.StartTransaction(tOpts)
+ if err != nil {
+ return
+ }
+ defer func() {
+ if rErr := recover(); rErr != nil {
+ log.Info("caught panic during transaction, aborting...", log.Any("", rErr))
+ aErr := sctx.AbortTransaction(sctx)
+ if aErr != nil {
+ log.Info("中止事务错误", log.E(aErr))
+ }
+ }
+ }()
+ err = fn(sctx)
+ if err != nil {
+ aErr := sctx.AbortTransaction(sctx)
+ if aErr != nil {
+ log.Info("中止事务错误", log.E(aErr))
+ }
+ log.Info("事务内错误", log.E(err))
+ return
+ }
+ return commitWithRetry(sctx)
+ }
+ return runTransactionWithRetry(sctx, lHandler)
+ }
+ return m.client.UseSessionWithOptions(ctx, opts, handler)
+}
+
+func runTransactionWithRetry(sctx mongo.SessionContext, txnFn func(mongo.SessionContext) error) error {
+ var err error
+ for i := int64(0); i < 100; i++ {
+ err = txnFn(sctx) // Performs transaction.
+ if err == nil {
+ return nil
+ }
+ // If transient error, retry the whole transaction
+ if cmdErr, ok := err.(mongo.CommandError); ok && cmdErr.HasErrorLabel("TransientTransactionError") {
+ continue
+ }
+ return err
+ }
+ return err
+}
+
+// commitWithRetry is an example function demonstrating transaction retry logic.
+func commitWithRetry(sctx mongo.SessionContext) error {
+ for {
+ err := sctx.CommitTransaction(sctx)
+ switch e := err.(type) {
+ case nil:
+ return nil
+ case mongo.CommandError:
+ // Can retry commit
+ if e.HasErrorLabel("UnknownTransactionCommitResult") {
+ continue
+ }
+ return e
+ default:
+ return e
+ }
+ }
+}
+
+// func Transaction(ctx context.Context, fn func(sctx mongo.SessionContext) error) error {
+// return connMgr.Transaction(ctx, fn)
+// }
diff --git a/pub/pubdef/pubdef.go b/pub/pubdef/pubdef.go
new file mode 100644
index 0000000..b113e1b
--- /dev/null
+++ b/pub/pubdef/pubdef.go
@@ -0,0 +1,25 @@
+package pubdef
+
+type Page struct {
+ PageNum int64 `form:"pageNum" json:"pageNum,omitempty"`
+ PageSize int64 `form:"pageSize" json:"pageSize,omitempty"`
+}
+
+const (
+ DateISO = "2006-01-02"
+ TimeISO = "2006-01-02 15:04:05"
+ AppId = 1038
+)
+
+// TrackerAppId 数据上报 Web SDK 的业务 appId。
+func TrackerAppId() string {
+ return "JHA-1043"
+}
+
+const PageSize int64 = 40
+
+type SeoValue struct {
+ Title string `json:"title" bson:"webTitle"` // 网站SEO标题
+ Keyworld []string `json:"keyworld" bson:"webKeyworld"` // 网站SEO关键字
+ Description string `json:"description" bson:"webDescription"` // 网站SEO描述
+}
diff --git a/pub/pubdef/user.go b/pub/pubdef/user.go
new file mode 100644
index 0000000..8e6b910
--- /dev/null
+++ b/pub/pubdef/user.go
@@ -0,0 +1,24 @@
+package pubdef
+
+type UserStatus int32
+
+const (
+ UserStatusNormal UserStatus = 0 // 正常
+ UserStatusDisable UserStatus = 1 // 封禁
+ UserStatusSlient UserStatus = 2 // 禁言
+)
+
+var UserStatusMap = map[UserStatus]string{
+ UserStatusNormal: "正常",
+ UserStatusDisable: "封禁",
+ UserStatusSlient: "禁止登陆",
+}
+
+type LoginType int32
+
+const (
+ LoginTypeDevice LoginType = 0 + iota // 设备登陆
+ LoginTypeMobile // 手机登陆
+ LoginTypeQrCode // 二维码登陆
+ LoginTypeAccount // 账号登陆
+)
\ No newline at end of file
diff --git a/pub/redis/List.go b/pub/redis/List.go
new file mode 100644
index 0000000..db85e71
--- /dev/null
+++ b/pub/redis/List.go
@@ -0,0 +1,95 @@
+package redis
+
+import (
+ "github.com/go-redis/redis"
+ "time"
+)
+
+// List represents a redis list.
+type List struct {
+ key string
+ client *redis.Client
+}
+
+// NewList creates a new redis list instance.
+func NewList(client *redis.Client, key string) *List {
+ return &List{
+ key: key,
+ client: client,
+ }
+}
+
+// Key returns the key of the list.
+func (l *List) Key() string {
+ return l.key
+}
+
+// Expire sets the expiration time of the list.
+func (l *List) Expire(expiration time.Duration) (bool, error) {
+ return l.client.Expire(l.key, expiration).Result()
+}
+
+// Delete deletes the list.
+func (l *List) Delete() (int64, error) {
+ return l.client.Del(l.key).Result()
+}
+
+// Len returns the length of the list.
+func (l *List) Len() (int64, error) {
+ return l.client.LLen(l.key).Result()
+}
+
+// Push adds a new element to the tail of the list.
+func (l *List) Push(value interface{}) error {
+ return l.client.RPush(l.key, value).Err()
+}
+
+// Pop removes and returns the first element of the list.
+func (l *List) Pop() (string, error) {
+ return l.client.LPop(l.key).Result()
+}
+
+// PushFront adds a new element to the head of the list.
+func (l *List) PushFront(value interface{}) error {
+ return l.client.LPush(l.key, value).Err()
+}
+
+// PopBack removes and returns the last element of the list.
+func (l *List) PopBack() (string, error) {
+ return l.client.RPop(l.key).Result()
+}
+
+// Index returns the element at the specified index.
+func (l *List) Index(index int64) (string, error) {
+ return l.client.LIndex(l.key, index).Result()
+}
+
+// Range returns a range of elements from the list.
+func (l *List) Range(start, stop int64) ([]string, error) {
+ return l.client.LRange(l.key, start, stop).Result()
+}
+
+// Trim removes elements from the list.
+func (l *List) Trim(start, stop int64) error {
+ return l.client.LTrim(l.key, start, stop).Err()
+}
+
+// Set sets the element at the specified index.
+func (l *List) Set(index int64, value interface{}) error {
+ return l.client.LSet(l.key, index, value).Err()
+}
+
+// InsertBefore inserts a new element before the specified element.
+func (l *List) InsertBefore(pivot, value interface{}) error {
+ return l.client.LInsertBefore(l.key, pivot, value).Err()
+}
+
+// InsertAfter inserts a new element after the specified element.
+func (l *List) InsertAfter(pivot, value interface{}) error {
+ return l.client.LInsertAfter(l.key, pivot, value).Err()
+}
+
+// Remove removes all occurrences of the specified element.
+func (l *List) Remove(count int64, value interface{}) (int64, error) {
+ return l.client.LRem(l.key, count, value).Result()
+}
diff --git a/pub/redis/channeldef.go b/pub/redis/channeldef.go
new file mode 100644
index 0000000..f76ed5d
--- /dev/null
+++ b/pub/redis/channeldef.go
@@ -0,0 +1,75 @@
+package redis
+
+import "time"
+
+// RCType redis channel type
+type RCType string
+
+const (
+ // config
+ RCT_cfg_all RCType = "config.*"
+ // tabkind 配置改变
+ RCT_cfg_tabkind_change RCType = "config.tabkindchange"
+ // mediatag 配置改变
+ RCT_cfg_mediatag_change RCType = "config.mediatagchange"
+ // mediacat 配置改变
+ RCT_cfg_mediacat_change RCType = "config.mediacatchange"
+ // mediatopic 配置改变
+ RCT_cfg_mediatopic_change RCType = "config.mediatopicchange"
+ // mediacolumn 配置改变
+ RCT_cfg_mediacolumn_change RCType = "config.mediacolumnchange"
+ // syscfg 系统配置改变
+ RCT_cfg_syscfg_change RCType = "config.syscfgchange"
+
+ // 广告
+ RCT_ad_all RCType = "ad.*"
+ // 广告配置改变
+ RCT_ad_cfgchange RCType = "ad.cfgchange"
+
+ // 系统通知
+ RCT_blt_all RCType = "blt.*"
+ // 系统通知
+ RCT_blt_cfgchange RCType = "blt.cfgchange"
+
+ //media
+ RCT_media_all RCType = "media.*"
+ //media 信息改变
+ RCT_media_change RCType = "media.change"
+ //media 信息添加
+ RCT_media_add RCType = "media.add"
+ //media 信息删除
+ RCT_media_del RCType = "media.del"
+ //media 相关排行榜改变
+ RCT_media_topchange RCType = "media.topchange"
+
+ //user
+ RCT_user_all RCType = "user.*"
+ //user 信息改变
+ RCT_user_change RCType = "user.change"
+ //user 信息添加
+ RCT_user_add RCType = "user.add"
+ //user 信息删除
+ RCT_user_del RCType = "user.del"
+ //user 相关排行榜改变
+ RCT_user_topchange RCType = "user.topchange"
+
+ //视频推荐
+ RCT_recommend_all RCType = "recommend.*"
+ //博主推荐
+ RCT_recommend_blogger_all RCType = "recommend.bloggerALL"
+ // 视频每日推荐
+ RCT_recommend_media_all RCType = "recommend.mediaAll"
+
+ //tg消息
+ RCT_tgmsg_all RCType = "tgmsg.*"
+ RCT_tgmsg_send_msg RCType = "tgmsg.sendmsg"
+
+ //vipcard
+ RCT_vipcard_all RCType = "vipcard.*"
+ RCT_vipcard_change RCType = "vipcard.change"
+
+ NameSource = "name:source" //姓名池
+ NameSourceExpire = 2 * 24 * time.Hour
+ CommunicateServerChannel = "comm:lld#server"
+ CommunicateClientChannel = "comm:lld#client"
+)
diff --git a/pub/redis/hash.go b/pub/redis/hash.go
new file mode 100644
index 0000000..c0f5d0c
--- /dev/null
+++ b/pub/redis/hash.go
@@ -0,0 +1,112 @@
+package redis
+
+import (
+ "time"
+
+ "github.com/go-redis/redis"
+)
+
+//key: {name:value, name:value, name:value}
+
+type Hash struct {
+ key string
+ client *redis.Client
+}
+
+func NewHash(client *redis.Client, key string) *Hash {
+ return &Hash{
+ key: key,
+ client: client,
+ }
+}
+
+func (s *Hash) Key() string {
+ return s.key
+}
+
+// Expire 设置集合过期时间
+func (s *Hash) Expire(expiration time.Duration) (bool, error) {
+ return s.client.Expire(s.key, expiration).Result()
+}
+
+// Delete 删除一个或多个 Name
+func (s *Hash) Delete(names ...string) (int64, error) {
+ return s.client.HDel(s.key, names...).Result()
+}
+
+// Exists 检测Name是否在哈希表中
+func (s *Hash) Exists(name string) (bool, error) {
+ return s.client.HExists(s.key, name).Result()
+}
+
+// NameValues 获取所有哈希表数据
+func (s *Hash) NameValues() (map[string]string, error) {
+ return s.client.HGetAll(s.key).Result()
+}
+
+// Names 获取所有哈希表的 Name
+func (s *Hash) Names() ([]string, error) {
+ return s.client.HKeys(s.key).Result()
+}
+
+// Values 获取所有哈希表的 Total
+func (s *Hash) Values() ([]string, error) {
+ return s.client.HVals(s.key).Result()
+}
+
+// SetNX 当name不存在时才向哈希表中设置一对Name:Total
+func (s *Hash) SetNX(name string, value interface{}) (bool, error) {
+ return s.client.HSetNX(s.key, name, value).Result()
+}
+
+// Set 向哈希表中设置一对Name:Total
+func (s *Hash) Set(name string, value interface{}) (bool, error) {
+ return s.client.HSet(s.key, name, value).Result()
+}
+
+// Get 获取哈希表中Name对应的Value
+func (s *Hash) Get(name string) (string, error) {
+ return s.client.HGet(s.key, name).Result()
+}
+
+// GetResult 获取哈希表中Name对应的Value
+func (s *Hash) GetResult(name string) *redis.StringCmd {
+ return s.client.HGet(s.key, name)
+}
+
+// IncInt 返回累加之后的新值
+func (s *Hash) IncInt(name string, value int64) (int64, error) {
+ return s.client.HIncrBy(s.key, name, value).Result()
+}
+
+// IncFloat 返回累加之后的新值
+func (s *Hash) IncFloat(name string, value float64) (float64, error) {
+ return s.client.HIncrByFloat(s.key, name, value).Result()
+}
+
+// Len 返回哈希表的大小
+func (s *Hash) Len() (int64, error) {
+ return s.client.HLen(s.key).Result()
+}
+
+// Scan 扫描哈希表数据
+func (s *Hash) Scan(match string, piece int64, callback func(name, value string)) error {
+ proc := func(l []string) {
+ for i := 0; i+1 < len(l); i += 2 {
+ callback(l[i], l[i+1])
+ }
+ }
+ lst, cur, err := s.client.HScan(s.key, 0, match, piece).Result()
+ if err != nil {
+ return err
+ }
+ proc(lst)
+ for cur > 0 {
+ lst, cur, err = s.client.HScan(s.key, cur, match, piece).Result()
+ if err != nil {
+ return err
+ }
+ proc(lst)
+ }
+ return nil
+}
diff --git a/pub/redis/manager.go b/pub/redis/manager.go
new file mode 100644
index 0000000..79b1d3e
--- /dev/null
+++ b/pub/redis/manager.go
@@ -0,0 +1,177 @@
+package redis
+
+import (
+ "fmt"
+ "regexp"
+ "sync"
+ "videoweb/pub/common/log"
+ "videoweb/pub/common/srvdef"
+
+ "github.com/go-redis/redis"
+)
+
+type SubscribeCallback func(channel RCType, message string)
+
+var (
+ redisCli *redis.Client
+ opts Options
+
+ //订阅参数
+ pubsub *redis.PubSub
+ subCBMap map[RCType]SubscribeCallback
+ cbMapLock sync.RWMutex
+)
+
+func Client() *redis.Client {
+ return redisCli
+}
+
+func Disconnect() {
+ if redisCli != nil {
+ if pubsub != nil {
+ pubsub.Close()
+ }
+ redisCli.Close()
+ redisCli = nil
+ }
+}
+
+func Init(opts Options) error {
+ c := redis.NewClient(
+ &redis.Options{
+ Addr: opts.Addr,
+ Password: opts.Psw,
+ DB: opts.Db,
+ },
+ )
+
+ if err := c.Ping().Err(); err != nil {
+ log.Error(fmt.Sprintf("[error] ping redis err %+v\n", err))
+ return err
+ }
+ redisCli = c
+
+ pubsub = nil
+ if opts.Subscribe {
+ subCBMap = make(map[RCType]SubscribeCallback)
+ pubsub = redisCli.Subscribe()
+ //接受发布消息
+ go ReceiveMessage()
+ }
+
+ log.Info("redis:Init Ok!")
+ return nil
+}
+
+// OnMessage 订阅消息回调
+func OnMessage(channel RCType, message string) {
+ cbMapLock.RLock()
+ defer cbMapLock.RUnlock()
+ flag := false
+ for k, v := range subCBMap {
+ reg := regexp.MustCompile(string(k))
+ //log.Info("test", log.Any("k", k), log.Any("channelchannel", channel))
+ if b := reg.MatchString(string(channel)); b {
+ v(channel, message)
+ flag = true
+ }
+ }
+ if !flag {
+ log.Warn("(redis:OnMessage) callback doesn't exist!", log.Any("channel", channel))
+ }
+}
+
+// ReceiveMessage 接受发布消息
+func ReceiveMessage() {
+ for {
+ msg, err := pubsub.Receive()
+ if err != nil {
+ fmt.Println("test1", err)
+ return
+ }
+
+ switch msg := msg.(type) {
+ case *redis.Subscription:
+ // Ignore.
+ log.Info("(redis:ReceiveMessage) subscribe success!", log.Any("channel", msg.Channel))
+ case *redis.Pong:
+ // Ignore.
+ fmt.Println("Pong")
+ case *redis.Message:
+ channel := msg.Channel
+ message := msg.Payload
+ OnMessage(RCType(channel), message)
+ default:
+ log.Error("(redis:ReceiveMessage) msg!", log.Any("msg", msg))
+ return
+ }
+ }
+}
+
+// Subscribe 订阅一个消息
+func Subscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:Subscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.Subscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:Subscribe) Subscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ subCBMap[channel] = cb
+ return nil
+}
+
+// UnSubscribe 取消订阅一个消息
+func UnSubscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:UnSubscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.Unsubscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:UnSubscribe) UnSubscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ delete(subCBMap, channel)
+ return nil
+}
+
+// PSubscribe 模式匹配消息
+func PSubscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:PSubscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.PSubscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:PSubscribe) PSubscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ subCBMap[channel] = cb
+ return nil
+}
+
+// PUnsubscribe 模式取消匹配消息
+func PUnsubscribe(channel RCType, cb SubscribeCallback) *srvdef.SrvErr {
+ if pubsub == nil {
+ log.Warn("(redis:PUnsubscribe) subscribe is not enabled!")
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ err := pubsub.PUnsubscribe(string(channel))
+ if err != nil {
+ log.Warn("(redis:PUnsubscribe) PUnsubscribe failed!", log.Any("err", err))
+ return srvdef.New(srvdef.ErrRdsOperError, "", "")
+ }
+ cbMapLock.Lock()
+ defer cbMapLock.Unlock()
+ delete(subCBMap, channel)
+ return nil
+}
diff --git a/pub/redis/options.go b/pub/redis/options.go
new file mode 100644
index 0000000..fed9708
--- /dev/null
+++ b/pub/redis/options.go
@@ -0,0 +1,9 @@
+package redis
+
+// Redis 配置项
+type Options struct {
+ Addr string `json:"addr"` // 地址
+ Psw string `json:"psw"` // 密码
+ Db int `json:"db"` // 库
+ Subscribe bool `json:"subscribe"` // 是否有订阅功能
+}
diff --git a/pub/redis/redis.go b/pub/redis/redis.go
new file mode 100644
index 0000000..d504209
--- /dev/null
+++ b/pub/redis/redis.go
@@ -0,0 +1,275 @@
+package redis
+
+import (
+ "errors"
+ "fmt"
+ "time"
+ sErr "videoweb/pub/common/errors"
+ "videoweb/pub/common/log"
+
+ goredis "github.com/go-redis/redis"
+)
+
+type ZMember = goredis.Z
+
+// Set 设置键值对
+func Set(key string, value interface{}, expireTime time.Duration) (err error) {
+ if err = redisCli.Set(key, value, expireTime).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// Incr 增加1
+func Incr(key string, expireTime time.Duration) (val int64, err error) {
+ val, err = redisCli.Incr(key).Result()
+ if err != nil {
+ return
+ }
+ duration := redisCli.TTL(key).Val()
+ //log.Info("Incr Test", log.Any("duration", duration))
+ if duration.Seconds() == -1 {
+ redisCli.Expire(key, expireTime)
+ }
+ return
+}
+
+func SAdd(key string, members ...interface{}) (err error) {
+ _, err = redisCli.SAdd(key, members...).Result()
+ return
+}
+
+// 无序集合操作 随机删除集合中某一元素
+func SPop(key string, members ...interface{}) (string, error) {
+ val, err := redisCli.SPop(key).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("SPop failed %+v:", err))
+ }
+ return val, err
+}
+
+func SRem(key string, members ...interface{}) (err error) {
+ if err = redisCli.SRem(key, members...).Err(); err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// ScanSlice 返回集合成员
+func SScanSlice(key string, container interface{}) (err error) {
+ err = redisCli.SMembers(key).ScanSlice(container)
+ return err
+}
+
+// SMembers
+func SMembers(key string) (list []string, err error) {
+ result := redisCli.SMembers(key)
+ if result.Err() != nil {
+ err = sErr.MakeWithErr(sErr.RedisGet, result.Err())
+ return
+ }
+ list = result.Val()
+ return
+}
+
+// RPush
+func RPush(key string, value string) error {
+ if key == "" {
+ return errors.New("key should not empty")
+ }
+ if err := redisCli.RPush(key, value).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisRPush, err)
+ }
+ return nil
+}
+
+// RPush
+func RPushValues(key string, value ...interface{}) error {
+ if key == "" {
+ return errors.New("key should not empty")
+ }
+ if err := redisCli.RPush(key, value...).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisRPush, err)
+ }
+ return nil
+}
+
+// LPop
+func LPop(key string) (*string, error) {
+ if key == "" {
+ return nil, errors.New("key should not empty")
+ }
+
+ cmd := redisCli.LPop(key)
+ if cmd.Err() != nil {
+ return nil, sErr.MakeWithErr(sErr.RedisLPop, cmd.Err())
+ }
+
+ data, err := cmd.Result()
+ if err != nil {
+ if err == goredis.Nil {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+// Get 根据键 获取值
+func Get(key string) (*string, error) {
+ if key == "" {
+ return nil, errors.New("key should not empty")
+ }
+ data, err := redisCli.Get(key).Result()
+ if err != nil {
+ if err == goredis.Nil {
+ return nil, nil
+ }
+ return nil, err
+ } else {
+ return &data, nil
+ }
+}
+
+// Get 根据键 获取值
+func GetBytes(key string) (bytes []byte, err error) {
+ bytes, err = redisCli.Get(key).Bytes()
+ if err != nil {
+ return nil, sErr.MakeWithErr(sErr.RedisGet, err)
+ }
+ return
+}
+
+// Del 删除
+func Del(keys ...string) (err error) {
+ if err = redisCli.Del(keys...).Err(); err != nil {
+ return sErr.MakeWithErr(sErr.RedisDel, err)
+ }
+ return
+}
+
+// Exists keys exists?
+func Exists(keys ...string) (has bool, err error) {
+ intcmd := redisCli.Exists(keys...)
+ if err = intcmd.Err(); err != nil {
+ return
+ }
+ has = intcmd.Val() > 0
+ return
+}
+
+func Keys(pattern string) (keys []string, err error) {
+ list := redisCli.Keys(pattern)
+ keys = list.Val()
+ err = list.Err()
+ return
+}
+
+// Setnx 设置键值对
+func Setnx(key string, value interface{}, expirTime time.Duration) (err error) {
+ if err = redisCli.SetNX(key, value, expirTime).Err(); err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// Setnx 设置键值对 正确的封装
+func Setnx_NewOK(key string, value interface{}, expirTime time.Duration) (v bool, err error) {
+ cmd := redisCli.SetNX(key, value, expirTime)
+ return cmd.Val(), cmd.Err()
+}
+
+func Expire(key string, expirTime time.Duration) (err error) {
+ if err = redisCli.Expire(key, expirTime).Err(); err != nil {
+ err = sErr.MakeWithErr(sErr.RedisSet, err)
+ }
+ return
+}
+
+// Redis `SRANDMEMBER key count` command.
+func SRandMemberN(key string, count int64) (list []string, err error) {
+ result := redisCli.SRandMemberN(key, count)
+ if result.Err() != nil {
+ return
+ }
+ list = result.Val()
+ return
+}
+
+// 有序集合操作 添加多个有序集合
+func ZAdd(key string, z ...ZMember) (int64, error) {
+ count, err := redisCli.ZAdd(key, z...).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZAdd failed %+v:", err))
+ }
+ return count, err
+}
+
+// 有序集合 数量
+func ZCard(key string) (int64, error) {
+ count, err := redisCli.ZCard(key).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZCard failed %+v:", err))
+ }
+ return count, err
+}
+
+// 返回区间成员
+func ZRange(key string, start, end int64) ([]string, error) {
+ data, err := redisCli.ZRange(key, start, end).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZRange failed %+v:", err))
+ }
+ return data, err
+}
+
+// 按照元素排序 删除指定排名的元素
+func ZRemRangeByRank(key string, start, end int64) (int64, error) {
+ count, err := redisCli.ZRemRangeByRank(key, start, end).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZRemRangeByRank failed %+v:", err))
+ }
+ return count, err
+}
+
+// 按照元素排序 删除按指定的分数区间的的元素
+func ZRemRangeByScore(key, start, end string) (int64, error) {
+ count, err := redisCli.ZRemRangeByScore(key, start, end).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("ZRemRangeByScore failed %+v:", err))
+ }
+ return count, err
+}
+
+// Publish 发布消息
+func Publish(channel RCType, message string) error {
+ if err := redisCli.Publish(string(channel), message).Err(); err != nil {
+ log.Error(fmt.Sprintf("Publish failed %+v:", err))
+ return err
+ }
+ return nil
+}
+
+func SendMsgToTg(message string) error {
+ return Publish(RCT_tgmsg_send_msg, message)
+}
+
+// SCard 发布消息
+func SCard(key string) (int64, error) {
+ count, err := redisCli.SCard(key).Result()
+ if err != nil {
+ log.Error(fmt.Sprintf("SCard failed %+v:", err))
+ }
+
+ return count, err
+}
+
+// 获取key的剩余生存时间
+func TTL(key string) time.Duration {
+ durCmd := redisCli.TTL(key)
+ if durCmd != nil {
+ return durCmd.Val()
+ }
+ return 0
+}
diff --git a/pub/redis/zset.go b/pub/redis/zset.go
new file mode 100644
index 0000000..c3d1e7c
--- /dev/null
+++ b/pub/redis/zset.go
@@ -0,0 +1,136 @@
+package redis
+
+import (
+ "github.com/go-redis/redis"
+ "strconv"
+ "time"
+)
+
+type Z = redis.Z
+
+type ZSet struct {
+ key string
+ client *redis.Client
+}
+
+func NewZSet(client *redis.Client, key string) *ZSet {
+ return &ZSet{
+ key: key,
+ client: client,
+ }
+}
+
+func (s *ZSet) Key() string {
+ return s.key
+}
+
+// Expire 设置集合过期时间
+func (s *ZSet) Expire(expiration time.Duration) (bool, error) {
+ return s.client.Expire(s.key, expiration).Result()
+}
+
+// Add 添加一个或多个成员到有序集合
+func (s *ZSet) Add(members ...Z) (int64, error) {
+ return s.client.ZAdd(s.key, members...).Result()
+}
+
+// Count 返回有序集合在指定分数范围内的成员数量
+func (s *ZSet) Count(min, max float64) (int64, error) {
+ return s.client.ZCount(s.key, strconv.FormatFloat(min, 'f', -1, 64), strconv.FormatFloat(max, 'f', -1, 64)).Result()
+}
+
+// Score 获取成员的分数
+func (s *ZSet) Score(member string) (float64, error) {
+ return s.client.ZScore(s.key, member).Result()
+}
+
+// Rank 获取成员的排名(从小到大,0开始)
+func (s *ZSet) Rank(member string) (int64, error) {
+ return s.client.ZRank(s.key, member).Result()
+}
+
+// RevRank 获取成员的排名(从大到小,0开始)
+func (s *ZSet) RevRank(member string) (int64, error) {
+ return s.client.ZRevRank(s.key, member).Result()
+}
+
+// Range 返回指定排名范围的成员
+func (s *ZSet) Range(start, stop int64) ([]string, error) {
+ return s.client.ZRange(s.key, start, stop).Result()
+}
+
+// RangeWithScores 返回指定排名范围的成员和分数
+func (s *ZSet) RangeWithScores(start, stop int64) ([]Z, error) {
+ return s.client.ZRangeWithScores(s.key, start, stop).Result()
+}
+
+// RangeByScore 返回指定分数范围的成员
+func (s *ZSet) RangeByScore(min, max float64) ([]string, error) {
+ opt := redis.ZRangeBy{
+ Min: strconv.FormatFloat(min, 'f', -1, 64),
+ Max: strconv.FormatFloat(max, 'f', -1, 64),
+ }
+ return s.client.ZRangeByScore(s.key, opt).Result()
+}
+
+// RangeByScoreWithScores 返回指定分数范围的成员和分数
+func (s *ZSet) RangeByScoreWithScores(min, max float64) ([]Z, error) {
+ opt := redis.ZRangeBy{
+ Min: strconv.FormatFloat(min, 'f', -1, 64),
+ Max: strconv.FormatFloat(max, 'f', -1, 64),
+ }
+ return s.client.ZRangeByScoreWithScores(s.key, opt).Result()
+}
+
+// Remove 删除一个或多个成员
+func (s *ZSet) Remove(members ...string) (int64, error) {
+ return s.client.ZRem(s.key, members).Result()
+}
+
+// RemoveRangeByRank 删除指定排名范围的成员
+func (s *ZSet) RemoveRangeByRank(start, stop int64) (int64, error) {
+ return s.client.ZRemRangeByRank(s.key, start, stop).Result()
+}
+
+// RemoveRangeByScore 删除指定分数范围的成员
+func (s *ZSet) RemoveRangeByScore(min, max float64) (int64, error) {
+ return s.client.ZRemRangeByScore(s.key, strconv.FormatFloat(min, 'f', -1, 64), strconv.FormatFloat(max, 'f', -1, 64)).Result()
+}
+
+// Len 返回有序集合的成员数
+func (s *ZSet) Len() (int64, error) {
+ return s.client.ZCard(s.key).Result()
+}
+
+// IncScore 为成员的分数加上增量
+func (s *ZSet) IncScore(member string, increment float64) (float64, error) {
+ return s.client.ZIncrBy(s.key, increment, member).Result()
+}
+
+// Scan 扫描有序集合
+func (s *ZSet) Scan(match string, count int64, callback func(member string, score float64) error) error {
+ cursor := uint64(0)
+ for {
+ keys, newCursor, err := s.client.ZScan(s.key, cursor, match, count).Result()
+ if err != nil {
+ return err
+ }
+
+ for i := 0; i < len(keys); i += 2 {
+ score, err := strconv.ParseFloat(keys[i+1], 64)
+ if err != nil {
+ return err
+ }
+ err = callback(keys[i], score)
+ if err != nil {
+ return err
+ }
+ }
+
+ if newCursor == 0 {
+ break
+ }
+ cursor = newCursor
+ }
+ return nil
+}
diff --git a/pub/storage/proxy/object.go b/pub/storage/proxy/object.go
new file mode 100644
index 0000000..d2068f7
--- /dev/null
+++ b/pub/storage/proxy/object.go
@@ -0,0 +1,384 @@
+package proxy
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+ "videoweb/pub/common/http"
+ "videoweb/pub/common/log"
+)
+
+const (
+ apiDomain = "http://resourcets.bbaazz.me/file/upload"
+)
+
+var (
+ header = map[string]string{"Content-Type": "application/json"}
+)
+
+const (
+ Success = 200
+)
+
+func UploadImg(fileName string, data []byte, domain string) (pathStr string, err error) {
+ r := objreq{
+ Name: fileName,
+ Data: base64.StdEncoding.EncodeToString(data),
+ }
+ jsonbytes, err := json.Marshal(r)
+ if err != nil {
+ return
+ }
+ url := ImageUploadAddressRepair(domain, BucketImageAddressV2)
+
+ resp, err := http.Post(url, header, bytes.NewReader(jsonbytes))
+ if err != nil {
+ return
+ }
+
+ var or objresp
+ if err = json.Unmarshal(resp.Data, &or); err != nil {
+ return
+ }
+ if or.Code != Success {
+ err = fmt.Errorf("上传失败:%s", or.Msg)
+ return
+ }
+ pathStr = or.Object.Name
+ return
+}
+
+func UploadBase64Img(fileName string, data string, domain string) (pathStr string, err error) {
+ r := objreq{
+ Name: fileName,
+ Data: data,
+ }
+ jsonbytes, err := json.Marshal(r)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = apiDomain
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/upload") {
+ url = domain + "/file/upload"
+ }
+ resp, err := http.Post(url, header, bytes.NewReader(jsonbytes))
+ if err != nil {
+ return
+ }
+
+ var or objresp
+ if err = json.Unmarshal(resp.Data, &or); err != nil {
+ return
+ }
+ if or.Code != Success {
+ err = fmt.Errorf("上传失败:%s", or.Msg)
+ return
+ }
+ pathStr = or.Object.Name
+ if strings.Contains(domain, "resourceimg.bbaazz.me") &&
+ !strings.HasPrefix(pathStr, "v2/") {
+ pathStr = "v2/" + pathStr
+ }
+ return
+}
+
+func fileServer() (url string) {
+ return "http://resourcets.bbaazz.me"
+}
+
+func PostOk(baseInfo *ShardOk, domain string) (result *VideoOkResult, err error) {
+ result = &VideoOkResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ url := VideoOkAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(baseInfo)
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ fmt.Println(string(fr.Data))
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(result)
+ return nil, err
+ }
+ fmt.Println(result)
+ return
+}
+func PostOkV2(baseInfo *ShardOk, domain string) (result *VideoOkResult, err error) {
+ result = &VideoOkResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ url := VideoOkAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(paramJson)
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(result)
+ return nil, err
+ }
+ fmt.Println(result)
+ return
+}
+func QueryMedia(baseInfo *MediaQueryeReq, domain string) (result *VideoQueryResult, err error) {
+ result = &VideoQueryResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/query") {
+ url = domain + "/file/web/media/query"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+
+ return
+}
+func QueryMedias(baseInfo *MediaQueryesReq, domain string) (result *VideoQuerysResult, err error) {
+ result = &VideoQuerysResult{
+ Data: make([]MediaResp, 0),
+ }
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/task/querys") {
+ url = domain + "/media/web/task/querys"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+ return
+}
+func SyncMedia(baseInfo *MediaTaskSyncReq, domain string) (result *VideoQuerysResult, err error) {
+ result = &VideoQuerysResult{}
+ paramJson, err := json.Marshal(baseInfo)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/media/web/task/sync") {
+ url = domain + "/media/web/task/sync"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+
+ return
+}
+
+func UploadVideo(domain string, body *ShardBody) (result *Result, err error) {
+ if body == nil {
+ return nil, errors.New("no video to upload")
+ }
+ url := VideoBodyAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(url)
+
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Post(url, header, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println("resp-->", string(resp.Data))
+ result = new(Result)
+ if err = json.Unmarshal(resp.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func UploadVideoV2(domain string, body *ShardBodyV2) (result *Result, err error) {
+ if body == nil {
+ return nil, errors.New("no video to upload")
+ }
+ url := VideoBodyAddressRepair(domain, BucketVideoAddressV2)
+ fmt.Println(url)
+
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Post(url, header, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println("resp-->", string(resp.Data))
+ result = new(Result)
+ if err = json.Unmarshal(resp.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func PostSearch(param *MediaSearchReq, domain string) (result *MediaSearchResult, err error) {
+ result = &MediaSearchResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/search") {
+ url = domain + "/file/web/media/search"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ //log.Info("proxy:test", log.Any("url", url), log.Any("result", result))
+ return
+}
+
+func PostUpdate(param *MediaUpdateReq, domain string) (result *MediaUpdateResult, err error) {
+ result = &MediaUpdateResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/update") {
+ url = domain + "/file/web/media/update"
+ }
+ fr, perr := http.Post(url, header, bytes.NewReader(paramJson))
+ if perr != nil {
+ log.Error("PostUpdate Unmarshal", log.E(perr))
+ err = perr
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ log.Error("PostUpdate Unmarshal", log.E(err), log.Any("data", string(fr.Data)))
+ return nil, err
+ }
+ return
+}
+
+func PostDelete(param *MediaDelReq, domain string) (result *MediaDelResult, err error) {
+ result = &MediaDelResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/delete") {
+ url = domain + "/file/web/media/delete"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func PostUpdatePreTime(param *MediaUpdatePreTime, domain string) (result *MediaDelResult, err error) {
+ result = &MediaDelResult{}
+ paramJson, err := json.Marshal(param)
+ if err != nil {
+ return
+ }
+ if len(domain) == 0 {
+ domain = DomainAddressRepair(domain, BucketVideoAddressV2)
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/media/update/prefile") {
+ url = domain + "/file/web/media/update/prefile"
+ }
+ fr, err := http.Post(url, header, bytes.NewReader(paramJson))
+ if err != nil {
+ return
+ }
+ if err = json.Unmarshal(fr.Data, result); err != nil {
+ return nil, err
+ }
+ return
+}
+
+func UploadVideoV2New(domain string, body *ShardBodyV2) (result *Result, err error) {
+ if body == nil {
+ return nil, errors.New("no video to upload")
+ }
+ if len(domain) == 0 {
+ domain = fileServer()
+ }
+ url := domain
+ if !strings.Contains(domain, "/file/web/proxy/videoBodyV2") {
+ url = domain + "/file/web/proxy/videoBodyV2"
+ }
+ fmt.Println(url)
+
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := http.Post(url, header, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println("resp-->", string(resp.Data))
+ result = new(Result)
+ if err = json.Unmarshal(resp.Data, result); err != nil {
+ return nil, err
+ }
+ // if strings.Contains(domain, "resourcets.bbaazz.me") && !strings.HasPrefix(result.Data.VideoUri, "v2/") {
+ // result.Data.VideoUri = "v2/" + result.Data.VideoUri
+ // }
+ fmt.Println("result-->", result)
+ if result.Code != 200 {
+ err = fmt.Errorf("result code err")
+ }
+ return
+}
diff --git a/pub/storage/proxy/object_test.go b/pub/storage/proxy/object_test.go
new file mode 100644
index 0000000..943b369
--- /dev/null
+++ b/pub/storage/proxy/object_test.go
@@ -0,0 +1 @@
+package proxy
diff --git a/pub/storage/proxy/test.html b/pub/storage/proxy/test.html
new file mode 100644
index 0000000..e0c8bb8
--- /dev/null
+++ b/pub/storage/proxy/test.html
@@ -0,0 +1,3 @@
+
+ 51吃瓜国内最新地址 https://51cga33.com
51吃瓜海外永久地址 https://51cg1.com
播放异常? 请刷新 或 使用51吃瓜App
苹果App白屏 请等待1分钟/切换网络/重新下载
发邮件获取 最新网址 👇长按复制保存👇 51cgfun@pm.me
曲阜师范大学学生【何鑫】的私密自拍视频流出,尺度炸裂,令人瞠目!这位清纯外表的学妹私下化身小母狗,为完成神秘“主人”的任务大秀骚浪本性!视频中,她身着宽松裸体背带裤,半遮半掩的布料勾勒出白嫩胴体,饱满小胸和粉嫩美穴若隐若现。
何鑫裸体自拍玩穴 她站在宿舍镜前,挑逗地拉下背带裤,露出湿漉漉的私处,手指大胆深入玩穴,湿滑水声混杂娇喘,镜头特写捕捉到她揉搓嫩穴的每一个细节,令人血脉喷张!
何鑫足交伺候主人 何鑫还完成足交任务,赤裸坐在床边,用白嫩玉足轻抚主人粗大肉棒,脚趾灵活挑逗,油亮光泽的脚掌摩擦到主人爽到低吼!
关键词:#曲阜师范大学何鑫 #何鑫裸体自拍 #何鑫对镜玩穴自慰 #何鑫足交伺候主人 #何鑫不雅自拍视频 #曲阜师范大学 #何鑫 #自慰 #足交 #51吃瓜 #51吃瓜网 #黑料 #吃瓜
想看更多精彩完整版,下载51吃瓜APP一爽到底~
51吃瓜APP下载地址: 最新地址 https://url46.co/1222 安卓下载备用地址一: https://url46.co/1237 安卓下载备用地址二: https://url46.co/1238 iOS下载备用地址 https://url46.co/1239
+
\ No newline at end of file
diff --git a/pub/storage/proxy/types.go b/pub/storage/proxy/types.go
new file mode 100644
index 0000000..e789571
--- /dev/null
+++ b/pub/storage/proxy/types.go
@@ -0,0 +1,404 @@
+package proxy
+
+import (
+ "strings"
+ "time"
+ "videoweb/pub/models/videoinformationmod"
+ "videoweb/pub/mongos"
+
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type objreq struct {
+ Name string `json:"fileName"`
+ Data string `json:"fileData"`
+}
+
+type Obj struct {
+ Domain string `json:"domain"`
+ Name string `json:"fileName"`
+}
+
+type objresp struct {
+ Code int `json:"code"`
+ Object Obj `json:"data"`
+ Msg string `json:"msg"`
+}
+
+type ShardBody struct {
+ TaskId string `json:"taskId,omitempty"` // 文档md5
+ Pos int `json:"pos,omitempty"` // 位置
+ FileData string `json:"fileData,omitempty"` // 数据
+ Type string `json:"type,omitempty"`
+ TotalPos int `json:"totalPos,omitempty"` // 分片总数
+}
+
+type ShardBodyV2 struct {
+ TaskId string `json:"taskId,omitempty"` // 文档md5
+ Pos int `json:"pos,omitempty"` // 位置
+ FileData string `json:"fileData,omitempty"` // 数据
+ Type string `json:"type,omitempty"`
+ TotalPos int `json:"totalPos,omitempty"`
+ FileName string `json:"fileName,omitempty"`
+ Publish string `json:"publish,omitempty"`
+ AppId int `json:"appId,omitempty"`
+ MP4Folder string `json:"mp4Folder"` // 保存mp4的文件夹名,为空表示不需要保存mp4
+}
+
+type Data struct {
+ Id string `json:"id,omitempty"`
+ VideoUri string `json:"videoUri,omitempty"`
+}
+
+type Result struct {
+ Code int `json:"code,omitempty"` //
+ Data Data `json:"data,omitempty"` //
+ Msg string `json:"msg,omitempty"` //
+}
+
+type VideoOkResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data string `json:"data,omitempty"`
+}
+type ShardOk struct {
+ TaskId string `json:"taskId,omitempty"` // 文档md5
+ Id string `json:"id,omitempty"` // Id
+ Title *string `json:"title,omitempty"` // 标题
+ CoverImg *string `json:"coverImg,omitempty"` // 封面
+}
+
+type MediaTaskSyncReq struct {
+ AppId int64 `bson:"appId" json:"appId"` // APPID
+ LastUpdateTime time.Time `bson:"lastUpdateTime" json:"lastUpdateTime"` // 最后更新时间
+ PageSize int64 `bson:"pageSize" json:"pageSize"`
+}
+
+type MediaQueryeReq struct {
+ Id string `json:"id"`
+ TaskId string `json:"taskId"`
+}
+type MediaQueryesReq struct {
+ Ids []string `json:"ids"`
+}
+type VideoQueryResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaResp `json:"data,omitempty"`
+}
+type VideoQuerysResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data []MediaResp `json:"data,omitempty"`
+}
+type MediaSyncReq struct {
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+ Param *SyncParam `json:"param"`
+}
+type SyncParam struct {
+ Status string `bson:"status,omitempty" json:"status,omitempty"` // 当前状态
+ FirstTag string `json:"firstTag,omitempty"` // 一级标签
+ Publish string `json:"publish,omitempty"` // 视频发布者
+}
+type VideoSyncResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaSearchResp `json:"data,omitempty"`
+}
+
+// type MediaSearchResp struct {
+// Total int64 `json:"total"`
+// List []*MediaSync `json:"list"`
+// }
+
+type MediaResp struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"id"`
+ MediaSourceId primitive.ObjectID `bson:"mediaSourceId,omitempty" json:"mediaSourceId"` // 原始Id
+ VideoTime int32 `bson:"videoTime,omitempty" json:"videoTime"` // 影片长度
+ Size int64 `bson:"size,omitempty" json:"size"` // 文件size,单位byte
+ AppId int64 `bson:"appId,omitempty" json:"appId"` // 应用列表
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ VideoCover string `bson:"videoCover,omitempty" json:"videoCover"` // 视频封面
+ Title string `bson:"title,omitempty" json:"title"` // 标题
+ Publish string `bson:"publish,omitempty" json:"publish"` // 视频发布者
+ Remark string `bson:"remark,omitempty" json:"remark"` // 备注
+ Keyworld []string `bson:"keyworld,omitempty" json:"keyworld"` // 关键字
+
+ Nodes []videoinformationmod.Node `bson:"nodes" json:"nodes"` // 节点
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ Status string `bson:"status,omitempty" json:"status"` // 当前状态
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ TaskStartTime time.Time `bson:"taskStartTime,omitempty" json:"taskStartTime"` // 任务开始时间
+ SourceNameFs string `bson:"sourceNameFs,omitempty" json:"sourceNameFs"` // 远程源文件
+ IsDelLocalDir bool `bson:"isDelLocalDir,omitempty" json:"isDelLocalDir"` // 是否删除本机文件
+ WatermarkImage string `json:"watermarkImage" bson:"watermarkImage"` // 图片水印文件
+ WatermarkMedia string `json:"watermarkMedia" bson:"watermarkMedia"` // 视频水印文件
+ DownloadRetry int `bson:"downloadRetry,omitempty" json:"downloadRetry"` // 下载重试次数
+ WatermarkRetry int `bson:"watermarkRetry" json:"watermarkRetry"` // 水印重试次数
+ CompressRetry int `bson:"compressRetry" json:"compressRetry"` // 压缩重试次数
+ SegmentRetry int `bson:"segmentRetry" json:"segmentRetry"` // 切片重试次数
+ LinodeRetry int `bson:"linodeRetry" json:"linodeRetry"` // 上传重试次数
+
+ // 切片相关信息
+ Filename string `bson:"filename,omitempty" json:"filename"` // 文件名称
+ FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs"` // 文件名称
+ PreMp4FileNameFs string `bson:"preMp4FileNameFs,omitempty" json:"preMp4FileNameFs"` // 预览MP4文件
+ UnencryptedPreMp4 string `bson:"unencryptedPreMp4,omitempty" json:"unencryptedPreMp4"` // 不加密的预览MP4
+ TempFieldNameFs string `bson:"tempFieldNameFs,omitempty" json:"tempFieldNameFs"` // 临时存储
+ Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀
+ TaskID string `bson:"taskId,omitempty" json:"taskId"` // 项目组传来的唯一标示
+ Width int64 `bson:"width,omitempty" json:"width"` // 分辨率宽
+ Height int64 `bson:"height,omitempty" json:"height"` // 分辨率高
+ FrameRate string `bson:"frameRate,omitempty" json:"frameRate"` // 视频贞数
+ SyBitRate string `bson:"syBitRate,omitempty" json:"syBitRate"` // 音频bit率
+ VidBitRate string `bson:"vidBitRate,omitempty" json:"vidBitRate"` // 视频bit率
+ CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updatedAt"` // 更新时间
+ ProxyUpload *int32 `bson:"proxyUpload,omitempty" json:"proxyUpload"` // 代理上传
+ Retry int `bson:"retry" json:"retry"` // 重试次数 默认为零 最大重试3次
+ NotifySignal bool `bson:"notifySignal" json:"notifySignal"` //中止通知的信号 false 可以发送通知,true 中止发送通知
+
+ InfoTsName string `bson:"infoTsName" json:"infoTsName"` // 从该ts中获取视频分辨率等信息
+ SyncFlag int64 `bson:"syncFlag" json:"syncFlag"` // 同步状态
+ SrcKey string `bson:"srcKey,omitempty" json:"srcKey"` // 原始Key
+}
+
+func (info *MediaResp) ToVideoInfo() *videoinformationmod.VideoInformation {
+ rsl := &videoinformationmod.VideoInformation{
+ Id: mongos.Lid.VideoInfo.Get(),
+ Title: info.Title,
+ AddedTime: time.Time{},
+ Keyworld: info.Keyworld,
+ FirstTag: info.FirstTag,
+ SecondTags: info.SecondTags,
+ CoverImg: info.CoverImg,
+ UnencryptedImage: info.UnencryptedImage,
+ Desc: info.Desc,
+ Remark: info.Remark,
+ MediaType: videoinformationmod.MediaType_Video,
+ Status: videoinformationmod.InformationSatusWaiting,
+ Tags: make([]int64, 0),
+ Real: videoinformationmod.Flow{},
+ UpdatedAt: time.Now(),
+ CreatedAt: time.Now(),
+ }
+ rsl.Nodes = make([]videoinformationmod.Node, 0)
+ if info.Nodes != nil && len(info.Nodes) > 0 {
+ for _, node := range info.Nodes {
+ node.Videos = make([]videoinformationmod.VideoInfo, 0)
+ rsl.Nodes = append(rsl.Nodes, node)
+ }
+ }
+ if len(info.FieldNameFs) > 0 {
+ rsl.Video = videoinformationmod.VideoInfo{
+ TaskId: info.TaskID,
+ FileId: info.Id.Hex(),
+ Url: info.FieldNameFs,
+ Cover: info.VideoCover,
+ Height: info.Height,
+ Width: info.Width,
+ VideoTime: info.VideoTime,
+ PreMp4FileNameFs: info.PreMp4FileNameFs,
+ UnencryptedPreMp4: info.UnencryptedPreMp4,
+ }
+ if !strings.HasSuffix(rsl.Video.Url, ".m3u8") {
+ rsl.Video.Url += ".m3u8"
+ }
+ }
+ return rsl
+}
+
+type MediaType int
+
+const (
+ Preview MediaType = 1
+ Normal MediaType = 2
+)
+
+type FailReason string
+
+const (
+ FailReasonNone FailReason = ""
+ FailReasonUploadTimeoutError FailReason = "UploadTimeoutError" // 上传超时
+ FailReasonMergeError FailReason = "MergeError" // 合并错误
+ FailReasonCompressError FailReason = "CompressError" // 压缩错误
+ FailReasonSegmentError FailReason = "SegmentError" // 分片错误
+ FailReasonLinodeError FailReason = "LinodeError" // Linode错误
+)
+
+type BaseInfo struct {
+ ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
+ SrcId int64 `bson:"srcId,omitempty" json:"srcId"` // 原始ID
+ Title string `bson:"title,omitempty" json:"title"` // 标题
+ CheckSum string `bson:"checkSum,omitempty" json:"checkSum"` // md5校验码
+ VideoTime int32 `bson:"videoTime,omitempty" json:"videoTime"` // 影片长度
+ Publish string `json:"publish,omitempty" bson:"publish"` // 视频发布者
+ Type string `bson:"type,omitempty" json:"type"` // 长视频/短视频
+ Preview MediaType `bson:"preview,omitempty" json:"preview"` // 预览普通视频:1:预览 2:正片
+ Country string `bson:"country,omitempty" json:"country"` // 国家 (国产、日本、欧美.... 前端固定)
+ FirstTag string `bson:"firstTag,omitempty" json:"firstTag"` // 一级标签
+ SecondTags []string `bson:"secondTags,omitempty" json:"secondTags"` // 二级标签标签
+ CoverImg string `bson:"coverImg,omitempty" json:"coverImg"` // 封面
+ UnencryptedImage string `bson:"unencryptedImage,omitempty" json:"unencryptedImage"` // 不加密封面
+ PreFileName string `bson:"preFileName,omitempty" json:"preFileName"` // 预览文件
+ Status string `bson:"status,omitempty" json:"status"` // 当前状态
+ Desc string `bson:"desc,omitempty" json:"desc"` // 详细描述
+ Size int64 `bson:"size,omitempty" json:"size"` // 文件size,单位byte
+ Bango string `bson:"bango,omitempty" json:"bango"` // 番号,唯一
+ Directors []string `bson:"directors,omitempty" json:"directors"` // 导演
+ Actors []string `bson:"actors,omitempty" json:"actors"` // 演员
+ Studio string `bson:"studio,omitempty" json:"studio"` // 工作室/所有者
+ AddedTime time.Time `bson:"addedTime,omitempty" json:"addedTime"` // 上架时间
+ AppIds []int64 `bson:"appids,omitempty" json:"appids,omitempty"` // 应用列表
+
+ // 切片相关信息
+ Filename string `bson:"filename,omitempty" json:"filename"` // 文件名称
+ FieldNameFs string `bson:"fieldNameFs,omitempty" json:"fieldNameFs"` // 文件名称
+ Ext string `bson:"ext,omitempty" json:"ext"` // 视频后缀
+ TaskID string `bson:"taskId,omitempty" json:"taskId"` // 项目组传来的唯一标示
+ Width int `bson:"width,omitempty" json:"width"` // 分辨率宽
+ Height int `bson:"height,omitempty" json:"height"` // 分辨率高
+ Ratio float64 `bson:"ratio,omitempty" json:"ratio"` // 宽高比
+ FrameRate string `bson:"frameRate,omitempty" json:"frameRate"` // 视频贞数
+ SyBitRate string `bson:"syBitRate,omitempty" json:"syBitRate"` // 音频bit率
+ VidBitRate string `bson:"vidBitRate,omitempty" json:"vidBitRate"` // 视频bit率
+ CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt"` // 创建时间
+ UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updatedAt"` // 更新时间
+ ProxyUpload *int32 `bson:"proxyUpload,omitempty" json:"proxyUpload"` // 代理上传
+
+ IsAdd bool `bson:"isAdd,omitempty" json:"isAdd"` // 是否加入到本地
+ Retry int `bson:"retry" json:"retry"` // 重试次数 默认为零 最大重试3次
+ NotifySignal bool `bson:"notifySignal" json:"notifySignal"` //中止通知的信号 false 可以发送通知,true 中止发送通知
+ ShowLocation int64 `bson:"showLocation" json:"showLocation"` // 显示位置
+ Remark string `bson:"remark,omitempty" json:"remark"`
+
+ MergeRetry int `bson:"mergeRetry" json:"mergeRetry"` //合并重试次数
+ CompressRetry int `bson:"compressRetry" json:"compressRetry"` //压缩重试次数
+ SegmentRetry int `bson:"segmentRetry" json:"segmentRetry"` //切片重试次数
+ LinodeRetry int `bson:"linodeRetry" json:"linodeRetry"` //上传linode重试次数
+ FailReason FailReason `bson:"failReason" json:"failReason"` //失败原因
+ DeleteDisk bool `bson:"deleteDisk" json:"deleteDisk"` //是否删除了对于磁盘文件
+ InfoTsName string `bson:"infoTsName" json:"infoTsName"` //从该ts中获取视频分辨率等信息
+ UploadedPoses []int64 `bson:"uploadedPoses" json:"uploadedPoses"` //上传过的pos列表
+}
+
+type MediaUpdate struct {
+ ID string `json:"id,omitempty"`
+ Title *string `bson:"title,omitempty" json:"title,omitempty"` // 标题
+ AddedTime *time.Time `bson:"addedTime,omitempty" json:"addedTime,omitempty"` // 上架时间
+ Publish *string `json:"publish,omitempty" bson:"publish,omitempty"` // 视频发布者
+ Country *string `bson:"country,omitempty" json:"country,omitempty"` // 国家 (国产、日本、欧美....)
+ Preview *MediaType `bson:"preview,omitempty" json:"preview,omitempty"` // 预览普通视频:1:预览 2:正片
+ Type *string `bson:"type,omitempty" json:"type,omitempty"` // 长视频/短视频
+ FirstTag *string `bson:"firstTag,omitempty" json:"firstTag,omitempty"` // 一级标签
+ SecondTags *[]string `bson:"secondTags,omitempty" json:"secondTags,omitempty"` // 二级标签标签
+ Bango *string `bson:"bango,omitempty" json:"bango,omitempty"` // 番号,唯一
+ AppIds *[]int64 `bson:"appids,omitempty" json:"appids,omitempty"` // 应用列表
+ Desc *string `bson:"desc,omitempty" json:"desc,omitempty"` // 详细描述
+ PreFileName *string `bson:"preFileName,omitempty" json:"preFileName,omitempty"` // 预览文件
+ CoverImg *string `bson:"coverImg,omitempty" json:"coverImg,omitempty"` // 封面
+ UnencryptedImage *string `bson:"unencryptedImage,omitempty" json:"unencryptedImage,omitempty"` // 不加密封面
+ CreatedAt *time.Time `bson:"createdAt,omitempty" json:"createdAt,omitempty"` // 创建时间
+ UpdatedAt *time.Time `bson:"updatedAt,omitempty" json:"updatedAt,omitempty"` // 更新时间
+ ShowLocation *int64 `bson:"showLocation,omitempty" json:"showLocation"` // 显示位置
+ MoveInfo *MoveInfo `bson:"moveInfo,omitempty" json:"moveInfo,omitempty"` // 电影自动更新
+}
+type MoveInfo struct {
+ Score float64 `json:"score" bson:"score"` // 评分 暂无评分=0
+ SerialCount int64 `json:"serialCount" bson:"serialCount"` // 总集数 serialCount
+ SerialIndex int64 `json:"serialIndex" bson:"serialIndex"` // 集数 lastName
+ Likes int64 `json:"likes" bson:"likes"` // 点赞数量 favoriteCount
+ Watchs int64 `json:"watchs" bson:"watchs"` // 点赞数量 view
+ UpdateDate time.Time `json:"updateDate" bson:"updateDate"` // 更新时间 languagesplaylist 短剧和剧集有更新 电影
+ MoveTag []string `json:"moveTag" bson:"moveTag"` // 标签:cidMapper + post_Year + regional
+}
+type VideoSearch struct {
+ ID string `json:"id,omitempty"`
+ Title *string `json:"title,omitempty"` // 标题
+ TaskID *string `json:"taskId,omitempty"` // 项目组传来的唯一标示
+ Publish *string `bson:"publish,omitempty"` // 视频发布者
+ Type *string `json:"type"` // 视频:长视频/短视频--时间阈值
+ Preview *int32 `json:"preview"` // 预览:1:预览 2:普通视频
+ Country *string `json:"country"` // 国家 (国产、日本、欧美.... 前端固定)
+ Appid *int64 `json:"appid"` // 应用ID
+ FirstTag *string `json:"firstTag,omitempty"` // 一级标签
+ SecondTags *string `json:"secondTags,omitempty"` // 二级标签标签
+ Status *string `json:"status,omitempty"` // 当前状态
+ Directors *[]string `json:"director,omitempty"` // 导演
+ Actors *[]string `json:"actors,omitempty"` // 演员
+ Studio *string `json:"studio,omitempty"` // 工作室
+ Bango *string `json:"bango,omitempty"` // 番号,唯一
+ Via *string `json:"via,omitempty"` // 来源
+ Remark *string `json:"remark,omitempty"` // 备注
+ AddedTime *string `json:"addedTime,omitempty"` // 上架时间
+ UploadTime *string `json:"uploadTime,omitempty"` // 上传时间
+ Start *time.Time `json:"start"` // 开始时间
+ End *time.Time `json:"end"` // 结束时间
+ ShowLocation *int64 `json:"showLocation"` // 显示位置
+}
+
+type MediaSearchReq struct {
+ PageNum int64 `json:"pageNum"`
+ PageSize int64 `json:"pageSize"`
+ Param VideoSearch `json:"param"`
+}
+
+// MediaSearchReq 媒体搜索请求返回
+type MediaSearchResp struct {
+ Total int64 `json:"total"`
+ List []*BaseInfo `json:"list"`
+}
+
+type MediaSearchResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaSearchResp `json:"data,omitempty"`
+}
+
+type MediaDelReq struct {
+ ID string `json:"id,omitempty"`
+}
+
+// MediasDelResp 请求批量删除媒体回复
+type MediaDelResp struct {
+}
+
+type MediaUpdatePreTime struct {
+ ID string `json:"id,omitempty"`
+ PreFileTime int64 `bson:"preFileTime,omitempty" json:"preFileTime"` // 预览文件(开始时间)
+}
+
+type MediaDelResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ // Data MediaDelResp `json:"data,omitempty"`
+}
+
+type MediaUpdateReq struct {
+ //Ids []string `json:"id"`
+ MediaUpdate
+}
+
+// MediasUpdateResp 请求批量更新媒体回复
+type MediaUpdateResp struct {
+}
+
+type MediaUpdateResult struct {
+ Code int `json:"code,omitempty"`
+ Msg string `json:"msg,omitempty"` //
+ Data MediaUpdateResp `json:"data,omitempty"`
+}
+
+type AppAdvitiseInfoReq struct {
+ Id int64 `json:"id" binding:"required"`
+}
+type AdvitiseInfo struct {
+ Id string `json:"id" bson:"_id"` // 广告id
+ Location int64 `json:"location" bson:"location"` // 位置id
+ Title string `json:"title" bson:"title"` // 广告标题
+ Images string `json:"images" bson:"images"` // 视频banner
+ Link string `json:"link" bson:"link"` // 链接
+}
diff --git a/pub/storage/proxy/utils.go b/pub/storage/proxy/utils.go
new file mode 100644
index 0000000..7fc0a24
--- /dev/null
+++ b/pub/storage/proxy/utils.go
@@ -0,0 +1,139 @@
+package proxy
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ // 视频桶地址
+ BucketVideoAddressV1 = "http://ts.bbaazz.me"
+ // 图片桶地址
+ BucketImageAddressV1 = "http://img.bbaazz.me"
+ BucketImageAddressV2 = "http://tong3yuanimg.bbaazz.me"
+
+ // 上传路径
+ ImageUploadPathV1 = "/file/upload"
+ ImageUploadPathV2 = "/file/upload" // "/file/web/media/uploadImg"
+ VideoBodyPathV1 = "/file/web/proxy/videoBody"
+ VideoBodyPathV2 = "/file/web/proxy/videoBodyV2"
+ VideoOkPathV1 = "/file/web/proxy/videoOk"
+ VideoOkPathV2 = "/file/web/proxy/videoOkv2"
+)
+
+var (
+ BucketVideoAddressV2 = "http://139.162.22.207:24996" //测试
+)
+
+func InitBucketVideoAddress(addressV2 string) {
+ BucketVideoAddressV2 = addressV2
+}
+
+func DomainAddressRepair(domain string, defaultBucketAddress ...string) string {
+ if domain == "" {
+ if len(defaultBucketAddress) > 0 && defaultBucketAddress[0] != "" {
+ domain = defaultBucketAddress[0]
+ } else {
+ domain = BucketVideoAddressV2
+ }
+ }
+ return domain
+}
+
+func ImageUploadAddressRepair(domain string, defaultBucketAddress ...string) string {
+ domain = DomainAddressRepair(domain, defaultBucketAddress...)
+ if !strings.Contains(domain, ImageUploadPathV1) && !strings.Contains(domain, ImageUploadPathV2) {
+ if strings.HasPrefix(domain, BucketImageAddressV1) {
+ return domain + ImageUploadPathV1
+ } else if strings.HasPrefix(domain, BucketImageAddressV2) {
+ return domain + ImageUploadPathV2
+ }
+ return domain + ImageUploadPathV2
+ }
+ return domain
+}
+
+func VideoBodyAddressRepair(domain string, defaultBucketAddress ...string) string {
+ domain = DomainAddressRepair(domain, defaultBucketAddress...)
+ if strings.HasPrefix(domain, BucketVideoAddressV1) {
+ if !strings.Contains(domain, VideoBodyPathV1) {
+ return domain + VideoBodyPathV1
+ }
+ return domain
+ } else if strings.HasPrefix(domain, BucketVideoAddressV2) {
+ if !strings.Contains(domain, VideoBodyPathV2) {
+ return domain + VideoBodyPathV2
+ }
+ return domain
+ } else { // 未知存储桶,默认v2
+ if !strings.Contains(domain, VideoBodyPathV2) {
+ return domain + VideoBodyPathV2
+ }
+ return domain
+ }
+}
+
+func VideoOkAddressRepair(domain string, defaultBucketAddress ...string) string {
+ domain = DomainAddressRepair(domain, defaultBucketAddress...)
+ if strings.HasPrefix(domain, BucketVideoAddressV1) {
+ if !strings.Contains(domain, VideoOkPathV1) {
+ return domain + VideoOkPathV1
+ }
+ return domain
+ } else if strings.HasPrefix(domain, BucketVideoAddressV2) {
+ if !strings.Contains(domain, VideoOkPathV2) {
+ return domain + VideoOkPathV2
+ }
+ return domain
+ } else { // 未知存储桶,默认v2
+ if !strings.Contains(domain, VideoOkPathV2) {
+ return domain + VideoOkPathV2
+ }
+ return domain
+ }
+}
+
+func GetExistsUrlPathBucketAddress(path string) string {
+ if strings.HasPrefix(path, "v1/") {
+ return BucketVideoAddressV1
+ } else if strings.HasPrefix(path, "v2/") {
+ return BucketVideoAddressV2
+ } else if strings.HasPrefix(path, "v3/") {
+ return BucketVideoAddressV2
+ } else {
+ return BucketVideoAddressV1 // 欲漫涩、微博、快看禁漫 默认用桶2,其他的app,默认用桶1
+ }
+}
+
+func AppendV2PrefixToUrlPath(name string) string {
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ name = fmt.Sprintf("v2/%s", name)
+ }
+ return name
+}
+
+func AppendV1PrefixToUrlPath(name string) string {
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ name = fmt.Sprintf("v1/%s", name)
+ }
+ return name
+}
+
+func AppendsMediaS3Version(name string) string {
+ // 默认使用V1
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ return "/v2"
+ }
+ return ""
+}
+func AppendsMediaS3VersionV1(name string) string {
+ // 默认使用V1
+ if name != "" && !strings.HasPrefix(name, "v2/") && !strings.HasPrefix(name, "v1/") && !strings.HasPrefix(name, "v3/") &&
+ !strings.HasPrefix(name, "/v2") && !strings.HasPrefix(name, "/v1") && !strings.HasPrefix(name, "/v3") {
+ return "/v1"
+ }
+ return ""
+}
\ No newline at end of file
diff --git a/pub/storage/proxy/utils_test.go b/pub/storage/proxy/utils_test.go
new file mode 100644
index 0000000..4bcabed
--- /dev/null
+++ b/pub/storage/proxy/utils_test.go
@@ -0,0 +1,157 @@
+package proxy
+
+import "testing"
+
+func TestImageUploadAddressRepair(t *testing.T) {
+ type args struct {
+ domain string
+ defaultBucketAddress []string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{
+ domain: "http://aaa",
+ defaultBucketAddress: []string{},
+ },
+ want: "http://aaa/file/upload",
+ },
+ {
+ name: "b",
+ args: args{
+ domain: "http://ts.bbaazz.me",
+ defaultBucketAddress: []string{},
+ },
+ want: "http://ts.bbaazz.me/file/upload",
+ },
+ {
+ name: "c",
+ args: args{
+ domain: "http://resourcets.bbaazz.me",
+ defaultBucketAddress: []string{},
+ },
+ want: "http://resourcets.bbaazz.me/file/upload",
+ },
+ {
+ name: "d",
+ args: args{
+ domain: "",
+ defaultBucketAddress: []string{BucketImageAddressV1},
+ },
+ want: "http://ts.bbaazz.me/file/upload",
+ },
+ {
+ name: "e",
+ args: args{
+ domain: "",
+ defaultBucketAddress: []string{BucketImageAddressV2},
+ },
+ want: "http://resourcets.bbaazz.me/file/upload",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ImageUploadAddressRepair(tt.args.domain, tt.args.defaultBucketAddress...); got != tt.want {
+ t.Errorf("ImageUploadAddressRepair() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestVideoBodyAddressRepair(t *testing.T) {
+ type args struct {
+ domain string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{domain: "http://aaa"},
+ want: "http://aaa/file/web/proxy/videoBodyV2",
+ },
+ {
+ name: "b",
+ args: args{domain: "http://ts.bbaazz.me"},
+ want: "http://ts.bbaazz.me/file/web/proxy/videoBody",
+ },
+ {
+ name: "c",
+ args: args{domain: "http://resourcets.bbaazz.me"},
+ want: "http://resourcets.bbaazz.me/file/web/proxy/videoBodyV2",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := VideoBodyAddressRepair(tt.args.domain); got != tt.want {
+ t.Errorf("VideoBodyAddressRepair() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestVideoOkAddressRepair(t *testing.T) {
+ type args struct {
+ domain string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{domain: "http://aaa"},
+ want: "http://aaa/file/web/proxy/videoOkv2",
+ },
+ {
+ name: "b",
+ args: args{domain: "http://ts.bbaazz.me"},
+ want: "http://ts.bbaazz.me/file/web/proxy/videoOk",
+ },
+ {
+ name: "c",
+ args: args{domain: "http://resourcets.bbaazz.me"},
+ want: "http://resourcets.bbaazz.me/file/web/proxy/videoOkv2",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := VideoOkAddressRepair(tt.args.domain); got != tt.want {
+ t.Errorf("VideoOkAddressRepair() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestAppendV2PrefixToUrlPath(t *testing.T) {
+ type args struct {
+ name string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "a",
+ args: args{
+ name: "abc/aaa",
+ },
+ want: "v2/abc/aaa",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := AppendV2PrefixToUrlPath(tt.args.name); got != tt.want {
+ t.Errorf("AppendV2PrefixToUrlPath() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pub/storage/upload.go b/pub/storage/upload.go
new file mode 100644
index 0000000..646c5f0
--- /dev/null
+++ b/pub/storage/upload.go
@@ -0,0 +1,276 @@
+package storage
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "videoweb/pub/storage/proxy"
+
+ "strconv"
+)
+
+var (
+// buffer = bytes.Buffer{}
+// mtx sync.Mutex
+)
+
+// web后台上传使用, form-data方式上传
+func UploadImage(ctx context.Context, f *multipart.FileHeader, domain string) (path string, err error) {
+ if f == nil {
+ return "", errors.New("file is nil!")
+ }
+ var file multipart.File
+ if file, err = f.Open(); err != nil {
+ return
+ }
+
+ defer file.Close()
+
+ //mtx.Lock()
+ //defer mtx.Unlock()
+ buffer := bytes.Buffer{}
+ if _, err = buffer.ReadFrom(file); err != nil {
+ return
+ }
+ return proxy.UploadImg(f.Filename, buffer.Bytes(), domain)
+}
+
+func UploadVideoBody(ctx context.Context, form *multipart.Form, domain string) (*proxy.Result, error) {
+ if form == nil {
+ return nil, errors.New("multipart from is nil")
+ }
+ sb, err := makeShardBodyByForm(form)
+ if err != nil {
+ return nil, err
+ }
+ return proxy.UploadVideo(domain, sb)
+}
+
+func UploadVideoBodyV2(ctx context.Context, form *multipart.Form,
+ domain string, appIdIn int, publish string) (*proxy.Result, error) {
+ if form == nil {
+ return nil, errors.New("multipart from is nil")
+ }
+ sb, err := makeShardBodyByFormV2(form, appIdIn, publish)
+ if err != nil {
+ return nil, err
+ }
+ return proxy.UploadVideoV2(domain, sb)
+}
+
+func VideoBody(ctx context.Context, taskId, typ, domain string, pos int, data []byte) (*proxy.Result, error) {
+ body := &proxy.ShardBody{
+ TaskId: taskId,
+ Pos: pos,
+ FileData: base64.StdEncoding.EncodeToString(data),
+ Type: typ,
+ }
+ if typ == "1" {
+ body.Type = "AV"
+ } else if typ == "2" {
+ body.Type = "SP"
+ }
+ return proxy.UploadVideo(domain, body)
+}
+
+func UploadVideoOK(ctx context.Context, in *proxy.ShardOk, domain string) (*proxy.VideoOkResult, error) {
+ return proxy.PostOk(in, domain)
+}
+func UploadVideoOKV2(ctx context.Context, in *proxy.ShardOk, domain string) (*proxy.VideoOkResult, error) {
+ return proxy.PostOkV2(in, domain)
+}
+
+func QueryMedia(ctx context.Context, in *proxy.MediaQueryeReq, domain string) (*proxy.VideoQueryResult, error) {
+ return proxy.QueryMedia(in, domain)
+}
+func QueryMedias(ctx context.Context, in *proxy.MediaQueryesReq, domain string) (*proxy.VideoQuerysResult, error) {
+ return proxy.QueryMedias(in, domain)
+}
+
+func QueryAdvitiseList(ctx context.Context, in *proxy.AppAdvitiseInfoReq, domain string) (*proxy.AdvitiseQuerysResult, error) {
+ return proxy.QueryAdvitise(in, domain)
+}
+
+func SyncMedia(in *proxy.MediaTaskSyncReq, domain string) (result *proxy.VideoQuerysResult, err error) {
+ return proxy.SyncMedia(in, domain)
+}
+func makeShardBodyByForm(form *multipart.Form) (*proxy.ShardBody, error) {
+ poss := form.Value["pos"]
+ taskIds := form.Value["taskId"]
+ types := form.Value["type"]
+ fs := form.File["file"]
+ totalPoss := form.Value["totalChunks"]
+
+ var (
+ pos, total int
+ taskId string
+ typ string
+ f *multipart.FileHeader
+ err error
+ )
+
+ if len(fs) == 0 || fs[0] == nil {
+ return nil, errors.New("no file")
+ }
+ f = fs[0]
+
+ if len(poss) == 0 || poss[0] == "" {
+ return nil, errors.New("no pos")
+ }
+ if pos, err = strconv.Atoi(poss[0]); err != nil {
+ return nil, fmt.Errorf("invalid pos")
+ }
+ if len(totalPoss) == 0 || totalPoss[0] == "" {
+ return nil, errors.New("no totalPos")
+ }
+ if total, err = strconv.Atoi(totalPoss[0]); err != nil {
+ return nil, fmt.Errorf("invalid totalPos")
+ }
+
+ if len(taskIds) == 0 || taskIds[0] == "" {
+ return nil, errors.New("no taskId")
+ }
+ taskId = taskIds[0]
+
+ if len(types) == 0 || types[0] == "" {
+ return nil, errors.New("no type")
+ }
+ if types[0] == "1" {
+ typ = "AV"
+ } else if types[0] == "2" {
+ typ = "SP"
+ } else if types[0] == "3" {
+ typ = "AI"
+ } else {
+ typ = types[0]
+ }
+
+ file, ne := f.Open()
+ if ne != nil {
+ return nil, ne
+ }
+ defer file.Close()
+ //mtx.Lock()
+ //defer mtx.Unlock()
+ buffer := bytes.Buffer{}
+ if _, err = buffer.ReadFrom(file); err != nil {
+ return nil, err
+ }
+
+ resp := &proxy.ShardBody{
+ TaskId: taskId,
+ Pos: pos,
+ FileData: base64.StdEncoding.EncodeToString(buffer.Bytes()),
+ Type: typ,
+ TotalPos: total,
+ }
+ return resp, nil
+}
+
+func makeShardBodyByFormV2(form *multipart.Form, appIdIn int, publishIn string) (
+ *proxy.ShardBodyV2, error) {
+ taskIds := form.Value["taskId"]
+ types := form.Value["type"]
+ poss := form.Value["pos"]
+ fs := form.File["file"]
+ totalPoss := form.Value["totalChunks"]
+ fileNames := form.Value["filename"]
+ appIds := form.Value["appId"]
+ publishs := form.Value["publish"]
+
+ var (
+ pos, total int
+ taskId string
+ f *multipart.FileHeader
+ fileName string
+ typ string
+ publish string
+ appId int
+ err error
+ )
+
+ if len(fs) == 0 || fs[0] == nil {
+ return nil, errors.New("no file")
+ }
+ f = fs[0]
+
+ if len(poss) == 0 || poss[0] == "" {
+ return nil, errors.New("no pos")
+ }
+ if pos, err = strconv.Atoi(poss[0]); err != nil {
+ return nil, fmt.Errorf("invalid pos")
+ }
+ if len(totalPoss) == 0 || totalPoss[0] == "" {
+ return nil, errors.New("no totalPos")
+ }
+ if total, err = strconv.Atoi(totalPoss[0]); err != nil {
+ return nil, fmt.Errorf("invalid totalPos")
+ }
+
+ if len(taskIds) == 0 || taskIds[0] == "" {
+ return nil, errors.New("no taskId")
+ }
+ taskId = taskIds[0]
+
+ if len(types) == 0 || types[0] == "" {
+ return nil, errors.New("no type")
+ }
+ if types[0] == "1" {
+ typ = "AV"
+ } else if types[0] == "2" {
+ typ = "SP"
+ } else if types[0] == "3" {
+ typ = "AI"
+ } else {
+ typ = types[0]
+ }
+
+ if len(fileNames) == 0 || fileNames[0] == "" {
+ return nil, errors.New("no fileName")
+ }
+ fileName = fileNames[0]
+
+ if len(publishs) == 0 || publishs[0] == "" {
+ return nil, errors.New("no publish")
+ }
+ publish = publishs[0]
+ if publish == "" {
+ publish = publishIn
+ }
+
+ if len(appIds) == 0 || appIds[0] == "" {
+ return nil, errors.New("no appId")
+ }
+ if appId, err = strconv.Atoi(appIds[0]); err != nil {
+ return nil, fmt.Errorf("invalid appId")
+ }
+
+ if appId == 0 {
+ appId = appIdIn
+ }
+
+ file, ne := f.Open()
+ if ne != nil {
+ return nil, ne
+ }
+ defer file.Close()
+ buffer := bytes.Buffer{}
+ if _, err = buffer.ReadFrom(file); err != nil {
+ return nil, err
+ }
+
+ resp := &proxy.ShardBodyV2{
+ TaskId: taskId,
+ Pos: pos,
+ FileData: base64.StdEncoding.EncodeToString(buffer.Bytes()),
+ Type: typ,
+ TotalPos: total,
+ FileName: fileName,
+ AppId: appId,
+ Publish: publish,
+ }
+ return resp, nil
+}
diff --git a/read.md b/read.md
new file mode 100644
index 0000000..e69de29
评论
+