commit 3639d2184aa5f999481bee51d9b1a2fe2a5d049f Author: 谢宇宁 Date: Thu Oct 22 14:47:17 2026 +0700 Initial commit Co-Authored-By: Claude Opus 5.5 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..c5203ac --- /dev/null +++ b/app/api/web/application.go @@ -0,0 +1,23 @@ +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": "福利应用", + "pageName": "应用推荐", + "navActive": "application", + "categories": vm.Categories, + } + if len(vm.Categories) == 0 { + setNoIndex(data, true) + } + HTMLOK(c, "application.html", data) +} 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..2cc091d --- /dev/null +++ b/app/api/web/chapter_detail.go @@ -0,0 +1,45 @@ +package web + +import ( + "net/http" + "strconv" + "strings" + "videoweb/app/service/playletser" + "videoweb/pub/common/log" + + "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 + } + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": "success", + "tip": "成功", + "data": data, + }) +} diff --git a/app/api/web/comment.go b/app/api/web/comment.go new file mode 100644 index 0000000..bc89b64 --- /dev/null +++ b/app/api/web/comment.go @@ -0,0 +1,54 @@ +package web + +import ( + "net/http" + "strconv" + "videoweb/app/service/commentser" + "videoweb/pub/common/log" + + "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, + }) +} 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..ce31a55 --- /dev/null +++ b/app/api/web/dialog.go @@ -0,0 +1,32 @@ +package web + +import ( + "net/http" + "videoweb/app/service/configser" + + "github.com/gin-gonic/gin" +) + +var dialogTemplates = map[string]string{ + "download": "downloadAppDialog", + "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..5307e7a --- /dev/null +++ b/app/api/web/feedback.go @@ -0,0 +1,198 @@ +package web + +import ( + "net/http" + "strings" + "unicode/utf8" + "videoweb/app/service/authser" + "videoweb/app/service/feedbackser" + "videoweb/pub/common/log" + + "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) { + authser.EnsureGuestOnEnter(c) + + 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) { + authser.EnsureGuestOnEnter(c) + 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/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..680c498 --- /dev/null +++ b/app/api/web/index.go @@ -0,0 +1,23 @@ +package web + +import ( + "videoweb/app/service/playletser" + + "github.com/gin-gonic/gin" +) + +// Index 首页 SSR:拉取 /api/app/playlet/home,直接用接口字段渲染。 +func Index(c *gin.Context) { + 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), + } + 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..cdf94aa --- /dev/null +++ b/app/api/web/play.go @@ -0,0 +1,70 @@ +package web + +import ( + "strings" + "videoweb/app/service/playletser" + + "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) + + 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/rank.go b/app/api/web/rank.go new file mode 100644 index 0000000..0dbb155 --- /dev/null +++ b/app/api/web/rank.go @@ -0,0 +1,83 @@ +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": "排行榜", + "pageName": "排行榜", + "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..11b087b --- /dev/null +++ b/app/api/web/render.go @@ -0,0 +1,250 @@ +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 / 导航 / 公共配置,并确保存在游客会话。 +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) + } + 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() + 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) + + c.HTML(code, name, data) +} + +// buildPageConsoleData 构建浏览器控制台使用的页面返回数据快照。 +// 快照不包含公共配置等大字段。 +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": + 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)) +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return v + } + } + return "" +} diff --git a/app/api/web/render_test.go b/app/api/web/render_test.go new file mode 100644 index 0000000..cfae114 --- /dev/null +++ b/app/api/web/render_test.go @@ -0,0 +1,64 @@ +package web + +import ( + "net/http" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestBuildPageConsoleDataFiltersCommonFields(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", + "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", "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..5d353b1 --- /dev/null +++ b/app/api/web/richtext.go @@ -0,0 +1,90 @@ +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, "pageName": title} + applyRichtextIndexPolicy(data, title) + if title == "帮助中心" { + data["faqItems"] = []seoser.FAQItem{ + {Question: "付费剧集怎么观看?", Answer: "网页端可免费观看免费剧集;付费剧集请下载黄果短剧 App 观看完整内容。"}, + {Question: "需要注册账号吗?", Answer: "不需要,网页端无需注册即可直接观看免费剧集。"}, + {Question: "视频无法播放或加载缓慢?", Answer: "建议切换网络、刷新页面或更新浏览器;问题持续时可提交反馈。"}, + {Question: "在哪里下载 App?", Answer: "点击页面上的“下载APP”入口,按提示扫码或直接下载安装。"}, + } + } + HTMLOK(c, fallbackTemplate, data) + return + } + } + renderRichtextData(c, title, title, page) + } +} + +func renderRichtext(c *gin.Context, title, pageName string) { + title = strings.TrimSpace(title) + page := configser.FetchRichTextByTitle(c, title) + renderRichtextData(c, title, pageName, page) +} + +// pageName 为面包屑名称,与上游富文本标题可能不同。 +func renderRichtextData(c *gin.Context, title, pageName 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, + "pageName": pageName, + } + 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..7d7e06c --- /dev/null +++ b/app/api/web/session.go @@ -0,0 +1,12 @@ +package web + +import ( + "videoweb/app/service/authser" + + "github.com/gin-gonic/gin" +) + +// TryJSONSessionRecover 上游返回会话失效时重建游客会话;返回 true 表示调用方应重试一次。 +func TryJSONSessionRecover(c *gin.Context, code int) bool { + return authser.RecoverContentSession(c, code) +} 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..41edfc7 --- /dev/null +++ b/app/api/web/today_update.go @@ -0,0 +1,34 @@ +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": "今日更新", + "pageName": "今日更新", + "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/appg/appg.go b/app/appg/appg.go new file mode 100644 index 0000000..e634012 --- /dev/null +++ b/app/appg/appg.go @@ -0,0 +1,26 @@ +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"` +} + +// 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 +} diff --git a/app/conf/app.yaml b/app/conf/app.yaml new file mode 100644 index 0000000..a97e69b --- /dev/null +++ b/app/conf/app.yaml @@ -0,0 +1,6 @@ +serverAddr: "https://webhgdjapi.mfcg.me" +listenAddr: "25119" +dhListenAddr: "26812" +runMod: "dev" +tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN" + 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..86b9c19 --- /dev/null +++ b/app/deploy_prod.sh @@ -0,0 +1,16 @@ +#!/bin/bash +echo "开始" +rm -rf ./web_huangguo_h5 ./web_huangguo_h5.tar.gz +GOOS=linux GOARCH=amd64 go build -o web_huangguo_h5 +tar zvcf web_huangguo_h5.tar.gz web_huangguo_h5 templates/ static/ \ + --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' + + +scp web_huangguo_h5.tar.gz root@172.104.166.87:/data/uzip +ssh root@172.104.166.87 "/server/web_huangguo/h5/up_app.sh" + +rm -rf ./web_huangguo_h5 ./web_huangguo_h5.tar.gz \ 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..bf4055c --- /dev/null +++ b/app/router/gin.go @@ -0,0 +1,276 @@ +package router + +import ( + "encoding/json" + "html/template" + "net/http" + "os" + "path/filepath" + "strings" + "videoweb/app/api/web" + "videoweb/app/appg" + "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 + 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..c1fa744 --- /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}} + + {{$ad.Title}} + + {{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..b12d47e --- /dev/null +++ b/app/templates/base/dialogAd.html @@ -0,0 +1,39 @@ +{{define "dialogAd"}} +{{if .appConfig.DialogList}} + + + +{{end}} +{{end}} diff --git a/app/templates/base/downloadAppDialog.html b/app/templates/base/downloadAppDialog.html new file mode 100644 index 0000000..812bddd --- /dev/null +++ b/app/templates/base/downloadAppDialog.html @@ -0,0 +1,25 @@ +{{define "downloadAppDialog"}} +
+
+ + +
+
+{{end}} diff --git a/app/templates/base/downloadAppFloat.html b/app/templates/base/downloadAppFloat.html new file mode 100644 index 0000000..737def0 --- /dev/null +++ b/app/templates/base/downloadAppFloat.html @@ -0,0 +1,40 @@ +{{define "downloadAppFloat"}} {{if .appConfig.AppAddr}} + + + 下载黄果短剧APP + 下载 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"}} + + + +{{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..77957a9 --- /dev/null +++ b/app/templates/base/footer.html @@ -0,0 +1,185 @@ +{{define "footer"}} + +
+ + + + + + {{if or .appConfig.NewURLAddr .appConfig.NewURLAddr1 .appConfig.ForeverURLAddr}} + + {{end}} +
+
+ +
+ + +{{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..d307886 --- /dev/null +++ b/app/templates/base/header.html @@ -0,0 +1,173 @@ +{{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..e1e4bd3 --- /dev/null +++ b/app/templates/base/homeAdRow.html @@ -0,0 +1,30 @@ +{{define "homeAdRow"}} +{{$ads := .Ads}} +{{if $ads}} + +
+ {{range $ad := $ads}} + + {{$ad.Title}} + + {{end}} +
+{{end}} +{{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/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..2e702e4 --- /dev/null +++ b/app/templates/base/seoCommon.html @@ -0,0 +1,75 @@ +{{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 "analytics" .}} + +{{end}} diff --git a/app/templates/base/squarePromo.html b/app/templates/base/squarePromo.html new file mode 100644 index 0000000..3c7aa95 --- /dev/null +++ b/app/templates/base/squarePromo.html @@ -0,0 +1,48 @@ +{{define "squarePromo"}} +{{$ads := .Ads}} +{{if $ads}} + +
+
+
+ {{range $ad := $ads}} + + + {{$ad.Title}} + + {{$ad.Title}} + + {{end}} +
+
+
+ +{{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/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" .}} +
+
+ +
+ 页面走丢了 +
+

+ {{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日
+
+
+ {{if .appConfig.Telegram}} + + + 商务合作 + + {{else}} + 商务合作 + {{end}} + {{if .appConfig.GmailAddr}} + + + 邮箱 + + {{else}} + 邮箱 + {{end}} + {{if .appConfig.TwitterAddr}} + + + 推特 + + {{else}} + 推特 + {{end}} + {{if .appConfig.TGGroup}} + + + TG群 + + {{else}} + TG群 + {{end}} + {{if .appConfig.AppAddr}} + + + APP + + {{else}} + APP + {{end}} + {{if .appConfig.PotaoGroup}} + + + 福利应用 + + {{else}} + 福利应用 + {{end}} +
+
+
+ {{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}}

+ {{if $cat.Intro}} +

+ {{$cat.Intro}} +

+ {{end}} +
+
+ {{range $cat.Apps}} + +
+ {{if .Logo}} + {{.Name}} + {{else}} + {{.Name}} + {{end}} +
+
{{.Name}}
+ {{if .Intro}} +
{{.Intro}}
+ {{end}} +
+ {{else}} +
该分类暂无应用
+ {{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}}

+

{{.categoryDescription}}

+
+

{{.pageName}}作品

+ + {{template "pagination" .}} +
+
+{{template "footer" .}} + + diff --git a/app/templates/page/detail.html b/app/templates/page/detail.html new file mode 100644 index 0000000..fb2d77f --- /dev/null +++ b/app/templates/page/detail.html @@ -0,0 +1,177 @@ + + + + + + {{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}} + +
+ + 返回 + +
+ + + 反馈 + +
+
+
+ +
+
+
+ + {{.Title}} + {{if .IsFeatured}}爆剧{{else if .IsRecommend}}黄果原创{{else if .IsNew}}上新{{end}} + {{if .TotalEpisode}}{{if eq .UpdateStatus 2}}全{{.TotalEpisode}}集{{else}}更新至{{.TotalEpisode}}集{{end}}{{end}} + + +
+
+ + 黄果短剧官方 +
+

{{.Title}}

+
+ {{updateStatusText .UpdateStatus}} + {{if .TotalEpisode}}共 {{.TotalEpisode}} 集{{end}} + {{browseText .CountBrowse}} + {{with playletDateText .OnlineAt .MediaCreatedAt .CreatedAt}}{{.}} 上线{{end}} +
+

{{.Title}}剧情简介

+

{{if .Desc}}{{.Desc}}{{else}}暂无简介{{end}}

+ + {{$detailTags := namedTags .TagIDs .TagNames .TagSlugs}} + {{if $detailTags}} +
+ {{range $detailTags}} + {{if .URL}}{{.Name}}{{else}}{{.Name}}{{end}} + {{end}} +
+ {{end}} + {{$themes := namedThemeTags .ThemeIDs .ThemeNames .ThemeSlugs}} + {{if $themes}} +
+ 所属专题 + {{range $themes}} + {{if .URL}}{{.Name}}{{else}}{{.Name}}{{end}} + {{end}} +
+ {{end}} +
+ 立即播放 + + 反馈 +
+
+
+ + {{template "contentPromo" $}} + +
+
+
+

{{.Title}}选集

+
{{if .TotalEpisode}}共 {{.TotalEpisode}} 集 · {{end}}{{updateStatusText .UpdateStatus}}
+
+ 点击集数直接播放 +
+ {{if gt (len $.episodeRanges) 1}} +
+ {{range $i, $r := $.episodeRanges}} + + {{end}} +
+ {{end}} +
+ {{$playletID := .ID}} + {{$playletSlug := .Slug}} + {{range .PlayletChapterInfo}} + {{if .AllowWatch}} + {{printf "%02d" .CurrentEpisode}} + {{else}} + {{printf "%02d" .CurrentEpisode}} + {{end}} + {{else}} +
暂无剧集
+ {{end}} +
+
+ + {{template "contentOfficialInfo" $}} + {{template "contentAds" $}} + +
+
+

评论

+
+ + +
+
+
+
评论加载中…
+
+ +
+
+ + +
+
+ +{{end}} +{{else}} +
+

{{$pageTitle}} - {{$siteTitle}}

+ {{template "empty" .}} +
+{{end}} +{{template "footer" .}} + + + 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 账号同步。 +
+
+
+
更新及时提醒
+
+ 关注的剧集更新后提醒,减少回访流失。 +
+
+ +
+
+ + +
+
+ 下载二维码 +
+ +
二维码与下载链接指向同一地址
+
+
+
+ {{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" .}} +
+ + + +
+ {{template "footer" .}} + + + diff --git a/app/templates/page/helpCenter.html b/app/templates/page/helpCenter.html new file mode 100644 index 0000000..a0f862c --- /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}} +
+

付费剧集怎么观看?

+

网页端可免费观看免费剧集;付费剧集请下载黄果短剧 App 观看完整内容。

+
+
+
+

需要注册账号吗?

+

不需要,网页端无需注册即可直接观看免费剧集。

+
+
+
+

视频无法播放或加载缓慢?

+

建议切换网络、刷新页面或更新浏览器;问题持续时可提交反馈。

+
+
+
+

在哪里下载 App?

+

点击页面上的“下载APP”入口,按提示扫码或直接下载安装。

+
+ {{end}} +
+
+
+ {{template "footer" .}} + + diff --git a/app/templates/page/homeRoad.html b/app/templates/page/homeRoad.html new file mode 100644 index 0000000..f98dfb3 --- /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}} +
+
+ +
+
+ +

防失联方式

+ 建议至少保存 2 种 +
+

通过以下方式获取最新地址或接收线路变更提醒。

+
+ {{range .homeRoad.Contacts}} + +

{{.Title}}

+

{{.Description}}

+ {{.Action}} +
+ {{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..cefc205 --- /dev/null +++ b/app/templates/page/index.html @@ -0,0 +1,216 @@ + + + + + + {{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}} + +
+
+
+
+ {{end}} + + {{$cfg := .appConfig}} + {{if or .announcements $cfg.ForeverURLAddr $cfg.NewURLAddr $cfg.NewURLAddr1}} +
+
+ + + 公告 + +
+
+
+ {{template "homeAnnouncementItems" .}} + +
+
+
+ {{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}} +
+
+
+

专题推荐

+
+
+ +
查看更多
+ 查看全部 +
+
+
+ {{range $theme := .homeRecommendThemes}} + + {{$theme.Name}}专题 + + + {{$theme.PlayletCount}}部剧情 + {{$theme.Name}} + + + {{end}} +
+
+ {{end}} +
+{{template "footer" .}} + + + + + +{{/* 跑马灯内容:后台公告在前,随后是官方访问地址 */}} +{{define "homeAnnouncementItems"}}{{$cfg := .appConfig}}{{$n := 0}}{{range .announcements}}{{if $n}}{{end}}{{$n = 1}}{{if .Title}}{{.Title}}{{if .Content}}:{{end}}{{end}}{{.Content}}{{end}}{{if $cfg.ForeverURLAddr}}{{if $n}}{{end}}{{$n = 1}}海外永久地址:{{$cfg.ForeverURLAddr}}{{end}}{{if $cfg.NewURLAddr}}{{if $n}}{{end}}{{$n = 1}}最新地址:{{$cfg.NewURLAddr}}{{end}}{{if $cfg.NewURLAddr1}}{{if $n}}{{end}}{{$n = 1}}备用地址:{{$cfg.NewURLAddr1}}{{end}}请收藏以上地址,防止迷路{{end}} diff --git a/app/templates/page/list.html b/app/templates/page/list.html new file mode 100644 index 0000000..d34c70c --- /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}}在线观看

+ +

{{.sectionTitle}}

+ + {{template "pagination" .}} + {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "list_nine_grid" "SlotName" "片库横幅广告" "Label" "广告"}} + {{if .appConfig.BottomBanner}} +
+ {{range $ad := .appConfig.BottomBanner}} + + {{$ad.Title}} + + {{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 .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..abf5119 --- /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..4dbab38 --- /dev/null +++ b/app/templates/page/play.html @@ -0,0 +1,399 @@ + + + + + + {{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}} +{{if $chapter}} +{{$canWatch = $chapter.AllowWatch}} +{{$needUnlock = $chapter.NeedUnlock}} +{{$needVip = $chapter.NeedVip}} +{{end}} +{{if or $chapter $playlet}} +
+ +
+
+
+ {{if and $chapter $canWatch $chapter.VideoURL}} +
+ {{end}} + {{if $playlet}} + {{$playlet.Title}} + {{else}} + 短剧封面 + {{end}} + {{if and $chapter $canWatch $chapter.VideoURL}} + + {{else if and $chapter (not $canWatch)}} +
+

本集为APP专享剧集

+ +
+ {{end}} + + + + +
+ + 返回 + +
{{if $chapter}}第{{$chapter.CurrentEpisode}}集{{else}}播放{{end}}
+
+ +
+ + 反馈 + +
+ +
+ + +
+ +
+
+ +
+ {{if $playlet}} + {{if $playlet.TagNames}} +
+ {{range namedTags $playlet.TagIDs $playlet.TagNames $playlet.TagSlugs}} + {{if .URL}}{{.Name}}{{else}}{{.Name}}{{end}} + {{end}} +
+ {{end}} +
+

{{if $playlet.Desc}}{{$playlet.Desc}}{{else}}暂无简介{{end}}

+ +
+ {{end}} +
+ + +
+ +
+ + + +
+ + {{template "contentPromo" $}} + +
+
+
{{if $chapter}}第 {{$chapter.CurrentEpisode}} 集{{else}}—{{end}}
+

{{if $playlet}}{{$playlet.Title}}{{else}}短剧播放{{end}}

+ {{if $playlet}} + {{if $playlet.TagNames}} +
+ {{range namedTags $playlet.TagIDs $playlet.TagNames $playlet.TagSlugs}} + {{if .URL}}{{.Name}}{{else}}{{.Name}}{{end}} + {{end}} +
+ {{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}} +
+
+ + + + + 反馈 + + 反馈 + +
+ 下载APP +
+ +
+

剧集

+ {{with $.episodeRanges}} +
+ {{range $i, $r := .}} + + {{end}} +
+ {{end}} +
+ {{range $.chapters}} + {{$active := and $chapter (eq .ID $chapter.ID)}} + {{if .AllowWatch}} + + {{.CurrentEpisode}} + + {{else}} + + {{.CurrentEpisode}} + + + {{end}} + {{else}} +
暂无剧集
+ {{end}} +
+
+ + {{template "contentOfficialInfo" $}} + {{template "contentAds" $}} + + {{if $chapter}} +
+
+

评论

+ +
+ + +
+
+
+
评论加载中…
+
+ +
+ {{end}} +
+ + +
+
+ + + + + + + +{{if $chapter}} + +{{end}} +{{else}} +
+

短剧播放 - 黄果短剧

+ {{template "empty" .}} +
+{{end}} + +{{template "footer" .}} + + + + 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..d475b69 --- /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" .}} +
+
+ +
+

全站排行榜

+
聚合站内播放、评分和近期热度表现突出的短剧内容,快速发现当前值得看的作品。
+
+
+ {{range .tabs}} + + {{.Label}} + + {{end}} +
+ + {{template "pagination" .}} + {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "rank_nine_grid" "SlotName" "排行榜横幅广告" "Label" "广告"}} + {{if .appConfig.BottomBanner}} +
+ {{range $ad := .appConfig.BottomBanner}} + + {{$ad.Title}} + + {{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.Subtitle}} +

{{.richtext.Subtitle}}

+ {{end}} +
+
+ +
+ + 返回 + +
+ {{if .richtext.Title}}{{.richtext.Title}}{{else}}内容详情{{end}} +
+ +
+ +
+ {{if .richtext.Found}} +
+
{{.richtext.ContentHTML}}
+
+ {{if eq .richtextKey "关于我们"}} +
+ {{if .appConfig.Telegram}} + + + 商务合作 + + {{else}} + 商务合作 + {{end}} + {{if .appConfig.GmailAddr}} + + + 邮箱 + + {{else}} + 邮箱 + {{end}} + {{if .appConfig.TwitterAddr}} + + + 推特 + + {{else}} + 推特 + {{end}} + {{if .appConfig.TGGroup}} + + + TG群 + + {{else}} + TG群 + {{end}} + {{if .appConfig.AppAddr}} + + + APP + + {{else}} + APP + {{end}} + {{if .appConfig.PotaoGroup}} + + + 福利应用 + + {{else}} + 福利应用 + {{end}} +
+ {{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..bd70436 --- /dev/null +++ b/app/templates/page/search.html @@ -0,0 +1,155 @@ + + + + + + {{template "seoCommon" .}} + + {{if .seoInfo.Title}}{{.seoInfo.Title}}{{else if .title}}{{.title}} - + 黄果短剧{{else}}搜索短剧 - 黄果短剧{{end}} + + + + + + + + + + + {{template "header" .}} +
+
+ + +

+ {{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..ad87a9b --- /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成人剧集在线观看

+ +

{{.sectionTitle}}

+ + {{template "pagination" .}} + {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "series_nine_grid" "SlotName" "剧集横幅广告" "Label" "广告"}} + {{if .appConfig.BottomBanner}} +
+ {{range $ad := .appConfig.BottomBanner}} + + {{$ad.Title}} + + {{end}} +
+ {{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 .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..30a7c03 --- /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成人短剧专题

+ +
+ {{range .list}} + + {{.Name}}专题 + + + {{.PlayletCount}}部剧情 + {{.Name}} + + + {{else}} +
暂无专题内容
+ {{end}} +
+ + {{template "pagination" .}} + + {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "topic_nine_grid" "SlotName" "专题横幅广告" "Label" "广告"}} + + {{if .appConfig.BottomBanner}} +
+ {{range $ad := .appConfig.BottomBanner}} + + {{$ad.Title}} + + {{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" .}} +
+
+ +
+

用户协议

+

使用黄果短剧服务前,请仔细阅读以下条款

+
+
+ +
+ + 返回 + +
用户协议
+ +
+ +
+
+ + +
+
+
+ {{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..f0f3fd9 --- /dev/null +++ b/pub/pubdef/pubdef.go @@ -0,0 +1,20 @@ +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 +) + +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

曲阜师范大学学生【何鑫】的私密自拍视频流出,尺度炸裂,令人瞠目!这位清纯外表的学妹私下化身小母狗,为完成神秘“主人”的任务大秀骚浪本性!视频中,她身着宽松裸体背带裤,半遮半掩的布料勾勒出白嫩胴体,饱满小胸和粉嫩美穴若隐若现。

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 1

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 2

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 3

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 4

何鑫裸体自拍玩穴

她站在宿舍镜前,挑逗地拉下背带裤,露出湿漉漉的私处,手指大胆深入玩穴,湿滑水声混杂娇喘,镜头特写捕捉到她揉搓嫩穴的每一个细节,令人血脉喷张!

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 5

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 6

曲阜师范大学05年小母狗何鑫裸体自拍玩穴 7

何鑫足交伺候主人

何鑫还完成足交任务,赤裸坐在床边,用白嫩玉足轻抚主人粗大肉棒,脚趾灵活挑逗,油亮光泽的脚掌摩擦到主人爽到低吼!

关键词:#曲阜师范大学何鑫 #何鑫裸体自拍 #何鑫对镜玩穴自慰 #何鑫足交伺候主人 #何鑫不雅自拍视频 #曲阜师范大学 #何鑫 #自慰 #足交 #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